When setting up a development environment using Docker Compose, you might encounter service dependency issues. Take a simple example: the database container usually needs to start up first before the API server container can follow. This is because the API server might need to establish a connection pool with the database before it can offer API services; otherwise, errors could occur and cause the container to fail to start.
In this situation, the API server container relies on the database container to function properly, which is what we refer to as service dependency. Without understanding service dependencies, you could frequently experience inconsistent results with docker-compose, sometimes successful and sometimes not, as services might or might not start up in time or be ready.
To address this, Docker Compose provides a solution with the depends_on setting.
This article will guide you on how to use the depends_on
setting.
Environment for this Article
- macOS
- OrbStack
Project Structure Example in this Article
Here’s the project structure for the example used in this article. The Dockerfile
is used to build a container for running app.py
, and compose.yaml
is the Docker Compose file to set up the development environment:
.
├── Dockerfile
├── app.py
└── compose.yaml
We’re using Python Flask to set up a simple web server as a simulation. Don’t worry if you’re not familiar with Python; it won’t affect the tutorial. Below is the content of app.py
, which connects to Redis to increment the hits key value each time you GET /
on app.py
.
app.py
import time
import redis
from flask import Flask
app = Flask(__name__)
cache = redis.Redis(host='redis', port=6379)
def get_hit_count():
retries = 5
while True:
try:
return cache.incr('hits')
except redis.exceptions.ConnectionError as exc:
if retries == 0:
raise exc
retries -= 1
time.sleep(0.5)
@app.route('/')
def hello():
count = get_hit_count()
return 'Hit count: {}\n'.format(count)
Below is the content of the Dockerfile
.
Dockerfile
FROM python:3.8-alpine
WORKDIR /app
ENV FLASK_APP=app.py
RUN apk add --no-cache gcc musl-dev linux-headers
RUN pip install flask redis
EXPOSE 5000
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
Below is the content of compose.yaml
, showing the development environment that needs to run Redis and our compiled web server.
compose.yaml
services:
web:
build: .
ports:
- "8000:5000"
redis:
image: "redis:alpine"
Explanation of depends_on
As mentioned earlier, depends_on
is a setting to solve service dependencies.
Short Syntax
In the example from this article, the web
container depends on the redis
container, which can be represented with the arrow relationship web → redis
. Therefore, you need to add the depends_on
setting to the web
container to clearly specify that it needs to wait for the redis
container to start.
Thus, you can modify the compose.yaml
like this:
services:
web:
build: .
ports:
- "8000:5000"
depends_on:
- redis
redis:
image: "redis:alpine"
If there are multiple dependencies, you can refer to the following example:
services:
web:
build: .
ports:
- "8000:5000"
depends_on:
- container_1
- container_2
container_1:
image: "redis:alpine"
container_2:
image: "redis:alpine"
The previous depends_on
setting just lists the names of dependent containers, which is called short syntax 1. It only indicates the order of startup, so the redis
container will start before the web
container.
Although the above setting starts the redis
container before the web
container, there may still be issues under some circumstances:
- The
redis
container takes longer to start running than expected. - The
redis
container fails to function properly after starting.
These situations will cause the web
container to fail to start because the redis
container isn’t ready to provide services when the web
container attempts to connect.
This can be made clearer with a timeline:
To address time gaps or improper container operations, you can switch to long syntax to specify more explicitly when a dependent container should be ready before starting another.
Long Syntax
depends_on
in long syntax allows you to explicitly specify conditions that a dependent container must meet.
Condition
In this example, we need the redis
container to be fully operational before starting the web
container. We can use the condition
setting to specify that we should wait until the redis
is in a service_healthy
state before starting web
. Thus, you can further modify compose.yaml
like this:
services:
web:
build: .
ports:
- "8000:5000"
depends_on:
redis:
condition: service_healthy
redis:
image: "redis:alpine"
Three values can be specified for the condition
setting:
service_started
: similar to short syntax; it passes the condition simply if the dependent container has started.service_completed_successfully
: waits for the dependent container to finish execution successfully (exit code must be 0). This is generally less common.service_healthy
: waits for the dependent container to be in a healthy state. This requires containers with a specifiedHEALTHCHECK
either during the build phase or explicitly in the docker-compose file; otherwise, errors likedependency failed to start
might occur.
Since the official Docker image of Redis doesn’t include a HEALTHCHECK
, you’ll need to add a healthcheck
in compose.yaml
:
services:
web:
build: .
ports:
- "8000:5000"
depends_on:
redis:
condition: service_healthy
redis:
image: "redis:alpine"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
timeout: 10s
retries: 3
start_period: 10s
Here’s an explanation of the healthcheck
block:
test: ["CMD", "redis-cli", "ping"]
: the command to check the health status.CMD
indicates executing a command. Redis’sredis-cli ping
is handy to verify if the service is operational.start_period: 10s
: wait 10 seconds after the container starts before running the check command.timeout: 10s
: if the check exceeds 10 seconds, it’s considered unhealthy, assuming the service should respond within 10 seconds.retries: 3
: retries up to 3 times if unhealthy. A single successful response ends the checks, marking it healthy; if all 3 retries fail, the container is considered unhealthy.
You can test the above Docker Compose file with the command below, and you’ll notice that the web
container waits about 10 seconds after the redis
container starts:
$ docker compose up --build
For more information on HEALTHCHECK, see the documentation.
Required
Besides condition
, there is also required
, which you can use to specify if a container is optional with required: false
.
Initially, the redis
container needs to be healthy to start the web
container:
services:
web:
build: .
ports:
- "8000:5000"
depends_on:
redis:
condition: service_healthy
redis:
image: "redis:alpine"
But if the redis
container lacks a healthcheck
, you may encounter a dependency failed to start
error, ultimately preventing the web
container from starting.
If the state of redis
doesn’t matter for the web
container, use required: false
. This allows the web
container to start even if redis
doesn’t start correctly:
services:
web:
build: .
ports:
- "8000:5000"
depends_on:
redis:
condition: service_healthy
required: false
redis:
image: "redis:alpine"
You can execute the above docker-compose file with:
$ docker compose up --build
Restart
Along with condition
and required
, there’s also restart
, which requires a condition
.
The restart
setting ensures that if a dependent container restarts or is updated, the containers depending on it also restart.
For example:
services:
web:
build: .
ports:
- "8000:5000"
depends_on:
redis:
condition: service_started
restart: true
redis:
image: "redis:alpine"
If redis
is set with restart: true
, when you restart it with:
$ docker compose restart redis
You’ll notice the web
container restarts as well:
Restarting 2/2
✔ Container myrepo-redis-1 Started
✔ Container myrepo-web-1 Started
Conclusion
The depends_on
setting is crucial in a docker-compose file as modern development environments often require interactions among multiple containers, such as typical Web API servers and databases or cache servers. These dependencies might lead docker-compose not to behave as expected since some containers need others to function properly.
Learning to use depends_on
helps resolve dependency issues and results in a more robust docker-compose file, creating a more stable development environment.
That’s all!
Enjoy!