È uscito il Corso Java Completo
Torna al blog

Cos'è Docker Compose e come si usa

Cos'è Docker Compose e come si usa: anatomia del file compose.yaml, servizi, volumi, reti e healthcheck, con un esempio completo app e database.

Edoardo Midali

Edoardo Midali

Developer · Content Creator

9 min di lettura

Con un container solo Docker è comodo. Con tre — l'applicazione, il database e un reverse proxy — la riga di comando diventa uno script di comandi lunghissimi che devi ricordarti nell'ordine giusto, con la rete creata a mano e i volumi da collegare. In questo articolo vedi cos'è Docker Compose, come si legge un compose.yaml, perché depends_on da solo non fa quello che pensi e come mettere insieme un esempio completo che funziona davvero.

Cos'è Docker Compose

Docker Compose è lo strumento che descrive in un unico file di testo tutti i container di un'applicazione — con le loro immagini, porte, volumi, reti e variabili — e li avvia o li ferma tutti insieme con un comando solo.

È un cambio di mentalità: invece di dire a Docker cosa fare passo per passo, scrivi come deve essere il risultato e lasci a lui il compito di arrivarci. Se hai già letto cos'è Docker e sai avviare un container, Compose è il passo successivo naturale.

Compose oggi è integrato in Docker: si invoca con docker compose (due parole). Il vecchio docker-compose con il trattino era uno script Python separato, ormai fuori supporto — se una guida usa quello, è vecchia.

Il problema che risolve

Prendi un'applicazione web con un database PostgreSQL e una cache Redis. A mano:

docker network create rete-app
docker volume create dati-db
docker run -d --name db --network rete-app -v dati-db:/var/lib/postgresql/data \
  -e POSTGRES_PASSWORD=segreto postgres:16
docker run -d --name cache --network rete-app redis:7
docker run -d --name api --network rete-app -p 3000:3000 \
  -e DATABASE_URL=postgres://postgres:segreto@db:5432/app mia-api:1.0

Cinque comandi da ricordare nell'ordine giusto, con la password scritta nella cronologia della shell e nessun posto dove sia documentato come sta insieme il tutto. Un collega che clona il progetto non ha modo di saperlo.

Con Compose diventa un file versionato in Git accanto al codice, e l'avvio è docker compose up -d. Il valore vero non è digitare meno: è che la configurazione smette di stare nella tua testa e finisce nel repository.

Anatomia di un compose.yaml

Il file si chiama compose.yaml (o docker-compose.yml, accettato per compatibilità) e sta nella radice del progetto. La struttura è sempre la stessa:

services:      # i container
volumes:       # i dischi persistenti
networks:      # le reti (spesso puoi ometterlo)

Il vecchio campo version: "3.8" in cima non serve più ed è ignorato: se lo vedi in una guida, quella guida è di qualche anno fa.

services

Ogni voce sotto services è un container. Il nome che gli dai è anche il nome con cui gli altri container lo raggiungono in rete.

services:
  web:
    image: nginx:1.27-alpine
    ports:
      - "8080:80"
    volumes:
      - ./sito:/usr/share/nginx/html:ro
    restart: unless-stopped

In alternativa a image, puoi costruire l'immagine dal tuo Dockerfile:

  api:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:3000"

volumes

I volumi dichiarati in alto sono gestiti da Docker e sopravvivono al docker compose down. Quelli scritti come percorso (./sito:/...) sono bind mount, cioè cartelle del tuo computer viste dentro il container: comodissimi in sviluppo, sconsigliati per i dati di un database.

volumes:
  dati-db:

Due righe e basta: il resto lo fa Docker.

networks

Compose crea da solo una rete privata per il progetto e ci mette dentro tutti i servizi. Si raggiungono per nome: dal servizio api, il database è semplicemente db:5432. Non devi dichiarare niente finché non ti serve separare i servizi in reti diverse, per esempio per tenere il database irraggiungibile dal proxy pubblico.

Nota importante sulle porte: se un servizio serve solo agli altri container, non pubblicare la porta con ports. Un ports: "5432:5432" su un database lo espone sull'IP pubblico del VPS, ed è uno dei modi più comuni di farsi bucare un server. Dentro la rete di Compose i container si parlano lo stesso.

depends_on non basta (e qui si perde tempo)

Questa è la trappola classica.

  api:
    depends_on:
      - db

Sembra dire "avvia l'api dopo che il database è pronto". In realtà dice solo "avvia il container del database prima di quello dell'api". Il container di PostgreSQL parte in un decimo di secondo, ma il processo dentro impiega qualche secondo a diventare pronto ad accettare connessioni. Nel frattempo la tua applicazione tenta di connettersi, riceve un rifiuto e muore.

Il sintomo è inconfondibile: funziona quando riavvii solo l'api, si rompe quando avvii tutto da zero. E in CI/CD, dove si parte sempre da zero, si rompe ogni volta.

La soluzione è un healthcheck sul servizio da cui dipendi, più una condizione:

  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 20s

  api:
    depends_on:
      db:
        condition: service_healthy

Adesso api parte davvero solo quando pg_isready risponde positivamente.

Anche così, scrivi l'applicazione perché sopravviva a un database momentaneamente assente, con qualche tentativo di riconnessione. In produzione il database si riavvia per manutenzione, e l'healthcheck all'avvio non ti protegge da quello.

