DEV Community

Cover image for Wait for Services to Start in Docker Compose: wait-for-it vs Healthcheck
Pavel Loginov
Pavel Loginov

Posted on

Wait for Services to Start in Docker Compose: wait-for-it vs Healthcheck

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:

  1. Using the wait-for-it.sh script
  2. Leveraging depends_on and healthchecks 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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

Next, we’ll create a docker-compose.yml file to launch two services:

  1. web – the web app with a startup delay.
  2. 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
Enter fullscreen mode Exit fullscreen mode

Let's start the containers and observe the result:

docker compose up --build
Enter fullscreen mode Exit fullscreen mode

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...
Enter fullscreen mode Exit fullscreen mode

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...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Run the services again:

docker-compose up
Enter fullscreen mode Exit fullscreen mode

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...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Run the services:

docker-compose up
Enter fullscreen mode Exit fullscreen mode

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...
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)