Skip to main content

Building container images

You can use the Dagger Engine to build container images, either by executing a Dockerfile, or specifying the build steps natively in CUE. Which method to choose depends on the requirements of your project. You can mix and match builds from both methods in the same plan.

tip

Check out the docker package guide for more detailed information on the actions seen here.

Executing a Dockerfile

The Dagger Engine can natively load and execute Dockerfiles. This is recommended in cases where compatibility with existing Dockerfiles is more important than fully leveraging the power of CUE.

Here's a simple example of a Dockerfile build:

package main

import (
"dagger.io/dagger"
"universe.dagger.io/docker"
)

// This action builds a docker image from a python app.
// Build steps are defined in an inline Dockerfile.
#PythonBuild: docker.#Dockerfile & {
dockerfile: contents: """
FROM python:3.9
COPY . /app
RUN pip install -r /app/requirements.txt
CMD python /app/app.py
"""
}

// Example usage in a plan
dagger.#Plan & {
client: filesystem: "./src": read: contents: dagger.#FS

actions: build: #PythonBuild & {
source: client.filesystem."./src".read.contents
}
}

Specifying a build in CUE

You can specify your container build natively in CUE, using the official Docker package: universe.dagger.io/docker. This is recommended when you don't need to worry about Dockerfile compatibility, and want to take advantage of the full power of CUE and the Dagger Engine's APIs.

Native CUE builds have the same backend as Dockerfile builds, so all the same features are available. Since CUE is a more powerful language than the Dockerfile syntax, every Dockerfile can be ported to an equivalent CUE configuration, but the opposite is not true. The following example produces the same image as above.

package main

import (
"dagger.io/dagger"
"universe.dagger.io/docker"
)

// This action builds a docker image from a python app.
// Build steps are defined in native CUE.
#PythonBuild: {
// Source code of the Python application
app: dagger.#FS

// Resulting container image
image: _build.output

// Build steps
_build: docker.#Build & {
steps: [
docker.#Pull & {
source: "python:3.9"
},
docker.#Copy & {
contents: app
dest: "/app"
},
docker.#Run & {
command: {
name: "pip"
args: ["install", "-r", "/app/requirements.txt"]
}
},
docker.#Set & {
config: cmd: ["python", "/app/app.py"]
},
]
}
}

// Example usage in a plan
dagger.#Plan & {
client: filesystem: "./src": read: contents: dagger.#FS

actions: build: #PythonBuild & {
app: client.filesystem."./src".read.contents
}
}

Because this build configuration is pure CUE, it can leverage the full power of the Dagger Engine's composition model.

Automation

Building images in CUE gives you greater flexibility. For example, you can automate building multiple versions of an image, and deploy, all using the Dagger Engine:

package main

import (
"dagger.io/dagger"
"universe.dagger.io/docker"
)

dagger.#Plan & {
actions: versions: {
"8.0": _
"5.7": _

// This is a template
// See https://cuelang.org/docs/tutorials/tour/types/templates/
[tag=string]: {
build: docker.#Build & {
steps: [
docker.#Pull & {
source: "mysql:\(tag)"
},
docker.#Set & {
config: cmd: [
"--character-set-server=utf8mb4",
"--collation-server=utf8mb4_unicode_ci",
]
},
]
}
push: docker.#Push & {
image: build.output
dest: "registry.example.com/mysql:\(tag)"
}
}
}
}

Now you can deploy all versions:

dagger-cue do versions

Or just build a specific version, without pushing:

dagger-cue do versions 8.0 build

Multi-stage build

Another common pattern is multi-stage builds. This allows you to have heavier build images during the build process, and copy the built artifacts into a cleaner and lighter image to run in production.

package main

import (
"dagger.io/dagger"
"universe.dagger.io/alpine"
"universe.dagger.io/docker"
"universe.dagger.io/go"
)

dagger.#Plan & {
client: filesystem: "./src": read: contents: dagger.#FS

actions: {
// Build app in a "golang" container image.
build: go.#Build & {
source: client.filesystem."./src".read.contents
}

// Build lighter image,
// without app's build dependencies.
run: docker.#Build & {
steps: [
alpine.#Build & {
packages: "ca-certificates": _
},
// This is the important part, it works like
// `COPY --from=build /output /opt` in a Dockerfile.
docker.#Copy & {
contents: build.output
dest: "/opt"
},
docker.#Set & {
config: cmd: ["/opt/testmulti"]
},
]
}

push: docker.#Push & {
image: run.output
dest: "registry.example.com/app"
}
}
}