Docker Compose depends_on Not Waiting for Database Readiness on WSL2 Ubuntu
Resolve Docker Compose services starting before databases are ready on Windows WSL2 Ubuntu. Implement robust startup dependencies.
Resolve Docker Compose services starting before databases are ready on Windows WSL2 Ubuntu. Implement robust startup dependencies.
Docker Compose's depends_on directive is a fundamental tool for orchestrating multi-service applications. However, a common pitfall encountered by developers, particularly in Windows Subsystem for Linux 2 (WSL2) environments, is when an application service attempts to connect to a database before the database container is fully initialized and ready to accept connections. This guide provides a highly technical, step-by-step resolution to this classic race condition.
Symptom & Error Signature
When deploying an application stack with Docker Compose on WSL2, the application container often starts and exits quickly with database connection errors, even though depends_on specifies the database service. This happens because depends_on only guarantees the start order of containers, not the readiness of the services running inside them.
You might observe logs similar to these from your application container (e.g., a Python, Node.js, or PHP application attempting to connect to PostgreSQL or MySQL):
# docker-compose up output snippet
db_1 | 2023-10-26 10:30:05.123 UTC [1] LOG: starting PostgreSQL 15.4 (Debian 15.4-1.pgdg120+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 12.2.0-14) 12.2.0, 64-bit
db_1 | 2023-10-26 10:30:05.125 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432
db_1 | 2023-10-26 10:30:05.125 UTC [1] LOG: listening on IPv6 address "::", port 5432
db_1 | 2023-10-26 10:30:05.128 UTC [1] LOG: database system was shut down at 2023-10-26 10:30:04 UTC
db_1 | 2023-10-26 10:30:05.456 UTC [1] LOG: database system is ready to accept connections
app_1 | Waiting for database connection...
app_1 | Traceback (most recent call last):
app_1 | File "/app/main.py", line 15, in <module>
app_1 | conn = psycopg2.connect(DATABASE_URL)
app_1 | psycopg2.OperationalError: connection to server at "db" (172.X.X.X), port 5432 failed: Connection refused
app_1 | Is the server running on that host and accepting TCP/IP connections?
app_1 |
app_1 | Exiting due to database connection error.
my_app-app-1 exited with code 1
Notice that the application attempts to connect before the database logs database system is ready to accept connections.
Root Cause Analysis
The core of this issue lies in the fundamental difference between "container started" and "service ready".
depends_onLimitations: Thedepends_ondirective indocker-compose.yml(e.g.,app: depends_on: - db) only ensures that thedbcontainer is started before theappcontainer. It does not wait for the database process inside thedbcontainer to be fully initialized, listening on its port, and capable of processing queries.- Database Initialization Time: Databases like PostgreSQL or MySQL require a period to initialize their data directories, start their server processes, apply migrations, and become fully operational. This can take anywhere from a few milliseconds to several seconds, depending on the database and its configuration.
- Race Condition: The application container starts, resolves the
dbhostname, and immediately attempts to establish a connection. If the database server isn't yet listening or ready, the connection attempt fails, leading to the "Connection refused" or similar errors. - WSL2 Context (Contributing Factor): While not the root cause, WSL2's virtualization layer can sometimes introduce subtle timing variations or initial resource allocation delays compared to a native Linux environment. This can make the inherent race condition more pronounced or frequent for some users.
To resolve this, we need to implement mechanisms that explicitly wait for service readiness before allowing dependent services to proceed.
Step-by-Step Resolution
The solution involves a combination of Docker Compose's advanced dependency options and custom container entrypoint scripts to ensure robust service readiness checks.
1. Define Database Health Checks (healthcheck)
Add a healthcheck block to your database service in docker-compose.yml. This tells Docker how to determine if the service inside the container is truly healthy, not just running.
For PostgreSQL (e.g., service named db):
# docker-compose.yml
version: '3.8'
services:
db:
image: postgres:15
restart: always
environment:
POSTGRES_DB: mydatabase
POSTGRES_USER: user
POSTGRES_PASSWORD: password
ports:
- "5432:5432"
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 5
start_period: 10s # Give the DB time to initialize before starting health checks
# ... other services
volumes:
db_data:
For MySQL (e.g., service named db):
# docker-compose.yml
version: '3.8'
services:
db:
image: mysql:8.0
restart: always
environment:
MYSQL_DATABASE: mydatabase
MYSQL_USER: user
MYSQL_PASSWORD: password
MYSQL_ROOT_PASSWORD: rootpassword
ports:
- "3306:3306"
volumes:
- db_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "${MYSQL_USER}", "-p${MYSQL_PASSWORD}"]
interval: 5s
timeout: 5s
retries: 5
start_period: 10s # Give the DB time to initialize before starting health checks
# ... other services
volumes:
db_data:
Ensure the
testcommand within yourhealthcheckuses appropriate credentials and commands for your specific database. For PostgreSQL,pg_isreadyis excellent. For MySQL,mysqladmin pingis a standard choice. Thestart_periodis crucial as it prevents health checks from failing while the database is still performing its initial setup.
2. Configure Application to Wait for Database Readiness (depends_on with condition: service_healthy)
Now, update your application service's depends_on entry to leverage the newly defined healthcheck. This is a powerful feature of Docker Compose V2 (CLI plugin) and newer V1 versions.
# docker-compose.yml
version: '3.8'
services:
db:
# ... healthcheck defined above ...
app:
build: . # Or image: myapp:latest
ports:
- "8000:8000"
environment:
DATABASE_URL: postgres://user:password@db:5432/mydatabase
depends_on:
db:
condition: service_healthy # <-- This is the key
The
condition: service_healthysyntax requires Docker Compose V2 (e.g.,docker composecommand) or Docker Compose V1.27+ withversion: '3.8'in yourdocker-compose.yml. If you're using an older Docker Compose version, this condition will be ignored, and you'll rely solely on the next step.
3. Implement an Entrypoint Script for Application (Robust Waiting)
Even with condition: service_healthy, it's a best practice, especially in production or complex deployments, to have the application container itself perform a final check and retry mechanism. This catches any minor race conditions or temporary network glitches that might occur after the database is marked "healthy" but before the application truly connects.
Create an entrypoint.sh file in your application's root directory:
#!/bin/bash
# entrypoint.sh
set -e # Exit immediately if a command exits with a non-zero status
HOST="db" # The name of your database service in docker-compose
PORT="5432" # The port your database listens on
echo "Waiting for database '$HOST:$PORT' to be ready..."
# Loop until netcat (nc) can connect to the database host and port
# We use nc -z for zero-I/O scan, checking if the port is open
# This will retry every 1 second
while ! nc -z $HOST $PORT; do
sleep 1
done
echo "Database '$HOST:$PORT' is available. Starting application..."
# Execute the main command (CMD) specified in the Dockerfile
exec "$@"
Then, modify your application's Dockerfile to include this entrypoint script:
# Dockerfile for your application
FROM python:3.9-slim-buster # Or your base image
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
# Copy and make the entrypoint script executable
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
# Install netcat if your base image doesn't have it (common for slim/alpine images)
# Uncomment the appropriate line for your base image:
# For Debian/Ubuntu-based images (like python:*-slim-buster):
RUN apt-get update && apt-get install -y netcat-traditional && rm -rf /var/lib/apt/lists/*
# For Alpine-based images:
# RUN apk add --no-cache netcat-openbsd
# Set the entrypoint to our script, which will then execute the CMD
ENTRYPOINT ["entrypoint.sh"]
CMD ["python", "main.py"] # Replace with your actual application start command
Ensure you install
netcat(ornetcat-traditional/netcat-openbsd) in your Dockerfile as it's often not present in minimal base images. Theset -ein the entrypoint script is crucial for ensuring that if any command within the entrypoint script fails, the container exits rather than proceeding incorrectly.
4. (Optional) Use a Helper Script (e.g., wait-for-it.sh or dockerize)
For more advanced waiting logic (e.g., waiting for specific HTTP responses, longer timeouts, or multiple services), consider using battle-tested helper scripts like wait-for-it.sh or dockerize.
Using wait-for-it.sh:
Add
wait-for-it.shto your Dockerfile:# Dockerfile for your application # ... (rest of your Dockerfile) ... # Add wait-for-it.sh ADD https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh /usr/local/bin/wait-for-it.sh RUN chmod +x /usr/local/bin/wait-for-it.sh # Use wait-for-it.sh as the ENTRYPOINT ENTRYPOINT ["/usr/local/bin/wait-for-it.sh", "db:5432", "--timeout=30", "--strict", "--"] CMD ["python", "main.py"] # Your actual application start commandHere,
db:5432is the host and port to wait for,--timeout=30sets a 30-second timeout, and--strictwill exit if a connection can't be made. The--separateswait-for-it.sharguments from your application'sCMD.
5. Verify and Test
After implementing these changes, rebuild your application container and bring up your services:
docker-compose down # Stop and remove old containers
docker-compose build app # Rebuild only the app service (if using `build: .`)
docker-compose up # Start all services
Observe the logs. You should now see the application container waiting patiently for the database to become ready, indicated by messages from your entrypoint.sh script or wait-for-it.sh, before proceeding to start your main application process. The database logs should show it becoming ready before the application attempts its first connection.
By combining Docker Compose's healthcheck with depends_on condition: service_healthy and a robust entrypoint script, you create a resilient startup sequence that gracefully handles database readiness, ensuring your applications start reliably in any Docker environment, including WSL2.