Il problema che Compose risolve

Un’applicazione reale non è quasi mai fatta di un solo container. Un’API web ha bisogno di un database, il database ha bisogno di un volume perché i dati sopravvivano a un riavvio, e poi aggiungi una cache Redis e un worker in background. Sono quattro comandi docker run, ognuno con i suoi flag per porte, volumi, variabili d’ambiente e una rete condivisa. Nell’ordine giusto. Ogni volta che ti metti a lavorare.

Docker Compose sostituisce quei comandi con un file e un comando. Descrivi i container e come si collegano in un file chiamato compose.yaml, e docker compose up li avvia tutti. Se le immagini e docker run sono una novità per te, leggi prima cos’è Docker e come funzionano i container.

Cosa descrive il file compose

Un file compose ha una manciata di chiavi di primo livello. Quella che usi sempre è services — ogni servizio è un container che Compose eseguirà.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
services:
  api:
    build: .
    ports:
      - "8000:8000"
    environment:
      DATABASE_URL: postgres://app:secret@db:5432/app
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:18
    volumes:
      - db-data:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: app
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
      timeout: 3s
      retries: 5

volumes:
  db-data:

Due container. api viene costruito dal Dockerfile nella directory corrente e pubblica la porta 8000. db esegue l’immagine ufficiale postgres:18 e tiene i suoi dati in un volume con nome, così i dati restano quando il container viene ricreato.

Qui il lavoro vero lo fanno due dettagli:

  • api raggiunge db all’hostname db. Compose mette ogni servizio su una rete condivisa e registra il nome di ogni servizio come nome DNS. Niente indirizzi IP, niente vecchio --link. DATABASE_URL punta a db:5432 e viene risolto.
  • depends_on con condition: service_healthy trattiene api finché Postgres non risponde davvero. Un depends_on semplice aspetta solo che il container parta, non che il processo del database al suo interno accetti connessioni: è questa differenza il motivo più comune per cui un’app dà “connection refused” al primo avvio.

Le tre chiavi principali: services, volumes, networks

Tre chiavi di primo livello corrispondono a concetti Docker che già conosci:

ChiaveCosa definisceA mano si fa con
servicesi container da eseguiredocker run
volumesvolumi con nome per i dati che devono persisteredocker volume create
networksreti tra i servizidocker network create

networks lo dichiari raramente. Compose crea una rete per progetto e ci collega ogni servizio, ed è per questo che l’esempio con Postgres non aveva bisogno di alcuna configurazione di rete. Dichiara le reti esplicitamente solo quando vuoi impedire a gruppi di servizi di raggiungersi tra loro:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
services:
  api:
    build: .
    networks: [frontend, backend]
  db:
    image: postgres:18
    networks: [backend]        # not reachable from frontend
  proxy:
    image: caddy:2
    networks: [frontend]

networks:
  frontend:
  backend:

I comandi che userai davvero

Esegui questi dalla directory che contiene compose.yaml.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Avvia tutto, log a schermo sul terminale
docker compose up

# Avvia in background
docker compose up -d

# Ricostruisce le immagini che hanno una sezione build, poi avvia
docker compose up --build

# Ferma e rimuove container e reti (i volumi con nome restano)
docker compose down

# Come sopra, ma cancella anche i volumi con nome
docker compose down -v

# Cosa è in esecuzione per questo progetto
docker compose ps

# Segue i log di un servizio
docker compose logs -f api

# Comando singolo in un container nuovo
docker compose run --rm api python manage.py migrate

# Shell in un container già in esecuzione
docker compose exec api bash

up e down sono la coppia che digiti più spesso. up si può rilanciare quante volte vuoi: dopo che hai modificato il file, ricrea solo i servizi la cui configurazione è cambiata e lascia stare gli altri.

Cosa stampa docker compose up al primo avvio

Esegui l’esempio con Postgres qui sopra e ottieni più o meno questo:

1
2
3
4
5
[+] Running 4/4
 ✔ Network app_default    Created
 ✔ Volume "app_db-data"   Created
 ✔ Container app-db-1      Healthy
 ✔ Container app-api-1     Started

Ai nomi Compose antepone il nome del progetto (di default quello della directory) e aggiunge in coda un numero, perché è pronto a eseguire più di una replica di uno stesso servizio. app-db-1 risulta Healthy prima che app-api-1 parta — è condition: service_healthy che fa il suo lavoro.

Come tenere le password fuori dal file compose

Compose legge un file chiamato .env nella directory del progetto e sostituisce i riferimenti ${VAR} nel file compose:

