Intermediate 6 min · March 06, 2026

Docker Compose depends_on — Fixing Container Startup Crashes

depends_on controls start order, not readiness.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
Quick Answer
  • You declare desired state (services, volumes, networks) in docker-compose.yml
  • Compose translates that into container create/start/stop commands via the Docker API
  • Service names become DNS entries on an auto-created bridge network
  • services: each entry maps to one container
  • volumes: named storage that survives container lifecycle
  • networks: control which services can reach each other
  • depends_on with healthcheck: startup ordering with readiness gates

Every real-world application is more than one moving part. Your API needs a database. That database needs a volume so data survives restarts. Your frontend needs to know the API's address. Managing each with individual docker run commands is a copy-paste nightmare that breaks when a new developer joins or you move to a different machine.

Docker Compose is a declarative orchestrator for multi-container applications on a single host. You describe the desired state in YAML — services, networks, volumes, environment variables, health checks — and Compose handles creation, networking, and lifecycle ordering. The file is the documentation. The file is the deployment. There is no gap between what you describe and what runs.

The critical misconception: Compose is not just a convenience wrapper. It manages DNS resolution between services, network isolation between tiers, volume lifecycle across container restarts, and startup ordering with readiness gates. Understanding these mechanisms is what separates a working compose file from a production-grade one.

The Anatomy of a docker-compose.yml File (and Why Every Key Matters)

A Compose file is a declaration of intent, not a script. You're telling Docker 'here is the world I want' — Compose figures out how to build it. That mental shift matters: you're not writing steps, you're describing a state.

Every Compose file has a few top-level keys. services is the main one — each entry is a container. volumes defines named storage that outlives containers. networks lets you control which services can see each other (by default Compose creates one shared network, which is great for getting started but dangerous if you want to isolate, say, your admin panel from your public API).

The depends_on key is widely misunderstood. It controls start order, not readiness. Your app container will start after the database container starts, but not after Postgres is actually ready to accept connections. That's a real gotcha we'll cover shortly. For readiness you need health checks.

The build key is your escape hatch from public images — point it at a directory with a Dockerfile and Compose builds the image itself before starting the container. This is how you develop locally with live code while still using Compose for orchestration.

Service configuration depth: Each service can specify image or build (mutually exclusive in intent — image pulls from a registry, build creates locally). The restart policy controls behavior on container exit: no (default), always, on-failure, unless-stopped. In production, unless-stopped is almost always correct — it restarts on crash but respects your manual docker compose stop.

Volume lifecycle: Named volumes declared under the top-level volumes: key are managed by Docker. They survive docker compose down but are destroyed by docker compose down -v. Bind mounts (host:path syntax) are managed by the host filesystem. Anonymous volumes (- /app/node_modules) are created by Docker and removed when the container is removed.

Environment Variables and Secrets — The Right Way vs. The Dangerous Way

Hardcoding passwords directly in docker-compose.yml is one of the most common and dangerous mistakes in real projects. If that file ever gets committed to a public repo — even accidentally — your credentials are exposed permanently (git history remembers everything).

The correct pattern is a .env file alongside your Compose file. Docker Compose automatically reads .env and substitutes ${VARIABLE_NAME} placeholders. You commit a .env.example with dummy values to your repo, and each developer (and your CI system) fills in the real .env locally. The actual .env lives in .gitignore.

For production, environment variables should come from your hosting platform's secret manager (AWS Secrets Manager, Doppler, HashiCorp Vault) injected at runtime — never baked into an image or a file on disk.

You can also use multiple Compose files with docker compose -f docker-compose.yml -f docker-compose.prod.yml up. The second file merges and overrides the first. This is how you maintain one base config and swap out dev-specific settings (like bind mounts and debug ports) for production-hardened ones without duplicating the whole file.

Secret exposure vectors: - Hardcoded in docker-compose.yml → committed to git → visible in git history forever - ENV in Dockerfile → baked into every image layer → visible in docker history and docker inspect - .env file not in .gitignore → committed to git → same as hardcoded - docker compose config output includes resolved secrets → if captured in CI logs, credentials are exposed

The right pattern for each environment: - Local development: .env file, in .gitignore, .env.example committed as template - CI/CD: secrets injected from CI platform secrets (GitHub Secrets, GitLab CI variables) - Production: secrets from a secrets manager (AWS Secrets Manager, Vault), mounted as files or injected as environment variables at deploy time

Networking Between Containers — How Compose DNS Actually Works

This is where most intermediate developers have a fuzzy mental model, and it costs them hours of debugging. When Compose starts your services, it creates a virtual network and registers each service under its own DNS name — that name is simply the service name you defined in the YAML.

So when your API container wants to connect to Postgres, it doesn't use localhost (that points to itself) and it doesn't use an IP address (those change). It uses postgres_db:5432 — the service name and the container port, not the host-mapped port. This is a huge source of confusion: ports: '5432:5432' exposes port 5432 to your laptop. Other containers don't need that — they communicate on the internal network directly.

