docker provider
A docker stage is a wrapper, not a builder — see “Wrapper / relocation”
in Configuration. Instead of building anything itself, it relocates the
rest of the pipeline into a docker compose service.
[my-docker-stage]
provider = "docker"
[my-docker-stage.compose]
file = "docker-compose.yml"
(provider:/description:/disabled:/ci:/depends-on:/skip-on-success:/skip-on-failure:/scripts:/
env:/env-prepend:/env-append: are generic keys every stage has —
see “Generic stage keys” in Configuration. Everything below is specific to docker.)
Requires
docker with the Compose plugin must be available on the machine this
stage runs on — denver never installs it, and every command it issues is a
docker compose ... one (v2, the plugin — not the standalone
docker-compose script). The stage checks both upfront (docker on PATH,
then docker compose version) and dies naming whichever is missing, rather
than failing deep inside the first compose build/compose run. Skipped
under --dry-run, so an env can still be previewed on a machine without
docker. The daemon has to be reachable for the invoking user, too. Point
exe: at a specific docker binary if docker isn’t the right exe.
Setting up Docker
Three things have to be true before a docker stage can run, none of which
denver does for you:
Docker itself is installed, with the Compose plugin (
docker compose ..., not the old standalonedocker-compose). On Linux, follow Docker’s own install guide for your distribution — the Compose plugin is included by default on a current install. On macOS or Windows, Docker Desktop bundles both.The daemon is running, and your user can reach it without
sudo. On Linux that usually means being in thedockergroup (sudo usermod -aG docker $USER, then log out and back in) — running everydockercommand as root instead works too, but nothing in denver assumes it. Docker Desktop handles this for you.Check it actually works, the same way this stage checks it:
docker compose version
If that prints a version instead of an error, this stage is ready to run.
Anything beyond that — a private registry login, an udev rule for a USB
device the container needs — is project-specific, not part of getting
Docker itself working. That kind of one-time setup usually belongs in an
env’s own scripts: setup: (see
scripts: in
Configuration) rather than something a reader has to remember to do by hand
before their first run.
Key reference
exe(default:dockeronPATH) — the docker executable.authentication(defaulttrue) —true,falseor"may-fail", for everyregistries:entry (for one withoutusername:/password:, see stored login and netrc below). An entry can set its ownauthentication:, which wins over this one.true: denver runsdocker loginbefore it checks the registry, and a failed login stops the run.false: denver never runsdocker login. The registry is still checked, which works for a public image; otherwise the check fails and the registry counts as a miss. Use this when you work offline, have no credentials, or the registry is down."may-fail": liketrue, but a failed login is warned about and the registry counts as a miss instead of stopping the run. The normal rules for a miss then apply: the next registry is tried, then a local image is used or the image is built, and denver stops if the image is found nowhere andcompose.build: false.
Like any config key it can be set without editing the config file, e.g.
-c docker.authentication=false.registries(default[]) — ordered list of{url, username, password, authentication}entries to check, each as<url>/<image>, oncecompose.image:has missed locally (or--forceis set — see below), before ever considering a build: each entry in turn, viadocker manifest inspect— never a realdocker pull— first hit wins and no further entry is tried. On a hit,$DENVER_DOCKER_IMAGEis repointed from the barecompose.image:tag to that entry’s full<url>/<image>ref, so the actual pull happens lazily, later, wheneverdocker compose runitself needs the image. Empty (the default) disables this entirely. If nothing is found anywhere andcompose.build: false, denver dies with a clear error instead of silently doing nothing.url:is required per entry;username:/password:are optional but, if either is set, both must be — when present,docker login <url>runs automatically, credentials piped via stdin (never argv, never logged), right before the manifest check against that entry (unlessauthentication:says otherwise, see above). Under--verbose, every check is echoed (+ docker manifest inspect <url>/<image>) followed by aregistry hit:/registry miss:line, and a miss also prints the check’s own stderr (auth error, unknown tag, …) indented below it. Both fields go through denver’s normal${VAR}interpolation, so a literal (myusername) and an env-var-sourced secret (${DOCKER_PASSWORD_DOCKERHUB}) are written the same way.An entry without
username:/password:looks for credentials in this order: the login docker has stored (~/.docker/config.json, or$DOCKER_CONFIG), then themachineentry for the host in$NETRC(default~/.netrc).No stored login, but a netrc entry: denver runs
docker login <host>with it (password via stdin) on every run. A failed login stops the run — or, withauthentication: "may-fail", the registry is a miss.Neither: the registry is taken as public, no login.
Stored login, only under
--force: denver tests it with a non-interactivedocker login <host>, so an expired token shows up early, not later as “pull access denied”. If docker rejects it, denver tries the netrc entry, then on a terminal runsdocker login <host>for you (up to 3 tries). If all fail, the run stops — or, withauthentication: "may-fail", the registry is a miss.Registry not reachable (offline, proxy/DNS error, server down): not a rejection. The manifest check runs and misses, so denver falls back to a local image or a build.
authentication: falseskips all of this. The stored-login test is also skipped under--ci. Only hosts fromregistries:are used. Anetrcstage beforedockerexportsNETRC, so its file is used.compose.file(required) — a single path or a list for multiple-foverlays — never guessed, see “Explicit over implicit” in../concepts/philosophy.md.compose.service(default"dev") — the compose servicerun/buildtarget.compose.build(defaulttrue) — whether a build is ever attempted (see below).compose.default-cmd— the fallback interactive command once relocated into the container, read by denver’s own command resolution (command:at the top level still wins over this if set) — not read by this provider itself, but still a realdocker.compose:key, shown in--show-config-fulleven when unset. When it (or the top-levelcommand:, or the plain$SHELL/bashfallback) names a barebash/zsh/fish, denver wires updenver completefor it automatically before landing there — the container’s own image, unlike the host, is never something a user already has completion set up in themselves. Anything else (extra args aside, which are preserved) is left untouched. See Shell completion for the full mechanism.compose.image— the canonical local tag denver checks for before falling back to a build — checked whenevercompose.image:is set, whether or notregistries:is also configured; a hit skips the build entirely (--forceoverrides this, see below). Exported as$DENVER_DOCKER_IMAGEbefore build/run, so the compose file can sayimage: "${DENVER_DOCKER_IMAGE}"instead of hard-coding the same tag a second time — denver doesn’t cross-check the two, this is just how they stay in sync. If unset,$DENVER_DOCKER_IMAGEis an empty string, the local check never runs, andregistries:is silently ignored (nothing to search a registry for without a tag) rather than an error.compose.run-args(default["--rm"]) — extradocker compose runargs.compose.check-bind-mounts(defaulttrue) — check that every bind mount source of the service exists on the host before build/run, and stop if one is missing. Without this, docker creates the missing path as a root-owned directory (e.g.~/.gitconfigbecomes a directory). The mounts are read fromdocker compose config. Create needed paths in ahooks: pre-<stage>:script, or setfalseto turn the check off. Skipped with--fast(it is slow).
Need something computed at runtime before build/run — a compose .env file,
a login, anything a static compose file can’t express? Use a hooks: pre-<stage>: script (see “Hooks and scripts” in
Configuration) rather than a docker:-specific key — it runs on the
host right before this stage’s setup(), same timing, without denver
needing a per-provider mechanism for it.
Design notes
compose.build: true(the default) does nothing withoutcompose.image:. denver never callsdocker compose buildunlesscompose.image:is set — with no tag to check next time, it would just rebuild on every single run, defeating the point. Setcompose.image:to get denver’s own build-once behavior: it builds the first time, then a local (orregistries:) hit skips the build on every run after that. Withoutcompose.image:,compose build/compose runare left to the compose file’s ownbuild:section to sort out, exactly as if denver’s docker provider weren’t managing images at all. Setcompose.build: falsefor the opposite of thecompose.image:-managed case: nothing gets built, ever, denver just relocates into whatever image is already there — e.g. one pulled as part of ahooks: pre-<stage>:script or a CI step outside denver entirely.Host vs. container.
setup()(build the image) always runs on the host — that’s the only place thedockerCLI operates.wrap()(turn the resolved command intodocker compose run ...) is what actually relocates execution. Each prints its own progress banner (--verboseonly, see CLI Arguments) —setup()’sprepareand then whichever of “build”/”found locally”/”found on a registry”/skipped applies,wrap()’srunlast — so the relocation itself is visible under-v, not silent between the lastsetup()line and the container’s own output.--fasthas no effect here. Unlike uv/conan/zephyr, this provider doesn’t thread--fastthroughsetup()at all —compose.build:is read exactly as configured, so a realdocker compose buildstill runs under--fastif the image wasn’t found locally or on a registry. The local/registries:lookup itself is cheap and read-only, so it always runs regardless.--forceis the escape hatch back to “always rebuild”: it ignores a locally-cachedimage:hit, though aregistries:entry that already has it still wins over a forced local rebuild — a rebuild only actually happens if none of the configured registries have it either.--dry-runprints thedocker compose buildanddocker compose runcommands instead of running them; no container is ever started. The local/registries:existence checks still run (they’re the read-only queries deciding whether a build would be shown at all), but thedocker loginthat would precede a private-registry check does not — so a private entry may report a miss it wouldn’t report for real. The bigger limit is structural: since the container is never entered, the setup stages that run inside it can’t be previewed. denver prints a note saying so; use--skip <docker stage>to preview that same stack on the host instead.Multi-registry check, local-first, never pulling in
setup(). When several places might already have the image — a shared registry, a personal mirror, whatever a CI run pushed to —registries:lets denver search them in a fixed order instead of only knowing about one, usingdocker manifest inspectto check existence without downloading anything. It’s still fully explicit: nothing is scanned or guessed, only the entries named in the list are ever tried, in the order given. The actual image transfer is left entirely todocker compose runitself, once denver has pointed$DENVER_DOCKER_IMAGEat whichever ref (local tag or a specific registry’s) should be used.Automated per-registry login, inline. Each
registries:entry can carry its ownusername:/password:right alongside itsurl:, so a private registry in the search list doesn’t need a separate manual login step (e.g. ascripts: login:entry run viadenver run <env> --scripts login, Configuration’s generic one-shot mechanism) — denver logs in for you, right before it’s actually needed, only for entries that carry credentials.A registry is never required to log in. The registry is only checked when the local image is missing (or with
--force), and a run that does not need it should not stop because its login fails — the registry is down, you are offline, have no credentials, or your token has expired.authentication: falseor"may-fail"covers these cases without removingusername:/password:from the config. Same values and meaning as the conan provider’sauthentication:.