Back to Blog
DevOpsJuly 12, 20264 min read

Debugging Docker Compose Service Dependency Failures

Stop your app from crashing at startup. Learn to use docker compose depends_on, healthchecks, and wait-for-it to manage container startup order effectively.

DockerDocker ComposeDevOpsContainersOrchestration

We’ve all been there: you run docker compose up, and your application container immediately crashes with a "connection refused" error because the database container is still initializing. It’s a classic race condition. The app assumes the port is open the second the container starts, but the underlying service usually needs a few seconds to boot up its storage engine.

I spent about two days wrestling with this on a recent microservices migration. We initially tried simple depends_on blocks, but that only guarantees the container starts—not that the service inside is actually ready to accept connections. Here is how to fix this properly.

Understanding the depends_on Limitation

If you just use depends_on in your docker-compose.yml, Docker only ensures the service is created and running. It does not wait for the application inside to finish its boot sequence.

YAML
services:
  web:
    image: my-app:latest
    depends_on:
      - db
  db:
    image: postgres:16

In this setup, web starts as soon as the db container process initiates. If your database takes 5 seconds to run migrations or allocate buffers, your web app will fail to connect. I’ve seen this cause "crash-loop backoff" errors that make local development a nightmare.

Using Docker Healthchecks (The Modern Way)

The most robust way to handle this today is by using a docker healthcheck. This forces Docker to verify that the service is "healthy" before starting dependent containers.

I prefer this method because it keeps the logic inside your docker-compose.yml rather than cluttering your entrypoint scripts.

YAML
services:
  web:
    image: my-app:latest
    depends_on:
      db:
        condition: service_healthy
  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

By setting condition: service_healthy, you ensure the web container stays in a "starting" state until the database reports that it is ready. If you're struggling with intermittent connectivity, I recommend checking out my guide on Debugging Docker Compose Healthcheck: Solving Startup Race Conditions for more granular troubleshooting tips.

When to use a wait-for-it script

Sometimes a healthcheck isn't enough—maybe you're running a legacy image where you can't easily install shell tools, or you need to coordinate between five different services. This is where a wait-for-it script shines.

It’s a simple bash script that polls a specific host and port until they become available. You can wrap your application’s startup command with it:

Bash
# In your Dockerfile or entrypoint
./wait-for-it.sh db:5432 -- ./start-app.sh

I usually keep a copy of this script in my project root and copy it into the image during the build process. It’s a brute-force approach, but it works when you don't have control over the underlying service's healthcheck capabilities.

Comparison: Healthchecks vs. Wait-for-it

FeatureDocker HealthcheckWait-for-it Script
OrchestrationNative to Docker ComposeManual wrapper
Logic Locationdocker-compose.ymlEntrypoint/CMD
FlexibilityHigh (Custom commands)High (Shell-based)
MaintenanceLow (Built-in)Moderate (Needs script file)

Final Thoughts on Service Orchestration

If you find yourself constantly fighting these race conditions, it’s often a sign that your application code needs a more resilient connection strategy. Even with perfect docker compose service orchestration, your app should implement retries for database connections. Networks fail, and services restart—your code should be able to handle a temporary disconnection without crashing.

I’m still experimenting with how to handle even more complex dependency trees, specifically when services need to register themselves with a service discovery tool. For now, sticking to healthchecks has reduced my "container is not ready" issues by roughly 80%. If you need help containerizing your own projects or setting up robust CI/CD pipelines, I offer CI/CD Pipeline & Docker Containerization services to help you ship with confidence.

FAQ

Why does depends_on not wait for my database to be ready? depends_on only controls the order in which containers are started, not the state of the processes inside them. Use condition: service_healthy to wait for the actual service.

Can I use wait-for-it if I don't have bash in my container? No, wait-for-it requires a shell environment. If you're using a minimal image like scratch or distroless, you'll need to compile a small Go or C binary to perform the TCP check instead.

What is the best way to debug a failing dependency? Use docker compose logs -f <service_name> to watch the startup process. If the container is stuck in a restart loop, use docker inspect to check the exit codes. You can also read more about this in my guide on Docker Compose Networking: How to Fix Service Connectivity Issues.

Similar Posts