We use cookies to enhance your experience on the site
CodeWorlds

Docker Compose - multi-service networking and volumes

Your application is no longer one process. There is the API, there is MongoDB, there is Redis - and to run the whole thing locally you must call

docker run
three times with a long list of flags, in the right order, remembering the network and the fact that the database needs a moment to start. Every new team member receives these instructions by word of mouth, and slightly differently each time.

A legion did not march on verbal arrangements. It had a daily order: a written list of units, their sequence, and what each takes along. Docker Compose is such an order - it manages several containers at once.

Do not confuse it with its neighbours: building images from source is what

docker build
and a Dockerfile do, testing is your pipeline's job, and watching resource usage belongs to the tools you met under observability.

The daily order

The whole configuration fits in one

docker-compose.yml
file:

1services:
2  mongodb:
3    image: mongo:7
4    volumes:
5      - mongo-data:/data/db
6    healthcheck:
7      test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
8      interval: 10s
9      retries: 5
10
11  redis:
12    image: redis:7-alpine
13
14  api:
15    build: .
16    ports:
17      - "3000:3000"
18    environment:
19      DATABASE_URL: mongodb://mongodb:27017/legions
20      REDIS_HOST: redis
21    depends_on:
22      mongodb:
23        condition: service_healthy
24      redis:
25        condition: service_started
26
27volumes:
28  mongo-data:

Each entry under

services
is one container.
image
takes a ready image,
build: .
tells Compose to build one from the Dockerfile in this directory.

Startup order

The

depends_on
field provides the services' startup order. So our three services come up like this: MongoDB first, then Redis, and last the API, which waits for both.

But here lies the trap most first configurations hit:

depends_on
alone waits only for the container to start, not for it to be ready. MongoDB needs a few seconds to load its data - the container is already running while the database still refuses connections. The API starts, tries to connect and dies.

That is why the database carries a

healthcheck
and
depends_on
carries the condition
condition: service_healthy
. Now Compose waits until the command in
healthcheck
starts succeeding. Redis comes up quickly and
service_started
suffices for it - the default behaviour, meaning "the container is running".

Remember this difference, @name: started is not the same as ready. The great majority of "connection refused" errors at environment startup come from exactly here.

A network with no configuration

Note the database address:

mongodb://mongodb:27017/legions
. There is no
localhost
there and no IP address - there is a service name.

Compose creates a shared network for all the services and registers each in it under its own name. The

api
container connects to the database by simply writing
mongodb
- the rest happens by itself. You need no IP addresses and no configuration.

Note too what is absent from our file: MongoDB and Redis have no

ports
section. That is deliberate. Without it a service is visible only inside the Compose network - the API can reach it, nobody outside can. We expose a port only where it is genuinely needed, which is the API.

Volumes - data that outlives the container

A container is impermanent by design: delete it and everything it wrote disappears. For a database that is a disaster, so volumes store persistent data outside the container.

The

mongo-data:/data/db
entry says: whatever MongoDB writes into the
/data/db
directory, keep it in a volume named
mongo-data
, living independently of the container. Deleting and recreating the container will not touch that data.

Volumes do not limit processor usage, do not encrypt communication and scale nothing - they store data, and that is all.

Running it

The whole order is carried out by one command:

1docker compose up --build -d

The two parts after

up
are worth separating.
--build
rebuilds the images before starting - without it Compose uses the previously built one, however much you changed the code.
-d
(for detached) sends everything to the background and gives you the terminal back.

Stopping has two variants, and the difference between them can be costly.

docker compose down
removes the containers and the network, leaving the volumes.
docker compose down -v
removes the volumes too - meaning the database's data. You run the first daily; the second only when you truly want to start from an empty database.

Summary

The daily order is written and the units march in sequence:

  • Docker Compose manages several containers at once - it does not build images from source, does not test and does not monitor usage,
  • the configuration lives in
    docker-compose.yml
    , and every entry under
    services
    is one container,
  • depends_on
    provides the services' startup order
    ,
  • started is not ready -
    depends_on
    waits for the container to start, not for the service to be usable,
  • healthcheck
    plus
    condition: service_healthy
    waits for actual readiness;
    service_started
    is enough for fast services,
  • containers connect by service name -
    mongodb://mongodb:27017
    , with no IP addresses and no network configuration,
  • no
    ports
    section means the service is visible only inside the Compose network,
  • volumes store persistent data outside the container - they do not limit CPU, encrypt or scale,
  • running it:
    docker compose up --build -d
    , where
    --build
    rebuilds images and
    -d
    sends it to the background,
  • down
    leaves the volumes,
    down -v
    removes them along with the data
    .

In the next lesson we will deal with secrets - the passwords and keys we still wrote out in plain sight in this file. For now remember: Compose is a daily order - one file replacing the verbal instructions everybody remembered differently.

Go to CodeWorlds