1
2
3
4
5
services:
  db:
    image: postgres:18
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
1
2
# .env  — add this file to .gitignore
DB_PASSWORD=secret

Tieni .env fuori dal version control e committa invece un .env.example con valori vuoti o fittizi. Per qualcosa di davvero sensibile in un ambiente in produzione, usa i Docker secrets invece delle variabili d’ambiente.

Servizi opzionali con i profiles

Non tutti i servizi devono partire ogni volta. Una voce profiles tiene un servizio inattivo finché non lo chiedi:

1
2
3
4
5
6
7
8
9
services:
  api:
    build: .
  db:
    image: postgres:18
  seed:
    build: .
    command: python manage.py seed_demo_data
    profiles: [tools]

docker compose up avvia api e db e ignora del tutto seed. docker compose --profile tools run --rm seed esegue il seeder quando lo chiedi. Usa i profiles per tutto ciò che è una tantum: seeder, migrazioni, una shell di debug, un generatore di carico che vuoi solo durante un test.

Compose v2 vs il vecchio docker-compose

Se una guida ti dice di eseguire docker-compose con il trattino, è precedente al 2023. Quella era la v1, scritta in Python, ora a fine vita. Lo strumento attuale è docker compose come sottocomando della CLI di Docker; è incluso in Docker Desktop e nel pacchetto plugin Compose per Docker Engine.

Nei file più vecchi troverai in cima anche questa riga:

1
version: "3.8"   # delete it

La chiave version non ha alcun effetto in Compose v2. Compose valida rispetto alla Compose Specification attuale e ti avvisa quando la chiave è presente. Rimuovila e l’avviso sparisce.

Anche il nome di default del file è cambiato: compose.yaml è quello attuale, docker-compose.yml funziona ancora, e Compose cerca entrambi.

Live reload durante lo sviluppo

docker compose watch (Compose 2.22 e successivi) aggiorna i container mentre modifichi il codice. Aggiungi un blocco develop al servizio:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
services:
  api:
    build: .
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src
        - action: rebuild
          path: ./requirements.txt

sync copia i file modificati direttamente nel container in esecuzione, così una modifica al codice ha effetto senza un rebuild. rebuild fa scattare un rebuild completo dell’immagine quando cambia un file che influisce sulla build, come un file di lock delle dipendenze. Avvialo con docker compose watch, oppure aggiungi --watch a up.

Quando Compose è lo strumento sbagliato

Compose esegue i container su una sola macchina. È questo il confine, e gran parte di quello che sembra mancare a Compose sta dall’altra parte.

Va bene per lo sviluppo locale, i test automatici in CI e piccoli deploy su un singolo host. Non sa distribuire container su più server, sostituirne uno quando un nodo muore, fare rollout progressivi vincolati agli healthcheck, né gestire l’autoscaling.

Sono compiti di un orchestratore: Kubernetes, o Docker Swarm se vuoi qualcosa di più leggero. I due non sono in competizione. Un file compose è spesso la bozza che poi diventa un insieme di manifest Kubernetes, e parecchi team continuano a usare Compose in locale molto tempo dopo che la produzione è passata a un cluster.

Anche con Podman c’è un percorso compatibile: podman compose e podman-compose leggono lo stesso formato di file, con i distinguo trattati in Docker vs Podman.

Errori comuni

  • Secret committati in compose.yaml. Il file finisce su Git. Usa .env (in gitignore) o i Docker secrets.
  • Fidarsi di depends_on per aspettare che un servizio sia pronto. Da solo aspetta che il container parta, non che il servizio accetti connessioni. Abbinalo a un healthcheck e condition: service_healthy.
  • Impostare container_name. Ti impedisce di eseguire più di una copia del progetto e rompe docker compose up --scale. Lascia che sia Compose a nominare i container.
  • Fare bind-mount sopra una cartella di dipendenze installate. Montare la cartella del progetto in un container Node o Python può nascondere il node_modules o il virtualenv creato durante la build. Monta le sottocartelle del sorgente, oppure metti un volume anonimo sul percorso delle dipendenze.

Come portare un’app esistente su Compose

Prendi l’app che oggi avvii con uno script pieno di righe docker run e spostala in un compose.yaml un servizio alla volta. Falla partire, leggi i log, sistema quello che si rompe, aggiungi il servizio successivo. Dai un healthcheck a tutto ciò da cui altri servizi dipendono: è il passaggio che di solito si salta, ed è quello che ferma il “connection refused” all’avvio a freddo.

Saprai che ha funzionato quando docker compose down -v && docker compose up ricostruisce l’intero ambiente da zero e l’app riparte sempre allo stesso modo.

Articoli correlati