Multiple networks let you enforce security boundaries at the network layer, not just at the application layer. In our example, the frontend container literally cannot reach the database — there's no route. Even if someone finds an XSS vulnerability in your frontend, they can't pivot directly to the database because Compose's networking won't allow it.

Use docker compose exec api_server sh to shell into a running container and test DNS resolution live with nslookup postgres_db or curl http://api_server:3001/health from the frontend container.

DNS resolution internals: Compose uses Docker's embedded DNS server at 127.0.0.11. When a container resolves a service name, the request goes to this DNS server, which maps the service name to the container's internal IP on the appropriate network. If a container is on multiple networks, it can resolve services on all of them. If a container is on network A but not network B, it cannot resolve services that are only on network B.

Port mapping confusion: The ports directive maps HOST:CONTAINER. Your database connects at localhost:5432 from your laptop (host port). Your API connects at postgres_db:5432 from inside Docker (container port). These are completely separate network paths. Using the host port from inside a container (localhost:5432) either fails or connects to the wrong thing.

Profiles and Overrides — Running Different Stacks for Dev, Test and CI

A subtle but powerful Compose feature is profiles. You might have services that should only run in certain contexts — a database admin UI like pgAdmin in development, a mock email server in testing, or a metrics exporter in production. Profiles let you tag services and opt into them at runtime.

With docker compose --profile dev up, only services tagged with the dev profile (plus services with no profile) start. Your CI pipeline can run docker compose --profile test up --abort-on-container-exit to spin up integration test dependencies, run tests, and tear down — without launching the full dev UI tooling.

Compose file overrides are the production deployment pattern. You maintain a docker-compose.yml as the base truth and a docker-compose.prod.yml that overrides just the production-specific bits: replaces bind mounts with proper volumes, removes debug ports, adds resource limits, and switches to pre-built images instead of local builds. This avoids the 'it works on my machine' trap without duplicating hundreds of lines of YAML.

Override merge behavior: When multiple files are specified with -f, Compose deep-merges them. Arrays (like ports, volumes, environment) are replaced, not appended — if the override file specifies volumes: [], the base file's volumes are completely replaced. Scalar values (like image, restart) are overwritten. This is why docker compose config is essential — it shows the final merged result before you deploy.

Profile use cases beyond dev/prod: - Testing: profile test for mock services (WireMock, LocalStack) - Monitoring: profile monitoring for Prometheus, Grafana, cAdvisor - Debugging: profile debug for a debugger sidecar or log aggregator - Migration: profile migration for a one-shot database migration container

docker run vs Docker Compose
Aspectdocker run (manual)Docker Compose
Setup complexityOne command per container, manual flags every timeSingle YAML file, one command to start everything
NetworkingMust create networks manually and attach each containerAuto-creates a shared network; service names resolve via DNS
Startup orderYou remember the order yourselfDeclarative depends_on with optional health check conditions
Environment configLong --env-file or -e flags per commandBuilt-in .env file support and variable interpolation
Volume managementExplicit -v flags, easy to forget or mistypeNamed volumes and bind mounts declared in YAML
ReproducibilityLow — you have to document the exact commandsHigh — the file IS the documentation
Scaling a serviceRun more containers manually, manage names yourselfdocker compose up --scale api_server=3 (basic, no LB)
Best suited forQuick one-off containers, learning Docker basicsLocal dev, integration testing, simple multi-service deployments
Not suited forN/A — even single containers benefit from ComposeLarge-scale production orchestration (use Kubernetes for that)

Key Takeaways

  • Docker Compose is a declaration of the desired state of your entire app stack — you describe it once in YAML and docker compose up handles creation, networking, and ordering every time.
  • depends_on controls start order, not start readiness — always pair it with a healthcheck and condition: service_healthy to prevent race conditions when your app starts before the database is accepting connections.
  • Containers communicate by service name on the internal Docker network — never use localhost between containers, and never use the host-mapped port for container-to-container traffic.
  • Use multiple Compose files (-f flag) to maintain one base config and override only what changes between environments — cleaner and less error-prone than separate files for dev and prod.
  • Never hardcode secrets in Compose files or Dockerfiles. Use .env files for local dev (in .gitignore) and secrets managers for production. Pre-commit hooks catch accidental secret commits.

