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.
Requires Dagger v1.0.0-beta.15 or later.
dagger install github.com/dagger/dockerThe 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.
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 and
referenced 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.
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.
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 stages| 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 a
RUN 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 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"].
up on a project's services runs the selected services the way
docker 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 in
depends_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 from
portsandexposeare 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 an
image(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 as
nobody) can write to it; tmpfsmounts become temporary directories;- file-based secrets and configs are mounted at their targets
(
/run/secrets/<name>by default).
- 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).
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 reproduce
A 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'| 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 |
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.