Docker Compose Production Readiness: Health, Dependencies, and Safe Updates
Operate Docker Compose stacks deliberately: model startup readiness, migrations, networking, configuration, persistent data, and controlled single-host deployments.
Docker Compose is useful beyond local development. It can describe and operate a multi-container application on a single Docker host, and Docker documents a production workflow based on a production-specific Compose file. That does not make a Compose project a general cluster scheduler: it does not, by itself, provide multi-host placement, a highly available control plane, or an application-specific recovery strategy. Treat the boundary honestly. Compose can make one-host lifecycle and configuration repeatable; the application, host, storage, network edge, backups, and deployment process still determine whether the service meets its availability objectives.
The most common operational mistake is to treat docker compose up -d as proof that an application is ready. Compose can order service creation, but a process being started is not the same as a database accepting connections, a migration succeeding, or an HTTP endpoint serving useful requests. A reliable project makes those states explicit, validates the resolved configuration, preserves data intentionally, and uses an update command that actually applies configuration changes.
Model readiness, not just startup order
Short-form depends_on establishes a dependency ordering. It does not normally wait for the dependency’s application protocol to become ready. Long-form dependencies let a service wait for a health check (service_healthy) or for a one-shot dependency to exit successfully (service_completed_successfully). The health check must measure a condition meaningful to the dependent workload. A process check can pass while the database is still initializing; conversely, a probe that is stricter than the application’s actual requirement can block a healthy deployment.
Here is a deliberately small example. Replace APP_IMAGE, the migration command, and the server command with the names and interfaces used by the application. The sample pins a major PostgreSQL version for clarity, not a patch release or immutable digest; select and pin the exact image artifact tested by the release process.
name: orders
services:
db:
image: postgres:16
environment:
POSTGRES_DB: ${POSTGRES_DB:?set POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER:?set POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?provide a database password}
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U \"$${POSTGRES_USER}\" -d \"$${POSTGRES_DB}\""]
interval: 10s
timeout: 5s
retries: 6
start_period: 30s
restart: unless-stopped
migrate:
image: ${APP_IMAGE:?set APP_IMAGE to a tested application image}
command: ["./bin/migrate", "up"]
depends_on:
db:
condition: service_healthy
restart: "no"
app:
image: ${APP_IMAGE:?set APP_IMAGE to a tested application image}
command: ["./bin/server"]
depends_on:
db:
condition: service_healthy
migrate:
condition: service_completed_successfully
ports:
- "127.0.0.1:8080:8080"
restart: unless-stopped
volumes:
db-data:
The doubled dollar signs in the health-check command defer variable expansion to the container instead of letting Compose interpolate the host’s environment into the command. pg_isready reports PostgreSQL connection status; it does not prove that every application query, permission, schema, or downstream dependency is correct. The application should still handle transient connection failures and retry according to its own bounded policy. A Compose dependency condition is a startup gate, not a substitute for runtime resilience.
The migration service is a one-shot job. Compose can wait for it to complete successfully before creating app; a non-zero exit prevents that dependency condition from being met. This makes a failed migration visible instead of racing the web process against schema changes. It does not make a migration automatically safe: use backward-compatible expand/contract changes where deployments overlap, ensure the migration tool has appropriate locking, and decide how operators inspect and retry a failed job. Avoid launching concurrent deployment runs against the same database unless the release system serializes them or the migration system safely coordinates them.
Understand dependency restart semantics
Compose’s dependency option restart: true is distinct from a service’s container restart policy. On a dependency edge, it can restart a dependent service when the dependency is explicitly updated or restarted through a Compose operation. It is not a promise that every spontaneous crash of the dependency will restart all dependents, and it does not establish a permanent supervisor relationship between services. Container policies such as restart: unless-stopped govern container restart behavior; they do not prove that a service is healthy or that another service can recover its connections.
Applications should reconnect after a database or broker interruption and should fail requests in a controlled way when a dependency remains unavailable. Health checks should report a useful container state, but they do not automatically repair application-level faults. Test the actual failure path: interrupt a dependency in a non-production environment, observe health and logs, and verify that the application recovers without manual state changes or data loss.
Keep network names stable and exposure intentional
Services on the same Compose network can discover one another by service name. In the example, the application connects to host db on the database’s container port; localhost inside app means the application container itself. Do not hard-code a container IP address: recreated containers can receive different addresses while retaining the service name.
Only publish ports that must be reachable from the host or an external client. The database needs no host-published port for app to reach it over the project network. The example binds the HTTP port to loopback because it assumes a reverse proxy on the same host; if clients must connect directly, deliberately select the host interface and firewall policy instead. A published port is an exposure decision, not merely a service-discovery setting.
For a larger one-host stack, separate networks can express which services need to communicate. For example, attach a public-facing proxy and application to a front network, and the application and database to a back network. Network membership limits ordinary service-to-service connectivity, but it is not a replacement for host security controls, application authentication, or careful configuration of any published port.
Make configuration resolution reproducible
Compose combines the files supplied with -f in order; later files can override or extend earlier configuration. This supports a shared base file plus a narrowly scoped production override, rather than copying the whole development configuration into a second divergent file. Paths in a multi-file project are resolved relative to the first Compose file unless the project directory is explicitly changed, so runbook commands should use a consistent working directory or an explicit --project-directory.
docker compose \
-p orders-prod \
-f compose.yaml \
-f compose.production.yaml \
config --quiet
docker compose config --quiet validates the model without printing it. A plain docker compose config renders the resolved model and can help review interpolation and overrides, but it may reveal values that were inserted into configuration. docker compose config --environment prints the variables used for interpolation. Do not paste such output into public logs or tickets without checking for secrets. Run validation in the same deployment environment and with the same file list, project directory, and relevant variable sources as the eventual deployment; a successful parse does not validate that images exist, ports are available, external services are reachable, or credentials work.
The shell environment, an explicitly selected --env-file, and the default project .env file can all affect interpolation. Make the intended source explicit in automation and fail on required variables rather than silently substituting an empty value. An .env file is convenient configuration input, not a secure secret manager. Keep credentials out of version control and use the deployment platform’s secret delivery mechanism. Compose secrets can grant selected services access to secret data, but their source and runtime behavior must be checked against the target Docker environment; do not assume every Compose feature has identical semantics in Swarm or another orchestrator.
Separate projects on shared hosts
Compose derives resource names from a project name, which can be set with -p, COMPOSE_PROJECT_NAME, a top-level name, or directory naming rules. On a host running staging and production copies, choose stable, distinct project names in automation. Otherwise, two invocations can target the same project resources or operators can inspect the wrong stack. Record the project name and exact file list in the deployment runbook, and use the same values for ps, logs, exec, and lifecycle commands.
Named volumes outlive an ordinary docker compose down, but docker compose down --volumes removes named volumes declared by the project and anonymous volumes attached to its containers. Data that must survive recreation belongs in an intentionally managed volume or external storage, with tested backups and restore procedures. Do not use down --volumes as routine cleanup on a stateful production project. Volume persistence is not a backup: it does not protect against host loss, operator deletion, filesystem corruption, or an application writing logically invalid data.
Apply updates with the right lifecycle command
docker compose restart restarts existing containers; it does not apply edits to the Compose file, such as changed environment values. For configuration changes, use an operation that reconciles the project, normally docker compose up -d with the intended files and variables. If application code or a Dockerfile changed, build or pull the intended image first and recreate the affected service. Docker’s documented production example uses docker compose build web followed by docker compose up --no-deps -d web; --no-deps is appropriate only when dependencies do not need an update and the application remains compatible with their current state.
Prefer immutable image references or a release process that records exactly which digest was deployed. A mutable tag can resolve to different image content at different times, so a textually unchanged Compose file does not always guarantee the same artifact. Review the resolved configuration and image references, check the release diff, deploy one controlled change, and verify the expected container image, health state, application endpoint, and logs. Keep a rollback plan that accounts for database schema compatibility; switching an application image back is unsafe if the new release performed an irreversible migration.
Operate the result, not just the YAML
Before deployment, review at least these operational questions:
- Are all images built or pulled from the intended registry, and are production references pinned to the release artifact?
- Do health checks test meaningful readiness with bounded intervals, timeouts, and retries?
- Does the application tolerate dependency restarts and temporary unavailability after startup?
- Is migration execution visible, serialized where needed, and compatible with the rollout and rollback plan?
- Are ports published only where needed, with an explicit host binding and firewall path?
- Are persistent volumes identified, backed up, and restorable, and is destructive teardown excluded from routine automation?
- Are variable sources, project names, file order, and the deployment working directory deterministic?
- Can an operator find the stack with
docker compose ps, inspect recent logs, and distinguish a failed health check from an exited container?
Finally, test the runbook on a non-production host using the same Compose files and deployment mechanism. Include first startup, a normal update, an application rollback, a failed migration, a dependency restart, and restoration from backup. A Compose file is an executable operational interface: its real quality is demonstrated by repeatable, observable changes and recoverable failure, not by whether the YAML parses once on a developer laptop.
Related:
- Docker vs. Podman: Rootless Containers and the Daemon-less Architecture
- Container Runtime Internals: containerd, CRI-O, and the OCI Spec
Sources: