Inspect Docker Stack Merges Before You Deploy
Piping a merged Compose file straight into a deploy is how a forgotten override port goes live, which is why docker stack config lets you look first. It turns one or more Compose files into the final configuration Docker would use for a stack deployment, without creating a service or touching a running stack. Allow about ten minutes for a small stack, plus time to chase anything unexpected in the rendered YAML.
The route
Jump straight to the step you need, or tick off Done means at the end.
- 1. Confirm the command and create a safe test area
- 2. Render one Compose file
- 3. Merge an override file in order
- 4. Check missing and deliberately supplied variables
- 5. Preserve variables for a later deployment
- 6. Read from standard input when the files are generated
- 7. Stop before deployment if the output is wrong
The examples use Docker Community CLI 29.8.1 from the installed docker-ce-cli package. The local manual describes this command as outputting the final file after merges and interpolations. Option details can vary between CLI releases, so check the installed version before copying a script onto a different host.
1. Confirm the command and create a safe test area
Run these checks as your ordinary user. The command needs neither sudo nor a Docker daemon, since it only renders configuration:
$ docker version --format '{{.Client.Version}}'
29.8.1
$ docker stack config --help
Usage: docker stack config [OPTIONS]
Outputs the final config file, after doing merges and interpolations
Use a temporary directory, or an existing project directory where each Compose file is clearly identified:
$ workdir="$(mktemp -d)"
$ cd "$workdir"
$ printf 'temporary files: %s\n' "$workdir"
temporary files: /tmp/tmp.example
The directory name differs on every run. Keep it until you have checked the output, then remove it with rm -rf -- "$workdir" if it holds only these disposable test files. That removal is irreversible, so do not point it at a shared project path.
2. Render one Compose file
Create a minimal input that uses an environment variable. The quoted here-document keeps the ${IMAGE_TAG} text in the file instead of expanding it while the file is written:
$ cat > base.yml <<'YAML'
services:
web:
image: "nginx:${IMAGE_TAG:-latest}"
ports:
- "8080:80"
YAML
$ IMAGE_TAG=1.27 docker stack config --compose-file base.yml
services:
web:
image: nginx:1.27
ports:
- mode: ingress
target: 80
published: 8080
protocol: tcp
app_protocol: http
Formatting can differ slightly between releases, but the key check is that the image renders as nginx:1.27 and the port mapping appears in the final configuration. The command prints YAML to standard output and leaves base.yml untouched.
3. Merge an override file in order
Pass each file with --compose-file in the order you want Docker to merge them; later files supply the environment-specific changes:
$ cat > production.yml <<'YAML'
services:
web:
deploy:
replicas: 3
environment:
APP_MODE: production
YAML
$ IMAGE_TAG=1.27 docker stack config \
--compose-file base.yml \
--compose-file production.yml
services:
web:
image: nginx:1.27
ports:
- mode: ingress
target: 80
published: 8080
protocol: tcp
app_protocol: http
environment:
APP_MODE: production
deploy:
replicas: 3
Tip
Inspect the merged result instead of assuming a file was ignored. Swapping the file order, or passing only the override, is a common way to lose the base image or ports and wonder why. Keep the exact command in a script or review note if the merge is part of a release process.
4. Check missing and deliberately supplied variables
Interpolation happens by default. With the :-latest expression above, an unset IMAGE_TAG falls back to latest:
$ env -u IMAGE_TAG docker stack config --compose-file base.yml | grep 'image:'
image: nginx:latest
That fallback is convenient for a test and unsafe for a release. Set a specific tag in the command environment, or better, in the deployment's controlled environment; do not use a mutable tag when you need a reproducible rollout.
For a strict check, use a required variable expression in a separate input:
$ cat > required.yml <<'YAML'
services:
web:
image: "nginx:${REQUIRED_TAG?set REQUIRED_TAG before rendering}"
YAML
$ env -u REQUIRED_TAG docker stack config --compose-file required.yml
invalid interpolation format for services.web.image: "required variable REQUIRED_TAG is missing a value: set REQUIRED_TAG before rendering"
$ REQUIRED_TAG=1.27 docker stack config --compose-file required.yml | grep 'image:'
image: nginx:1.27
The exact error prefix can vary. The useful result is a non-zero command when the required value is absent. Treat that failure as a checkpoint, not a reason to add a silent default.
5. Preserve variables for a later deployment
--skip-interpolation suppresses interpolation while still producing the merged configuration, which matters when the output will be piped to another Docker command that must interpolate itself:
$ cat > runtime.yml <<'YAML'
services:
web:
image: "nginx:$${IMAGE_TAG}"
environment:
APP_MODE: "${APP_MODE:-production}"
YAML
$ env -u IMAGE_TAG docker stack config --compose-file runtime.yml --skip-interpolation
services:
web:
image: nginx:$${IMAGE_TAG}
environment:
APP_MODE: "${APP_MODE:-production}"
Warning
Use this mode deliberately. A value containing a dollar expression can be expanded twice if you pipe it onward without reviewing the whole pipeline, which can change a route, secret reference or command argument. If you only want to see the values reaching a deployment now, leave the option out and set the variables explicitly.
6. Read from standard input when the files are generated
The manual also accepts - as the Compose file path, useful for a controlled pipeline, but keep the source visible while debugging:
$ IMAGE_TAG=1.27 cat base.yml | docker stack config --compose-file -
services:
web:
image: nginx:latest
In this example the variable assignment belongs to cat, not to docker stack config, so Docker never receives IMAGE_TAG. Put the assignment on the Docker command instead:
$ cat base.yml | IMAGE_TAG=1.27 docker stack config --compose-file - | grep 'image:'
image: nginx:1.27
That shell-scope detail is easy to miss in a long pipeline. If the rendered value matters, print or test it before sending the result anywhere else.
7. Stop before deployment if the output is wrong
Review the rendered file for these before it goes anywhere near a deploy:
- Image names. Correct repository, tag or digest.
- Published ports. Nothing exposed that should stay internal.
- Environment values and bind paths. Especially anything supplied by the shell; that is configuration input, not harmless text.
- Secrets, networks and replica counts. Matching what you actually intend to ship.
Do not pipe output to docker stack deploy until the merge and interpolation mode match your deployment plan. This command is observational, so recovery is normally just correcting an input file or environment variable and running it again. If a later command has already changed a running stack, use that deployment's documented rollback or redeploy process; docker stack config itself has not created anything to roll back.
Done means
- Version confirmed. You checked the installed Docker CLI version and command syntax.
- Files merged in order. You rendered the intended base and override files in the sequence you meant.
- Interpolation checked. You verified it with an explicit image tag rather than trusting a fallback.
- Skip-interpolation understood. You know when it is needed for a later deployment step.
- Pipelines checked. You caught the shell variable-scope trap on standard input.
- Output reviewed. You read the final YAML before running any command that changes a Swarm stack.