The problem Link to heading

Multi-container applications often have implicit startup dependencies: the web container can’t start until the database is ready, or the worker can’t connect until the message broker is listening. When containers are started in parallel or in the wrong order, some fail immediately and either crash or sit in a retry loop.

This is more common with applications that weren’t originally designed as microservices and were later split into containers (what I’d call a “Franken-microservice” architecture). But even well-designed systems sometimes need to coordinate startup.

The workarounds below assume you either can’t or don’t want to change the application code itself. The constraints might be:

  • You don’t have access to the source
  • The change would be too invasive or time-consuming
  • You can’t modify the Dockerfile ENTRYPOINT

Workaround 1: healthcheck + depends_on in docker-compose Link to heading

Docker Compose lets you define a healthcheck on a service, a command it runs periodically to determine if the service is ready:

db:
  image: my-db-image
  container_name: db-management
  ports:
    - 31337:31337
  healthcheck:
    test: ["CMD", "curl", "-fk", "https://localhost:31337"]
    interval: 300s
    timeout: 400s
    retries: 10

Combined with depends_on and restart: on-failure on the dependent service:

web:
  image: my-web-image
  restart: on-failure
  depends_on:
    - db
  links:
    - db

What happens:

  • Compose starts db first (because web depends on it)
  • web starts shortly after; it doesn’t wait for db to be ready, just for the container to exist
  • While db is still initializing, web fails on startup and is restarted by restart: on-failure
  • Once db is marked healthy, web stays up

Updated 2026: In Docker Compose file format v2.1+, depends_on was extended to support condition: service_healthy, meaning the dependent container waits until the dependency passes its healthcheck before starting. This avoids the restart loop entirely. In Docker Compose v3 and the current Compose Specification, this condition syntax is also supported. Check your Compose file version and the current depends_on documentation.

Also note: links is a legacy feature and largely unnecessary with modern Docker Compose. Services in the same Compose network can reach each other by service name without links.

The main drawback of the restart approach: if web is running tests, restarts look like test failures, which can confuse CI pipelines.

Workaround 2: wait-for-it wrapper script Link to heading

wait-for-it is a shell script that blocks until a given host:port is accepting TCP connections, then runs a specified command. It’s more targeted than the restart approach:

db:
  container_name: db-management
  ports:
    - 31337:31337
  healthcheck:
    test: ["CMD", "curl", "-fk", "https://localhost:31337"]
    interval: 300s
    timeout: 400s
    retries: 10

web:
  image: my-web-image
  depends_on:
    - db
  links:
    - db
  command: ["./wait-for-it.sh", "db:31337", "--", "./webapp"]

web won’t attempt to start the application until db:31337 is open. The container doesn’t restart in a loop; it just waits.

The downside: this approach is invasive. The wait-for-it script needs to be included in the container image (use a multi-stage build to copy it in), and the original CMD or ENTRYPOINT has to be wrapped.

Note that wait-for-it only checks TCP connectivity, not application readiness. The port being open doesn’t guarantee the database is ready to accept queries.

The real solution: resilient application design Link to heading

Both workarounds above are patches on top of a design problem. The right answer is to make the application resilient to dependency unavailability: it should retry connections with backoff, degrade gracefully, and recover without external orchestration.

The 12-factor app methodology covers this well, particularly the process isolation and stateless process model principles. An application that can tolerate its dependencies being temporarily unavailable doesn’t need external startup ordering at all.

This takes real architectural work, but it’s the only approach that holds up at scale; restart loops and TCP polling scripts don’t compose well across dozens of services.