Variabili d'ambiente e file .env

Tre modi, con conseguenze diverse:

  api:
    environment:
      NODE_ENV: production
      DATABASE_URL: postgres://postgres:${DB_PASSWORD}@db:5432/app
    env_file:
      - .env.api
  • environment scrive i valori nel file. Va bene per cose non segrete come NODE_ENV.
  • ${VARIABILE} viene sostituita da Compose leggendo il file .env nella cartella del progetto. È il modo giusto per le password.
  • env_file passa un intero file di variabili al container senza che Compose lo interpreti.

Il file .env:

DB_PASSWORD=una-password-lunga-e-casuale
POSTGRES_DB=app

Il .env va nel .gitignore, sempre. Accanto ci metti un .env.example con le stesse chiavi e valori finti, così chi clona il progetto sa cosa deve valorizzare. Le credenziali finite in un repository pubblico sono una delle cause più banali di incidenti di sicurezza, e la cronologia di Git non le dimentica.

Attenzione a una differenza sottile: .env serve a Compose per sostituire i ${...} nel file YAML; env_file serve al container. Sono due cose diverse e confonderle porta a variabili che risultano vuote senza spiegazione.

Esempio completo: app + database

Un caso realistico e funzionante:

services:
  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: appdb
    volumes:
      - dati-db:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d appdb"]
      interval: 5s
      timeout: 3s
      retries: 10

  api:
    build: .
    restart: unless-stopped
    environment:
      NODE_ENV: production
      DATABASE_URL: postgres://app:${DB_PASSWORD}@db:5432/appdb
    depends_on:
      db:
        condition: service_healthy
    ports:
      - "127.0.0.1:3000:3000"

volumes:
  dati-db:

Due dettagli che valgono l'articolo intero:

  • db non ha ports. È raggiungibile solo dagli altri container. Se ti serve collegarti con un client dal tuo computer, usi un tunnel SSH invece di aprire la porta a internet.
  • 127.0.0.1:3000:3000 pubblica la porta solo sull'interfaccia locale. Da fuori si arriva passando dal reverse proxy, che gestisce HTTPS e domini.

I comandi

docker compose up -d              # avvia tutto in background
docker compose up -d --build      # ricostruisce le immagini prima
docker compose ps                 # stato dei servizi
docker compose logs -f api        # log di un servizio, in tempo reale
docker compose exec api sh        # shell dentro un servizio
docker compose restart api        # riavvia un solo servizio
docker compose down               # ferma e rimuove container e rete
docker compose down -v            # ...e cancella anche i volumi
docker compose config             # mostra il file risolto, variabili incluse
ComandoCosa toccaRischio
stopFerma i container, li lascia esistereNessuno
downRimuove container e rete, tiene i volumiNessuno sui dati
down -vRimuove anche i volumiCancella il database
up -d --buildRicostruisce le immaginiNessuno, solo lento

docker compose config è il comando sottovalutato. Ti mostra il file dopo la sostituzione delle variabili: se una password risulta vuota o un percorso è sbagliato, lo vedi lì in due secondi invece di indagare sui log.

E ricorda che down -v è definitivo: è lo stesso genere di errore descritto nella pulizia dello spazio in comandi Docker essenziali.

Altri errori comuni

Indentazione YAML sbagliata. Il YAML usa spazi, mai tabulazioni, e due spazi di troppo spostano una chiave in un altro blocco. L'errore che ricevi indica spesso una riga diversa da quella davvero colpevole.

Modificare il file e non ricostruire. Cambi il Dockerfile e lanci docker compose up -d: Compose riusa l'immagine già costruita e non vedi il cambiamento. Serve --build.

Usare localhost tra container. Dentro il container, localhost è il container stesso. Il database si raggiunge con il nome del servizio, db, non con localhost.

Credere che Compose sia un orchestratore. Compose gestisce container su una sola macchina. Se ti servono più nodi, riavvii senza interruzione e scalabilità automatica, il territorio è quello di Kubernetes — che però è molto più complesso e la maggior parte dei progetti non ne ha bisogno.

Quando non ti serve

Onestà: se hai un solo container e nessun database di contorno, Compose aggiunge un file e non toglie nulla dal lavoro. Un docker run con --restart unless-stopped gestito da systemd fa la stessa cosa.

Il momento in cui inizia a convenire è quando i container diventano due e devono parlarsi, oppure quando qualcun altro deve poter avviare il progetto senza chiederti istruzioni.

In sintesi

Docker Compose descrive un'applicazione a più container in un file solo, versionato accanto al codice, e la avvia con docker compose up -d.

I servizi si raggiungono per nome sulla rete privata che Compose crea da sé: pubblica con ports solo ciò che deve essere raggiungibile da fuori, mai il database.

depends_on non aspetta che un servizio sia pronto, aspetta solo che sia partito: senza healthcheck più condition: service_healthy il primo avvio fallisce in modo intermittente.

I segreti stanno nel .env escluso da Git, richiamati con ${...} nel compose, e docker compose config ti dice subito se sono arrivati.

Per i comandi Docker di base che stanno sotto a tutto questo, vedi comandi Docker essenziali; per mettere il tutto online su una macchina tua, configurare un VPS da zero.