🔧 Wait for Services to Start in Docker Compose: wait-for-it vs Healthcheck
Nachrichtenbereich: 🔧 Programmierung
🔗 Quelle: dev.to
When developing applications using Docker Compose, it's common to encounter situations where one service must wait for another to be ready before starting. For example, a web application might need to wait for a database or another dependent service to be up and running. In this post, we'll explore two popular ways to solve this problem:
- Using the
wait-for-it.sh
script - Leveraging
depends_on
andhealthchecks
in Docker Compose
We'll create a simple Python application for demonstration, but don't worry if you're not familiar with Python—you can just copy the code.
Setting up the Environment
Let's start by creating a working directory:
mkdir waitforit
cd waitforit
Next, we’ll build a simple aiohttp
-based web application with a /healthz
endpoint for health checks. We’ll also add a start delay using an environment variable SLEEP_BEFORE_START
, which allows us to simulate service startup delays. The delay can be set by passing the number of seconds in this variable.
File: app.py
import os
import time
from aiohttp import web
async def healthz(request):
return web.Response(text="OK")
if __name__ == "__main__":
sleep_time = int(os.getenv("SLEEP_BEFORE_START", 0))
print(f"Sleeping for {sleep_time} sec before starting...")
time.sleep(sleep_time)
app = web.Application()
app.add_routes([web.get("/healthz", healthz)])
print("Starting...")
web.run_app(app, host="0.0.0.0", port=8080)
Now, let's create a Dockerfile
for the application.
File: Dockerfile
FROM python:3.9-slim
WORKDIR /app
RUN pip install aiohttp \
&& apt-get update \
&& apt-get install -y curl
COPY app.py .
CMD ["python", "-u", "app.py"]
Next, we’ll create a docker-compose.yml
file to launch two services:
- web – the web app with a startup delay.
- dependent – a service that needs to wait until web is fully up.
File: docker-compose.yml
version: "3.8"
services:
web:
image: sleep-web-app
build: .
environment:
SLEEP_BEFORE_START: 10
dependent:
image: sleep-web-app
depends_on:
- web
Let's start the containers and observe the result:
docker compose up --build
You’ll notice that dependent starts before web is fully ready:
web-1 | Sleeping for 10 sec before starting...
dependent-1 | Sleeping for 0 sec before starting...
dependent-1 | Starting...
web-1 | Starting...
Our goal is to configure Docker Compose so that dependent only starts after web is fully ready. The desired log should look like this:
web-1 | Sleeping for 10 sec before starting...
web-1 | Starting...
dependent-1 | Sleeping for 0 sec before starting...
dependent-1 | Starting...
Using wait-for-it.sh
wait-for-it.sh
is a simple Bash script that blocks service execution until another service becomes available, checking the availability of a specific port. It’s useful for waiting on databases, web services, or any other TCP services.
The script needs to be run before the main command to start the application. This can be done in the entrypoint
or by directly inserting it before the start command. In our example, we'll use the second method for simplicity.
As arguments, it is enough to pass the host:port
whose availability we expect. The script will then wait for 15 seconds (the default value). To set your own wait time, you can add the argument --timeout <sec>
. If the port becomes available within that time, the script will stop, and the service will continue to start. However, if the host:port
remains unavailable after the timeout, the service will still start. To avoid this and stop the launch with an error status, you must pass the --strict
flag.
Here’s how to download and use it:
curl -o wait-for-it.sh https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh
chmod +x wait-for-it.sh
Let's add a dependent volume to the service, through which we will pass the script. We will also add the application startup command using wait-for-it.sh
. The script will wait for the web service to be ready on port 8080
. The main command to start the service is executed after --
. You can experiment with the parameters and different startup scenarios. We will consider an example where we have a rough idea of how long to wait for the web service (10 seconds) and will leave the timeout at the default (15 seconds).
dependent:
image: sleep-web-app
command:
- ./wait-for-it.sh
- web:8080
- --
- python
- -u
- app.py
volumes:
- ./wait-for-it.sh:/app/wait-for-it.sh
Run the services again:
docker-compose up
The log shows that dependent now waits for web to be available before starting:
dependent-1 | wait-for-it.sh: waiting 15 seconds for web:8080
web-1 | Sleeping for 10 sec before starting...
web-1 | Starting...
dependent-1 | wait-for-it.sh: web:8080 is available after 11 seconds
dependent-1 | Starting...
Pros of wait-for-it.sh
:
- Easy to implement: just download and run before the main command.
- Flexible: you can wait for any port on any service, even external ones.
- Simple: no additional configuration needed.
Cons:
- It only checks port availability, not the full readiness of the service (e.g., database schema initialization). The next approach can solve this.
Using depends_on
and healthcheck
Another option is to use Docker Compose’s depends_on
in combination with healthcheck
. This approach waits for a service to pass a health check before starting dependent services.
In this approach, the service must have a healthcheck
that checks its readiness to handle requests, while the dependent service should have depends_on
with a condition based on the healthcheck
of the first service. In simple terms, the dependent service starts when the first service is healthy (ready to handle requests).
Let's add a healthcheck
for the web service to check its readiness via the /healthz
endpoint. This way, we will know that not only is the port open, but the service is also ready to process requests. You can read more about healthcheck
here. In test
, we specify the command that checks the service's health. The command could simply check if the port is open, which would be equivalent to wait-for-it.sh
. However, the essence of healthcheck
is not just to check the port, but to verify that the service is ready to handle requests.
web:
image: sleep-web-app
build: .
environment:
SLEEP_BEFORE_START: 10
healthcheck:
test: ["CMD", "curl", "http://web:8080/healthz"]
interval: 10s # How often the status will be checked
retries: 5 # How many times to check before considering it unavailable
start_period: 10s # How long after startup to begin checks
timeout: 10s # Timeout for each test run
We will add depends_on
with a condition to the dependent service. The dependent service relies on web, but it will only start after the web service successfully passes the health check. You can read more about conditional depends_on
here. It's worth mentioning that not all versions of Compose support conditional depends_on
.
Final Compose:
version: "3.8"
services:
web:
image: sleep-web-app
build: .
environment:
SLEEP_BEFORE_START: 10
healthcheck:
test: ["CMD", "curl", "http://web:8080/healthz"]
interval: 10s
retries: 5
start_period: 10s
timeout: 10s
dependent:
image: sleep-web-app
depends_on:
web:
condition: service_healthy
Run the services:
docker-compose up
This time, the dependent service will start execution only after the health check of the web service via /healthz
is successful.
web-1 | Sleep 10 sec before start...
web-1 | Starting...
dependent-1 | Sleep 0 sec before start...
dependent-1 | Starting...
Popular services have ready-made solutions for healthcheck
:
postgresql: ["CMD", "pg_isready", "-U", "postgres"]
kafka: ["CMD-SHELL", "kafka-broker-api-versions.sh --bootstrap-server localhost:9092"]
redis: ["CMD", "redis-cli", "ping"]
mysql: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
mongodb: ["CMD", "mongo", "--eval", "db.adminCommand('ping')"]
elasticsearch: ["CMD-SHELL", "curl -f http://localhost:9200/_cluster/health || exit 1"]
Pros of depends_on
+ healthcheck
:
- Health checks give a more reliable indication of service readiness compared to port checks.
- It’s a built-in Docker Compose solution — no need for extra scripts.
- Easier to manage chains of service dependencies.
Cons:
- More configuration required.
- Not all images come with health checks by default, and custom scripts may be needed.
- It’s harder to check external services outside of Compose.
Which Method to Choose?
The choice between wait-for-it.sh
and depends_on
with healthcheck
depends on your use case:
Use
wait-for-it.sh
for quick and simple port availability checks, especially in environments where you have more control over dependencies.Use
depends_on
+healthcheck
for a more reliable, built-in Docker Compose solution that ensures full service readiness.
Conclusion
Managing service readiness in Docker Compose is crucial for building reliable multi-service applications. Whether you opt for the simplicity of wait-for-it.sh
or the more robust depends_on
with health checks, both methods help ensure that services wait for dependencies to be fully ready before starting. By choosing the right strategy, you can avoid potential issues and make service startup smoother in your Docker-based applications.