Interview Questions on This Topic

  • QWhat's the difference between depends_on and a health check condition in Docker Compose, and when would a plain depends_on cause a race condition in production?SeniorReveal
    depends_on only controls container start order — it does not wait for the service to be ready. A plain depends_on: postgres_db starts your API as soon as the Postgres container starts, not when Postgres is actually accepting connections (which takes 2-8 seconds). The race condition: API connects, Postgres isn't ready yet, API crashes. Fix: add a healthcheck to Postgres using pg_isready, then use condition: service_healthy in depends_on.
  • QIf you have two services in a Compose file and they can't communicate with each other by service name, what would you check first — and can you walk me through how Compose DNS actually resolves service names?SeniorReveal
    First check: are both services on the same network? By default Compose creates one shared bridge network for all services. If you define custom networks per tier, a service only resolves names of services on a shared network. Compose uses Docker's embedded DNS at 127.0.0.11 — each container's /etc/resolv.conf points there, and it maps service names to internal IPs. Test with: docker compose exec <service> nslookup <target>.
  • QYou have a docker-compose.yml that works perfectly for local development with bind mounts and debug ports. How would you adapt it for production deployment without duplicating the entire file?SeniorReveal
    Use override files: keep docker-compose.yml as the base and create docker-compose.prod.yml that only specifies what changes — replace bind mounts with named volumes, remove debug ports, switch from build: to image: with a registry tag, add resource limits and logging config. Deploy with: docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d. Always verify with docker compose config before deploying to see the fully merged result.
  • QExplain the difference between named volumes, bind mounts, and anonymous volumes. When would you use each? What happens to data in each when you run docker compose down vs docker compose down -v?JuniorReveal
    Named volumes (declared under top-level volumes:) are managed by Docker and survive docker compose down — only destroyed by down -v. Use for database data. Bind mounts (host path: container path) map a host directory into the container — great for hot-reloading code in development. They're not touched by either down command. Anonymous volumes (-v /app/node_modules) are created by Docker, not named, and are removed when the container is removed. Use for isolating container-internal directories (like node_modules) from bind-mounted host directories.
  • QYour CI pipeline runs docker compose config and sees the DATABASE_URL with the password in plain text. How do you prevent secrets from leaking into CI logs and build artifacts?SeniorReveal
    Three layers: (1) In CI, inject secrets from the platform secret store (GitHub Secrets, GitLab CI Variables) as environment variables — never hardcode in .env files committed to the repo. (2) Mask the secret variable in CI config so it never appears in logs. (3) In docker-compose.yml, reference as ${DATABASE_PASSWORD} — the resolved value won't appear in the compose file itself. Add docker compose config output to CI log masking. For production, use a secrets manager (AWS Secrets Manager, Vault) instead of environment variables entirely.
  • QYou have a Compose file with 8 services. In production, 2 should not run. In development, 3 additional services (pgAdmin, mock email, hot-reload proxy) should run. How do you structure this without maintaining multiple nearly-identical files?SeniorReveal
    Use profiles + override files together. Tag dev-only services with profiles: [dev] in the base file — they only start when you pass --profile dev. Tag prod-excluded services with profiles: [dev] as well. For config differences (bind mounts vs volumes, debug flags), create docker-compose.prod.yml override. Result: docker compose --profile dev up for local, docker compose -f docker-compose.yml -f docker-compose.prod.yml up for production. Run docker compose config to validate either combination before deploying.

Frequently Asked Questions

What is the difference between Docker Compose and Kubernetes?

Docker Compose is designed for defining and running multi-container apps on a single host — it's perfect for local development and simple deployments. Kubernetes is a full container orchestration platform that manages containers across a cluster of machines, handles auto-scaling, self-healing, rolling deployments, and much more. Start with Compose; graduate to Kubernetes when you need to scale across multiple servers or need enterprise-grade reliability.

Does `docker compose down` delete my database data?

It depends on how you defined your volume. docker compose down stops and removes containers and networks, but named volumes (declared under the top-level volumes: key) survive by default. Your data is safe. Only docker compose down -v removes named volumes. Anonymous volumes created by -v /some/path syntax are also removed by down. Use named volumes for any data you care about.

Can I use Docker Compose in production?

Yes, for small to medium deployments on a single server, Compose is perfectly valid in production — many successful apps run this way. The limitation is that it manages containers on one machine only. If you need to spread load across multiple servers, roll out zero-downtime deployments, or auto-scale based on traffic, you'll need Kubernetes or Docker Swarm. Use docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d with a production override file to harden your config.

How do I share environment variables between services without repeating them?

Docker Compose reads from a .env file in the same directory as docker-compose.yml and substitutes ${VARIABLE_NAME} placeholders. Define each variable once in .env and reference it in multiple services. For variables that should not be in the .env file (secrets), use Docker secrets (Swarm mode) or inject from your platform's secrets manager at deploy time. You can also use x- anchors in YAML to define reusable blocks.

What happens if I run docker compose up twice?

Compose is idempotent. Running up a second time detects that the containers already exist and are running — it does not create duplicates. If you changed the Compose file, Compose recreates only the affected containers. Use docker compose up --force-recreate to rebuild all containers even if nothing changed.

🔥

That's Docker. Mark it forged?

6 min read · try the examples if you haven't

Previous
Docker Volumes and Networking
9 / 18 · Docker
Next
Docker Registry and Docker Hub