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.
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.
YAMLservices: 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.
YAMLservices: 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
| Feature | Docker Healthcheck | Wait-for-it Script |
|---|---|---|
| Orchestration | Native to Docker Compose | Manual wrapper |
| Logic Location | docker-compose.yml | Entrypoint/CMD |
| Flexibility | High (Custom commands) | High (Shell-based) |
| Maintenance | Low (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.