Intro
Production containers are easy to start but hard to operate consistently when configuration leaks into images, logs, or the wrong environment. This guide shows how to keep sensitive values out of Docker images, how to choose between plain environment files and Docker secrets, and how to verify that the configuration actually works before it reaches CI/CD or a production‑like host.
You will see the exact commands to create a secret, reference it in a Compose file, mount it at runtime, and confirm the container sees the expected value. The goal is a repeatable, testable workflow that makes failure visible early.
Workflow Overview
- Identify the resource – decide which credential or config value needs protection (e.g., a database password, an API token, a TLS private key).
- Choose the mechanism – use a
.envfile for non‑secret settings (feature flags, log level) and a Docker secret for anything that must stay out of the image and out ofdocker inspectoutput. - Apply the change – create the secret with
docker secret create db_password ./password.txt(Swarm mode) or declare it in a Compose file undersecrets:for local development. - Verify the observed state – run a container that reads the secret and print it to prove the mount works:
docker run --rm --secret source=db_password,target=db_password alpine cat /run/secrets/db_password
If the command prints the password, the secret is correctly mounted at /run/secrets/db_password.
Hidden assumptions to surface
- Local bind‑mount paths (
./secrets/password.txt) may not exist on another developer’s machine or in CI. - Image tags (
myapp:latest) can drift; pin a digest for reproducibility. - Network names (
frontend_net) must be created beforedocker compose upor be defined in the Compose file. - Resource limits (
cpus,memory) and file‑descriptor limits affect whether the app can read the secret file. - Permissions on the secret file inside the container default to
0400; the running user must have read access.
Quick health checks
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"– list running containers.docker logs <container> --tail 100– see recent errors.docker inspect <container>– confirm mounts, environment variables, and health status.- For Compose:
docker compose ps,docker compose logs -f <service>,docker compose exec <service> shto debug without rebuilding.
Local Pilot Plan
Run a minimal end‑to‑end test on your laptop before committing the pattern to a pipeline.
- Create the secret material
echo "s3cr3t-p4ss" > ./secrets/db_password.txt
- Write a Compose file (
docker-compose.yml):
version: "3.9"
services:
app:
image: alpine:3.20
command: sh -c "cat /run/secrets/db_password && sleep 3600"
secrets:
- db_password
env_file:
- .env
secrets:
db_password:
file: ./secrets/db_password.txt
- Add a non‑secret
.envfile (optional, for comparison):
LOG_LEVEL=info
FEATURE_X=enabled
- Start the stack
docker compose up -d
- Confirm the secret is visible only where intended
docker compose exec app cat /run/secrets/db_password
The output should be s3cr3t-p4ss. Verify the .env values appear in the process environment:
docker compose exec app printenv | grep -E "LOG_LEVEL|FEATURE_X"
- Restart test – stop and recreate the container to ensure the secret persists across restarts:
docker compose down
docker compose up -d
docker compose exec app cat /run/secrets/db_password
The secret should still be present. If the file disappears, the service was writing to the container’s writable layer instead of the mounted secret.
- BuildKit secret example (build‑time only) – add a
Dockerfileline to consume a build‑time token without baking it into the image:
# syntax = docker/dockerfile:1.4
FROM alpine:3.20
RUN --mount=type=secret,id=npm_token \
npm config set //registry.npmjs.org/:_authToken="$(cat /run/secrets/npm_token)" && \
npm ci
Build with:
DOCKER_BUILDKIT=1 docker build --secret id=npm_token,src=./secrets/npm_token.txt -t myapp:dev .
The token never appears in docker history or the final image.
Conclusion
Treat configuration as code that must be tested, not copied. The safest path is to keep each example small, run the commands locally, and confirm the expected behavior before adding more services or automation.
Next step: pick one service, document the exact commands used to build, run, inspect, stop, and recreate it. Then compare the result with related practices such as Docker security hardening, Compose file versioning, and Dockerfile best‑practice patterns so the implementation fits the larger operating model.
A reliable container workflow makes failure visible: logs are easy to find, persistent data survives container rebuilds, and local behavior stays close enough to production to catch mistakes early.