NestJS course Β· Module 12: Containers and CI/CD

Docker Compose - multi-service networking and volumes

5 min read
In this lesson6

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: 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.

Code for this lesson: src/docker-compose-demo.ts
1// Docker Compose - Imperium Road Network
2console.log("=== DOCKER COMPOSE ===\n");
3
4interface ComposeService {
5  name: string;
6  image: string;
7  port: number;
8  role: string;
9  dependsOn?: string[];
10}
11
12const services: ComposeService[] = [
13  {
14    name: 'api',
15    image: 'Dockerfile (build)',
16    port: 4000,
17    role: 'Main Fortress (Praetorium)',
18    dependsOn: ['mongodb', 'redis'],
19  },
20  {
21    name: 'mongodb',
22    image: 'mongo:7.0',
23    port: 27017,
24    role: 'Imperium Archive (database)',
25  },
26  {
27    name: 'redis',
28    image: 'redis:7-alpine',
29    port: 6379,
30    role: 'Swift Messenger (cache)',
31  },
32];
33
34console.log("Services in docker-compose.yml:\n");
35services.forEach(s => {
36  console.log(`  ${s.name}:`);
37  console.log(`    Image: ${s.image}`);
38  console.log(`    Port: ${s.port}`);
39  console.log(`    Role: ${s.role}`);
40  if (s.dependsOn) {
41    console.log(`    depends_on: ${s.dependsOn.join(', ')}`);
42  }
43  console.log();
44});
45
46// Key commands
47const commands = [
48  { cmd: 'docker compose up -d', desc: 'Run everything in background' },
49  { cmd: 'docker compose up --build -d', desc: 'Build and run' },
50  { cmd: 'docker compose down', desc: 'Stop everything' },
51  { cmd: 'docker compose logs -f api', desc: 'Logs of the api service' },
52  { cmd: 'docker compose ps', desc: 'Services status' },
53];
54
55console.log("Docker Compose commands:\n");
56commands.forEach(c => {
57  console.log(`  $ ${c.cmd}`);
58  console.log(`    ${c.desc}\n`);
59});
60

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. What is Docker Compose used for?

  2. 2. What does the 'depends_on' field ensure in docker-compose.yml?

These are 2 of 3 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Code editor

    Define three services with appropriate images, ports, environment variables, and dependencies

  • Vertical ordering

    Arrange Docker Compose services by startup order (from first):

  • Click in order

    Arrange the elements of the command to run Docker Compose in detached mode with rebuild:

  • Code editor

    Set NODE_ENV, PORT, DATABASE, JWT_SECRET, REDIS_HOST, LOG_LEVEL, ALLOWED_ORIGINS variables for both environments

Useful articles