Technical Documentation: dagger/docker
โน๏ธ Provenance: Multi-Tier Ingestion ยท CodeWiki Reference
Overview (`README.md`)
docker
A Dagger module, written in Dang, for Dockerfile and
Docker Compose projects. It lints Dockerfiles with
hadolint, builds them, validates
Compose files, builds and runs Compose services, and wraps a Docker engine
and CLI.
Requirements
Requires Dagger v1.0.0-beta.15 or later.
Install
dagger install github.com/dagger/dockerWhat it finds
The module works on four collections. You can select from each by key:
| Collection | Key | Flag |
|---|---|---|
Docker projects (projects) |
directory holding a Dockerfile |
--docker-project=PATH |
Dockerfile stages (projects/stages) |
stage name | --docker-stage=NAME |
Compose projects (compose/projects) |
directory holding a Compose file | --docker-compose-project=PATH |
Compose services (compose/projects/services) |
service name | --docker-compose-service=NAME |
Paths are relative to the workspace root (. for the root). List them:
dagger list docker-projects -a
dagger list docker-stages -a
dagger list docker-compose-projects -a
dagger list docker-compose-services -aListing never starts a container. Projects come from file names, stages from
the Dockerfile's text, and services from the Compose files' text.
Stages
The stages collection holds only the named stages that the project build
never reaches. A stage is in it when:
- no other stage references it, by name or by index: not as a
FROMbase,
not as aCOPY/ADD --fromsource and not as aRUN --mountfrom=
source; - it is not the final stage;
- it is not the
buildTargetsetting.
Those are typically test or lint stages; the project build covers the
rest. Unnamed stages can't be targeted, so they are never keys.
allStages lists every stage with its index, name, base andreferenced flag, and stage(name:) returns any one of them.
Stages are read line by line from FROM instructions, so a FROM inside a
heredoc or after a line continuation looks like a real one. A leading UTF-8
byte order mark is ignored.
Compose files
A Compose project's file is compose.yaml, compose.yml,docker-compose.yml or docker-compose.yaml; when a directory holds several,
the first in that order wins, as in Compose. Compose also merges an override
file over it: the first of compose.override.yml, compose.override.yaml,docker-compose.override.yml and docker-compose.override.yaml. The checks
and runners call docker compose in the project directory without -f, so
Compose picks and merges the files itself (including COMPOSE_FILE from a.env file).
Service names are read from the top-level services mapping of the Compose
file and its override file, without running Compose. The parser understands:
- block mappings at any indentation
- flow mappings (
services: {web: {...}}) - quoted keys, comments and a leading byte order mark
It does not see:
- services pulled in with
include:, or from files named by-forCOMPOSE_FILE; - services merged in with YAML anchors and merge keys (
<<: *base) at theserviceslevel, or aservices:mapping given as an alias; - environment interpolation in service names.
A file the module can't read this way lists no services, and never fails the
listing.
Working directory
What you see depends on where you run dagger:
- At a project root: that project and the projects below it.
- Inside a project's subdirectory: the enclosing project, plus any
projects below where you stand. - Outside any project: the projects below you.
So you don't need a flag to check the project you are working in:
cd app/src && dagger check --docker # checks the app project and its stagesChecks
| Address | Check name | Runs |
|---|---|---|
docker/projects/lint |
lint |
hadolint on the project's Dockerfile |
docker/projects/build |
build |
a build of the project's Dockerfile |
docker/projects/stages/build-stage |
build-stage |
a build of each stage in the stages collection |
docker/compose/projects/lint |
lint |
docker compose config --quiet |
docker/compose/projects/services/build-service |
build-service |
a build of each service that declares build |
- The project
buildbuilds the final stage. When thebuildTargetsetting
names a stage the Dockerfile declares, it builds that stage instead. - A service's build uses its build args, target and Dockerfile from the
resolved Compose configuration. Its context may be anywhere in the
workspace (context: ../shared); a context outside the workspace fails the
check. - A service that runs an image has nothing to build, so its check passes.
Services behind a profile are built too.
dagger check # every check in the workspace
dagger check --docker # every check from this module
dagger check --docker-project=app # one project: lint, build and its stages
dagger check --docker-projects --check lint # hadolint on every project
dagger check --check build-stage # every stage build
dagger check --docker-project=app --docker-stage=test
dagger check --docker-compose-service=api # build one Compose service
dagger check -l --all --docker # list one line per check
dagger check -l --all --docker -f=cli # ...as flags you can paste back--docker-stage=NAME and --docker-compose-service=NAME match that name in
every project. In a repository where many Dockerfiles declare a dev-envs
stage, --docker-stage=dev-envs selects all of them. Add --docker-project
or --docker-compose-project to narrow it down.
The selected items run in parallel. Every failure is reported with the
project, stage or service and the step that failed. A build that fails in aRUN instruction reports its exit code. The instruction's output is in the
trace, not in the message, because the engine's build error doesn't carry
it:
docker build failed in 1 of 2 projects:
- broken: docker build failed (exit code 3 in a RUN step; see the trace for its output)Other build errors are reported without the engine's internal object IDs.
Run checks with dagger check, in CI especially: dagger call on a check
function does not fail the command when the check fails.
The flags for this module (see dagger check --help):
| Flag | Selects |
|---|---|
--docker, --by-docker |
checks from this module |
--docker-project=PATH |
one Docker project (repeatable) |
--docker-projects |
every Docker project |
--docker-stage=NAME |
stages with that name (repeatable) |
--docker-stages |
every stage |
--docker-compose-project=PATH |
one Compose project (repeatable) |
--docker-compose-projects |
every Compose project |
--docker-compose-service=NAME |
services with that name (repeatable) |
--docker-services |
every Compose service |
--check NAME |
checks with that name, in every module |
The flag names can change when another installed module has an item type
with the same name; dagger check --help lists the flags in effect.
hadolint
hadolint runs with --no-color. It reads the nearest .hadolint.yaml or.hadolint.yml from the project's directory up to the workspace root. Extra
arguments come from the lintArgs setting, for example["--failure-threshold", "warning"].
Running Compose services
up on a project's services runs the selected services the waydocker compose up would, as far as Dagger can:
- Dependencies: the selected services and every service they
depends_onare started. - Networking: each service's name is its hostname for the whole Dagger
session, so once a service is running, any other service can reach it by
name, as on Compose's default network. What differs is start order and
waiting. Each service is bound to the services that start before it, so it
starts only after they are up: Dagger waits until their exposed ports
accept connections. Services start independs_onorder; among services
ready to start, image-only services come first, then by name. A service
isn't held back for one that starts after it. In prometheus-grafana, for
example,grafanastarts beforeprometheus(by name), so its queries
fail until prometheus is up and then succeed. Declaredepends_onwhen a
service needs another to be up when it starts. Dagger service bindings
can't form cycles, so adepends_oncycle is broken in the same order. - Ports: every published TCP port of every started service is published
through one proxy service, as raw TCP,
including the same target port on several published ports. A port without
a published port is published on its target port. Two services can't
publish the same port, as with Compose. UDP ports are not published (see
the warnings below). Ports fromportsandexposeare what Dagger waits
for before starting dependents. The selected services' ports come first,
then their dependencies', so a consumer that takes a service's first port
(such as the Playwright module'sPLAYWRIGHT_BASE_URL) reaches the service
you selected, not its database. - Container: each service builds its build context when it declares one,
as Compose does, even when it also names animage(which only names the
result); otherwise it runs its image. Compose's entrypoint, command, environment (includingenv_file),
user, working directory andprivilegedare applied. - Mounts:
- bind mounts of files or directories inside the workspace are mounted as
copies, so writes don't reach your files; - named volumes become cache volumes, one per project and volume. As with a
new Docker volume, an empty one starts as a copy of the image's directory
at the mount path when the image has one. The volume belongs to the user
the service runs as, so non-root images (Prometheus runs asnobody) can
write to it; tmpfsmounts become temporary directories;- file-based secrets and configs are mounted at their targets
(/run/secrets/<name>by default).
- bind mounts of files or directories inside the workspace are mounted as
- Profiles: services behind a profile are not started.
What it can't reproduce is printed as a warning in the trace, per service,
for example warning: app (service web): cap_add is ignored. The same list
is available as warnings on a service:
- UDP ports and
exposeentries that aren't single TCP ports; - bind mounts outside the workspace (such as
/var/run/docker.sock) and
missing bind sources; - secrets and configs from the environment or external;
- other mount types;
cap_add,cap_drop,devices,dns,deploy,extra_hosts,init,ipc,network_mode,pid,platform,runtime,security_opt,shm_size,stop_signal,sysctls,tmpfsandulimits.
Not reproduced, and not warned about:
- healthchecks and
depends_onconditions: Dagger waits for exposed ports
instead; - networks and network aliases;
- restart policies and replicas;
- the exact ownership of a seeded volume: the whole volume belongs to the
service's user, where Docker keeps the image directory's ownership.
dagger up -l # what dagger up would start
dagger up --docker-compose-project=svc # the services of one project
dagger up --docker-compose-project=svc --docker-compose-service=web # web and its dependenciesup is on the services collection (every service of a project) and on each
service (that service and what it depends_on), so a DAG link can name
either. That makes one service wireable into another module's Service
setting:
[modules.playwright.settings]
service = "dag://docker/compose/projects/services/up?docker-compose-project=e2e&docker-compose-service=web"dagger list services -a -f link prints every such link.
A bare dagger up starts every service in the workspace, including this
module's engine functions (Docker-in-Docker), so select the Compose
project.
injectServices returns the composeBase container with services bound to
it by name, and composeEnvs set as environment variables. On the services
collection, it binds the selected services and their dependencies. The
top-level injectServices binds every service of the one Compose project in
your working directory; exactly one project must be visible. The services
are started as for up. composeBase is an empty container by default, so
set it to an image you can run commands in:
dagger settings docker composeBase alpine:3.20
cd svc && dagger call docker inject-services with-exec --args sh,-c,'getent hosts web' stdoutTo bind only some services, use injectServices on a subset of the services
collection from another module (below).
Using it from another module
projects(ws), stages(ws), compose.projects(ws) and services(ws)
return collections. Use keys, get(key:) and subset(keys:) to select
items, and batch to run a function over the selection:
let projects = docker.projects(ws)
projects.keys # ["app", "svc/api"]
run(projects.batch.lint(ws)) # lint every project
run(projects.subset(keys: ["app"]).batch.build(ws)) # build one
run(projects.get(key: "app").stages(ws).batch.buildStage(ws))
projects.get(key: "app").container(ws, target: "test") # the built image
let services = docker.compose.projects(ws).get(key: "svc").services(ws)
services.subset(keys: ["web", "db"]).batch.up(ws) # a Service
services.subset(keys: ["db"]).batch.injectServices(ws) # a Container
services.get(key: "web").up(ws) # web and its dependencies
services.get(key: "web").warnings(ws) # what up can't reproduceA check called through a dependency returns a Check that has not run yet.
Wrap it to run it and raise its failure:
let run(check: Check!): Void {
if (check.pass == false) {
raise check.error.message ?? "check failed"
}
null
}From the CLI, dagger call can't step into a collection yet; a Dagger script
can:
dagger -c 'docker | projects | get app | container | with-exec cat /etc/os-release | stdout'
dagger -c 'docker | projects | get app | all-stages | name'Docker engine and CLI
| Function | Returns |
|---|---|
engine(version, persist, namespace) |
an ephemeral Docker engine (Docker-in-Docker) as a Service |
cli(version, engine) |
a Docker CLI wired to an engine (a new ephemeral one by default) |
CLI.pull, push, load, run, image, images |
the matching docker commands |
Image.export, push, ref |
an image in the engine's cache, exported into Dagger or pushed |
project(path, findUp) |
the Docker project holding a path relative to your working directory |
Settings
Set these in your workspace dagger.toml:
[modules.docker]
source = "github.com/dagger/docker"
settings.lintImage = "docker.io/hadolint/hadolint:v2.14.0-alpine" # the default
settings.lintArgs = ["--failure-threshold", "warning"] # default: []; extra hadolint arguments
settings.buildTarget = "production" # default: ""; stage the project build builds where declared
settings.composeImage = "docker.io/docker/compose-bin:v5.5.0" # the default; provides /docker-compose
settings.composeBase = "alpine:3.20" # default: an empty container; what injectServices binds services to
settings.composeEnvs = ["LOG_LEVEL=debug"] # default: []; KEY=VALUE set on composeBaseOr from the CLI:
dagger settings docker buildTarget production # set
dagger settings -u docker buildTarget # unset, back to the default
dagger settings docker # showbuildTarget is useful when Dockerfiles end in a development stage, such as
the dev-envs stage some repositories add last. A Dockerfile that doesn't
declare the stage still builds its final stage.
In composeEnvs, only the first = separates the name from the value, and
an entry without = sets an empty value.
--- METRICS ---
- Files Extracted: 1
- Estimated Token Budget: ~4388 tokens
- Canonical Reference: https://codewiki.google/github.com/dagger/docker