Skip to main content

Project file organization

As time will pass, your Dagger configuration will grow. You will feel the need to better organise your config by splitting it into multiple files.

The simplest next step is to rely on the root module.

What is the root module?

A module in CUE is any directory including a cue.mod folder. It makes for the prefix/root of importable packages (e.g, is a module)

Anonymous module (default)

The default module name is an empty string, which means that it's an anonymous module. Anonymous modules are used when we don't need to import other packages. All Dagger configs start here: a plan represented by a single CUE file, e.g. plan.cue, which imports everything from third-party packages, like

When to use a module

You can add a different module name if you want to import other packages from inside your module. This is required so you have a prefix/root before your imports. This is when you want to split your code into multiple files.

The module path becomes the import path prefix for packages in the module. This might be the name of a domain you own or another name you control (such as your company name), even your email, followed optionally by a descriptive path (e.g., project name).

You import packages by prefixing the module name they're a part of, plus the path to them, relative to the cue.mod directory.

root                    // <- this is a module because it includes a cue.mod dir
|-- cue.mod
| |-- module.cue // module: ""
|-- schemas
| |-- compose // <- this is a package because it includes files with a package directive
| | |-- spec.cue // package compose
|-- plan.cue // import ""


Consider the root module as the URL to access the root of your project. Any subfolder inside this module needs to have CUE files with a package name equivalent to the directory name. File names inside each directory are not important, the package name is:

$  ls
bar cue.mod foo

$ cat foo/main.cue
cat foo/main.cue
package foo

$ cat bar/anything.cue
package bar

To reference CUE code from within these packages:

"" // imports CUE code inside `/foo`
"" // imports CUE code inside `/bar`

Initializing the root module

Project non initialized

The module name can be set during project initialisation: dagger project init --name <NAME>.

As mentioned above, the module name has to at least follow the structure of a domain: domain.extension or domain.extension/project. Email addresses can also be used for the module name, and the referenced domain doesn't necessarly have to exist.

In our case, let's use a fake email address, for convenience purposes:

dagger project init --name ""

Project already initialized

Manually edit the cue.mod/module.cue with the desired name:

$ cat cue.mod/module.cue
module: ""

Creating packages in subdirectories


Let's put everything above into practice:

  • Initialize the workdir environment
mkdir daggerTest && cd daggerTest
  • Initialize the project
dagger project init --name ""
  • Update dagger and universe dependencies
dagger project update
  • Create 2 subfolders: foo and bar:
mkdir foo bar
  • Inside the bar folder, create a CUE file with any name and whose package is bar (same as parent directory):
package bar

#Test: "world"
  • Inside the foo folder, create a CUE file with any name and whose package is foo (same as parent directory):
package foo

import (


#Foo: {
_img: alpine.#Build & {
packages: {
bash: _

bash.#Run & {
env: TEST: bar.#Test
always: true
input: _img.output

Our #Foo definition is just a wrapper around bash.#Run that sets the input field to the bash image (_img key above). Please notice that the _img step needs to reside inside the #Foo definition so that it is available to the action context.

For example, this doesn't work:

package foo

import (

"" // reference to the bar package

// _img is not being passed to the action as it lives outside #Foo
_img: alpine.#Build & {
packages: {
bash: _

#Foo: bash.#Run & {
env: TEST: bar.#Test // here, we reference the #Test definition present in the bar directory
always: true
input: _img.output
  • At the root of the project, create your main.cue file (file and package names are arbitrary):
package main

import (

"" // reference to the foo package

dagger.#Plan & {
actions: {
hello: foo.#Foo & {
script: contents: "echo \"Hello, inlined $TEST!\""


We now have the directory structure shown below:

$ tree  -L 2 .
├── bar
│ └── anything.cue
├── cue.mod
│ ├── module.cue
│ ├── pkg
│ └── usr
├── foo
│ └── main.cue
└── main.cue

5 directories, 3 files

And the expected output is:

$ dagger do hello --log-format plain
4:20PM INF actions.hello._img._dag."0"._op | computing
4:20PM INF actions.hello.script._write | computing
4:20PM INF actions.hello.script._write | completed duration=0s
4:20PM INF actions.hello._img._dag."0"._op | completed duration=1s
4:20PM INF actions.hello._img._dag."1"._exec | computing
4:20PM INF actions.hello._img._dag."1"._exec | #4 0.141 fetch
4:20PM INF actions.hello._img._dag."1"._exec | #4 0.366 fetch
4:20PM INF actions.hello._img._dag."1"._exec | #4 0.611 (1/4) Installing ncurses-terminfo-base (6.3_p20211120-r0)
4:20PM INF actions.hello._img._dag."1"._exec | #4 0.674 (2/4) Installing ncurses-libs (6.3_p20211120-r0)
4:20PM INF actions.hello._img._dag."1"._exec | #4 0.706 (3/4) Installing readline (8.1.1-r0)
4:20PM INF actions.hello._img._dag."1"._exec | #4 0.728 (4/4) Installing bash (5.1.16-r0)
4:20PM INF actions.hello._img._dag."1"._exec | #4 0.805 Executing
4:20PM INF actions.hello._img._dag."1"._exec | #4 0.817 Executing busybox-1.34.1-r3.trigger
4:20PM INF actions.hello._img._dag."1"._exec | #4 0.900 OK: 8 MiB in 18 packages
4:20PM INF actions.hello._img._dag."1"._exec | completed duration=1s
4:20PM INF actions.hello._exec | computing
4:20PM INF actions.hello._exec | completed duration=200ms
4:20PM INF actions.hello._exec | #5 0.155 Hello, inlined world! # <== It was properly executed