Linux31 agosto 2026 26 visualizzazioni

Docker Compose su un VPS Linux: distribuire e gestire uno stack in produzione

Docker Compose su un VPS Linux: distribuire e gestire uno stack in produzione

Docker Compose non sostituisce né una strategia di backup, né una politica di aggiornamento, né l'hardening del VPS. Fornisce però un modello dichiarativo che rende uno stack riproducibile: servizi, immagini, reti, volumi, porte e politiche di riavvio sono descritti in un file che si può rileggere, testare e versionare.

Questa guida non ripete l'installazione di Docker. Il motore e il plugin Compose devono essere già operativi secondo la guida Installare Docker su un VPS Linux. Anche la trappola Docker/UFW è trattata lì. Qui l'obiettivo è gestire in modo duraturo un'applicazione e il suo database su un VPS.

Il caso pratico utilizza le immagini ufficiali WordPress e MariaDB. I principi si applicano poi a Bitwarden, Pi-hole, Jellyfin, Portainer e alla maggior parte degli stack composti da un'applicazione e da un servizio dati.

Le tre convenzioni da applicare oggi

1. Non aggiungere la chiave version:

Un file moderno comincia direttamente con services:. La proprietà di primo livello version è mantenuta solo per compatibilità. Docker la definisce obsoleta, precisa che è puramente informativa e indica che il suo uso produce un avviso. Compose convalida comunque il file con la specifica più recente. Aggiungere version: "3.8" non seleziona quindi alcun motore di compatibilità particolare.

Fonte: docs.docker.com, proprietà version

2. Usare docker compose, in due parole

I comandi di questa guida utilizzano il plugin integrato nella CLI di Docker:

docker compose up -d

Il comando docker-compose, con il trattino, indica il vecchio strumento v1 scritto in Python. Compose v2, annunciata nel 2020, è scritta in Go e si invoca con docker compose. Compose v5, pubblicata nel 2025, è funzionalmente identica alla v2 sul lato CLI: la sua novità principale è un SDK Go ufficiale, e la numerazione è passata direttamente a 5 per evitare confusione con i vecchi formati di file etichettati «v2» e «v3». In ogni caso, la forma da usare resta docker compose.

Fonte: docs.docker.com, storia di Docker Compose

3. Chiamare il file compose.yaml

Il nome canonico è compose.yaml. Le varianti compose.yml, docker-compose.yaml e docker-compose.yml restano riconosciute per compatibilità, ma Docker raccomanda il nome canonico e lo privilegia quando più varianti si trovano nella stessa cartella.

Fonte: docs.docker.com, modello di applicazione Compose

L'architettura adottata

Lo stack rispetta quattro confini semplici:

  • il servizio app è l'unico a pubblicare una porta sull'host;
  • questa porta ascolta solo su 127.0.0.1, affinché un reverse proxy installato sul VPS sia l'unico punto d'ingresso pubblico;
  • il servizio db non pubblica alcuna porta e resta raggiungibile solo tramite il suo nome DNS db sulla rete privata backend;
  • i dati sopravvivono alla ricreazione dei contenitori grazie a due volumi con nome.

Compose crea un DNS interno per i servizi della stessa rete. L'applicazione si connette quindi a db:3306, non a un indirizzo IP di contenitore. Un IP di contenitore è effimero e non deve mai essere fissato nella configurazione.

Preparare una cartella di esercizio

Uno stack deve avere una posizione nota, permessi restrittivi e una struttura che si possa salvare.

sudo install -d -m 0750 -o "$USER" -g "$USER" /opt/stacks/wordpress-prod
cd /opt/stacks/wordpress-prod

umask 077
mkdir -p secrets backups
openssl rand -base64 48 > secrets/db_password.txt
openssl rand -base64 48 > secrets/db_root_password.txt
chmod 600 secrets/*.txt
chmod 700 secrets backups

L'account che avvia Compose deve poter leggere entrambi i file. Restano in chiaro sul disco del VPS: i segreti locali di Compose sono forniti al contenitore come file montati in /run/secrets, ma la loro origine resta un file locale da proteggere. Questo meccanismo limita la loro esposizione ai soli servizi autorizzati ed evita di collocarli direttamente nelle variabili d'ambiente. Non sostituisce un gestore di segreti centralizzato.

Fonte: docs.docker.com, gestione dei segreti con Compose

Il file compose.yaml, commentato riga per riga

Il file comincia deliberatamente con services:. Non contiene alcuna chiave version:.

services:
  # Servizio HTTP dell'applicazione.
  app:
    # Il riferimento all'immagine viene da .env e va convalidato prima della produzione.
    image: "${WORDPRESS_IMAGE:?Impostare WORDPRESS_IMAGE in .env}"
    # Riparte al riavvio, salvo che un amministratore abbia fermato il servizio di proposito.
    restart: unless-stopped
    # Evita processi zombie se l'immagine non li raccoglie da sola.
    init: true
    # Il database deve risultare sano prima della creazione dell'applicazione.
    depends_on:
      db:
        condition: service_healthy
    # La porta 80 del contenitore ascolta solo sul loopback del VPS.
    ports:
      - "${APP_BIND_IP:-127.0.0.1}:${APP_PORT:-8080}:80"
    # I valori non sensibili possono essere interpolati da .env.
    environment:
      WORDPRESS_DB_HOST: "db:3306"
      WORDPRESS_DB_NAME: "${DB_NAME:?Impostare DB_NAME in .env}"
      WORDPRESS_DB_USER: "${DB_USER:?Impostare DB_USER in .env}"
      # L'immagine ufficiale sa leggere la password da un file.
      WORDPRESS_DB_PASSWORD_FILE: /run/secrets/db_password
    # Il contenuto applicativo persiste oltre il ciclo di vita del contenitore.
    volumes:
      - wordpress_data:/var/www/html
    # All'applicazione viene concessa solo la password che le serve.
    secrets:
      - db_password
    # L'applicazione riceve il traffico frontale e dialoga con il database in privato.
    networks:
      - frontend
      - backend
    # Un limite locale evita che i log riempiano il disco del VPS.
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
    # Lascia al server HTTP il tempo di chiudere le connessioni correttamente.
    stop_grace_period: 30s

  # Servizio di database, senza porta pubblicata sull'host.
  db:
    # Anche questo riferimento all'immagine è gestito in .env.
    image: "${MARIADB_IMAGE:?Impostare MARIADB_IMAGE in .env}"
    # Un arresto manuale resta rispettato dopo il riavvio del demone o del VPS.
    restart: unless-stopped
    # Inizializza database e utente solo al primissimo avvio.
    environment:
      MARIADB_DATABASE: "${DB_NAME:?Impostare DB_NAME in .env}"
      MARIADB_USER: "${DB_USER:?Impostare DB_USER in .env}"
      MARIADB_PASSWORD_FILE: /run/secrets/db_password
      MARIADB_ROOT_PASSWORD_FILE: /run/secrets/db_root_password
    # I file di dati di MariaDB risiedono in un volume con nome.
    volumes:
      - mariadb_data:/var/lib/mysql
    # A differenza dell'applicazione, il database riceve entrambi i segreti.
    secrets:
      - db_password
      - db_root_password
    # Questo script è fornito dall'immagine ufficiale MariaDB.
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 10s
      timeout: 5s
      retries: 10
      start_period: 30s
    # Il database appartiene solo alla rete privata interna.
    networks:
      - backend
    # Stessa politica di rotazione locale dell'applicazione.
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
    # Un database può aver bisogno di tempo per completare le scritture.
    stop_grace_period: 1m

# Dichiarazione degli archivi persistenti gestiti da Docker.
volumes:
  wordpress_data:
  mariadb_data:

# Dichiarazione delle due zone di rete dello stack.
networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    # Impedisce a questa rete di fornire connettività esterna diretta.
    internal: true

# Ogni segreto locale proviene da un file distinto sull'host.
secrets:
  db_password:
    file: ./secrets/db_password.txt
  db_root_password:
    file: ./secrets/db_root_password.txt

L'immagine ufficiale WordPress supporta la convenzione WORDPRESS_DB_PASSWORD_FILE. L'immagine ufficiale MariaDB accetta le varianti MARIADB_*_FILE e include healthcheck.sh. Questi comportamenti appartengono alle immagini, non a Compose. Occorre quindi verificare la documentazione di ogni immagine prima di trasporre questo modello a un'altra applicazione.

Fonti: immagine ufficiale WordPress, immagine ufficiale MariaDB, riferimento MariaDB su healthcheck.sh

depends_on con condition: service_healthy impedisce a Compose di creare l'applicazione prima che il controllo di salute del database riesca. Questo migliora l'avvio iniziale, ma non esonera l'applicazione dal saper ritentare una connessione persa durante l'esercizio. Vedere l'ordine di avvio in Compose.

Il file .env: configurazione, non cassaforte

Compose legge automaticamente un file .env collocato accanto a compose.yaml e lo usa per interpolare le espressioni ${VARIABILE}.

# Nome stabile del progetto. Influenza tra l'altro il nome reale di volumi e reti.
COMPOSE_PROJECT_NAME=wordpress-prod

# Il servizio resta locale al VPS e sarà pubblicato da un reverse proxy.
APP_BIND_IP=127.0.0.1
APP_PORT=8080

# Valori non sensibili trasmessi a entrambi i servizi.
DB_NAME=wordpress
DB_USER=wordpress

# Selettori leggibili per preparare il primo pull.
# Prima della produzione, sostituiteli con i digest convalidati come spiegato sotto.
WORDPRESS_IMAGE=wordpress:apache
MARIADB_IMAGE=mariadb:lts

Fonte: docs.docker.com, interpolazione delle variabili in Compose

I tag delle immagini sono mutabili: un editore può far puntare lo stesso tag a un contenuto diverso. Per un deployment rigorosamente riproducibile, convalidate l'immagine e fissate poi il suo riferimento con un digest nome@sha256:.... Un aggiornamento del digest diventa così una modifica deliberata e verificabile. Vedere le buone pratiche Docker sul blocco tramite digest.

Per ottenere i riferimenti immutabili corrispondenti alle immagini appena testate:

docker pull wordpress:apache
docker pull mariadb:lts

docker image inspect wordpress:apache --format '{{index .RepoDigests 0}}'
docker image inspect mariadb:lts --format '{{index .RepoDigests 0}}'

Riportate ogni risultato completo nella variabile corrispondente di .env. Dopo questo blocco, docker compose pull non cambierà silenziosamente contenuto: l'aggiornamento passerà da una modifica esplicita del digest.

Il digest blocca l'immagine, non lo stato del volume. È particolarmente importante in questo esempio: l'immagine ufficiale WordPress indica che gli aggiornamenti automatici di WordPress possono modificare il contenuto di /var/www/html dopo il deployment. Una politica di aggiornamento riproducibile deve quindi coprire sia le immagini sia il meccanismo di aggiornamento proprio dell'applicazione.

Anche se l'esempio non mette alcuna password in .env, questo file non deve essere committato. In molti progetti finisce per ricevere un token, un URL privato o un valore specifico della produzione. Un segreto scritto direttamente in compose.yaml o in .env, e poi inviato a Git, resta nella cronologia anche dopo essere stato rimosso nell'ultimo commit.

Altra trappola importante: le variabili MARIADB_DATABASE, MARIADB_USER e MARIADB_*_PASSWORD_FILE servono a inizializzare una cartella di dati vuota. L'immagine ufficiale precisa che non riconfigurano un database già esistente. Sostituire il contenuto di un file di segreto non cambia quindi automaticamente la password registrata in MariaDB. Una rotazione deve modificare l'account nel database, aggiornare il segreto usato dall'applicazione e poi riavviare o ricaricare i componenti secondo la loro documentazione.

Create un file .gitignore:

.env
secrets/
backups/

Potete versionare un .env.example contenente solo nomi di variabili e valori fittizi. Proteggete il file reale:

chmod 600 .env

Attenzione anche a docker compose config senza opzioni: questo comando mostra la configurazione risolta e può rivelare valori interpolati se avete messo segreti nelle variabili. docker compose config --quiet convalida senza stampare nulla. Vedere il riferimento di docker compose config.

Volumi con nome e bind mount: da non confondere

Un contenitore è sostituibile. Ogni dato scritto solo nel suo strato interno scompare con esso. La persistenza va dichiarata esplicitamente.

CriterioVolume con nomeBind mount
OrigineOggetto gestito da DockerPercorso esplicito del VPS, per esempio /srv/app/config
Caso consigliatoDati generati dall'applicazione o dal databaseFile di configurazione amministrato dall'host, certificato, contenuti che l'host deve manipolare direttamente
PortabilitàPoco legato all'albero delle cartelle del VPSDipende dal percorso, dai permessi e talvolta dal contesto SELinux dell'host
Rischio principaleDimenticare che esiste al di fuori della cartella dello stackMontare il percorso sbagliato, nascondere contenuti già presenti nell'immagine o concedere un accesso in scrittura troppo ampio
BackupVa esportato esplicitamenteVa incluso esplicitamente nel backup del percorso host

Docker raccomanda i volumi per i dati persistenti prodotti dai contenitori. Un volume sopravvive alla cancellazione del contenitore che lo usava. Un bind mount è preferibile quando un amministratore o uno strumento dell'host deve modificare direttamente un file.

In questo stack:

  • wordpress_data conserva /var/www/html;
  • mariadb_data conserva /var/lib/mysql;
  • ./secrets/*.txt sono file dell'host montati separatamente nei servizi autorizzati.

Dove risiede davvero un volume con nome?

Il nome mariadb_data è il nome logico del modello Compose. Con COMPOSE_PROJECT_NAME=wordpress-prod, Docker crea di norma un nome come wordpress-prod_mariadb_data. Non costruite i vostri script su questa supposizione. Compose applica etichette ai volumi, e docker volume inspect restituisce il loro punto di mount reale:

docker volume ls \
  --filter label=com.docker.compose.project=wordpress-prod

DB_VOLUME="$(docker volume ls \
  --filter label=com.docker.compose.project=wordpress-prod \
  --filter label=com.docker.compose.volume=mariadb_data \
  --format '{{.Name}}')"

docker volume inspect "$DB_VOLUME" --format '{{.Mountpoint}}'

Con il driver locale e un demone Docker root classico, il percorso si trova spesso sotto /var/lib/docker/volumes/. Non è una garanzia: la modalità rootless, un data-root personalizzato o un driver remoto cambiano questa posizione. Fa fede la risposta di docker volume inspect. Non modificate direttamente i file interni di un database in questa cartella.

I comandi che distruggono i dati

docker compose down rimuove i contenitori e le reti dello stack, ma conserva i volumi con nome per impostazione predefinita. L'opzione -v ne richiede anche la cancellazione.

Non usate mai docker compose down -v come comando di aggiornamento o di risoluzione dei problemi. Verificate di avere un backup ripristinabile prima di cancellare deliberatamente un volume.

Allo stesso modo, docker volume prune elimina i volumi considerati inutilizzati. Un volume di produzione diventa «inutilizzato» non appena il suo contenitore viene eliminato, anche se i suoi dati restano indispensabili. Vedere il comportamento di docker compose down.

Pubblicazione delle porte: la differenza tra locale e pubblico

Queste due righe non sono equivalenti:

ports:
  - "8080:80"

Senza indirizzo host, Docker pubblica la porta su tutti gli indirizzi del VPS, in pratica 0.0.0.0 e, a seconda della configurazione, IPv6. Il servizio può allora diventare raggiungibile da internet se instradamento e regole di rete lo consentono.

ports:
  - "127.0.0.1:8080:80"

Qui la porta ascolta solo sul loopback IPv4. È la scelta giusta quando Nginx o Apache gira direttamente sul VPS e inoltra le richieste a http://127.0.0.1:8080. Docker documenta esplicitamente che l'assenza di indirizzo pubblica su tutti gli indirizzi e che un binding a 127.0.0.1 limita l'accesso all'host.

Fonte: docs.docker.com, pubblicazione delle porte

Verificate il risultato, non limitatevi a rileggere lo YAML:

docker compose ps
ss -lntp | grep ':8080'
curl --fail --head http://127.0.0.1:8080

Il database non ha una sezione ports. La direttiva expose non è necessaria perché app raggiunga db sulla stessa rete Compose.

Docker gestisce le proprie regole di firewall e una porta pubblicata può aggirare il percorso di filtraggio atteso con UFW. Questo punto è dettagliato in Proteggere un VPS Linux: la checklist completa e nella guida di installazione Docker citata come prerequisito. Le tre regole coerenti sono: non pubblicare ciò che non deve esserlo, fare il binding a 127.0.0.1 dietro un proxy locale, poi verificare che cosa ascolta davvero.

Se il reverse proxy è a sua volta un contenitore, 127.0.0.1 indica quel contenitore, non l'host né l'applicazione. Collegate allora il proxy e l'applicazione a una rete Docker condivisa, senza pubblicare la porta applicativa su internet. Per un proxy installato sull'host, consultate Ospitare più siti su un VPS con Nginx e poi Certbot: installare un SSL Let's Encrypt su VPS.

always o unless-stopped al riavvio del VPS?

Entrambe le politiche riavviano un contenitore dopo un guasto e al ritorno del demone Docker. La differenza emerge dopo un arresto amministrativo:

  • always riavvia un contenitore fermato manualmente al riavvio del demone Docker;
  • unless-stopped rispetta questo arresto manuale, anche dopo il riavvio del demone o del VPS.

Per uno stack amministrato manualmente, unless-stopped evita di rimettere online al prossimo riavvio un servizio che era stato fermato deliberatamente per manutenzione. always è adatto quando un servizio deve tornare sistematicamente e questo comportamento è espressamente desiderato. La politica non sostituisce un controllo di salute: reagisce all'arresto del processo principale, non a un'applicazione viva ma bloccata.

Fonte: docs.docker.com, politiche di riavvio

Convalidare e avviare lo stack

Cominciate verificando la CLI:

docker compose version
docker info

Poi convalidate e distribuite dalla cartella dello stack:

cd /opt/stacks/wordpress-prod

docker compose config --quiet
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=100 --timestamps

docker compose pull scarica le immagini ma non sostituisce i contenitori in esecuzione. docker compose up -d confronta poi lo stato desiderato con quello esistente, crea ciò che manca e ricrea i servizi la cui immagine o configurazione è cambiata. I volumi montati sono conservati durante questa ricreazione. Vedere il comportamento di docker compose up.

Dopo il primo avvio, controllate anche l'esposizione da un'altra macchina. Un test locale riuscito non prova che la porta 8080 sia irraggiungibile pubblicamente.

Aggiornare senza perdere i dati

Un aggiornamento in produzione è una piccola procedura di cambiamento, non un comando lanciato alla cieca.

Prima dell'aggiornamento

  1. Leggete le note di rilascio dell'applicazione e del database, in particolare le migrazioni e i percorsi di aggiornamento supportati.
  2. Verificate lo spazio su disco e gli inode con df -h e df -i.
  3. Effettuate un backup e testatene regolarmente il ripristino.
  4. Annotate i riferimenti delle immagini attualmente distribuite con docker compose images.
  5. Modificate in .env i tag precisi o i digest solo dopo la convalida.
  6. Eseguite docker compose config --quiet.

Applicare l'aggiornamento

cd /opt/stacks/wordpress-prod

docker compose pull
docker compose up -d
docker compose ps
docker compose logs --since=10m --timestamps

Ciò che può essere ricreato:

  • il contenitore app se cambia il suo riferimento di immagine o la sua configurazione;
  • il contenitore db se cambia il suo riferimento di immagine o la sua configurazione;
  • le reti se cambia la loro definizione.

Ciò che viene conservato:

  • il contenuto dei volumi con nome wordpress_data e mariadb_data;
  • i file del VPS usati come bind mount o come origine dei segreti;
  • le immagini precedenti finché non sono state eliminate.

docker compose restart non basta dopo una modifica di compose.yaml o delle variabili: questo comando riavvia i contenitori esistenti senza applicare la nuova configurazione. Usate docker compose up -d. Vedere il riferimento di docker compose restart.

Una ricreazione può provocare una breve interruzione. Compose su un VPS singolo non promette un aggiornamento senza fermo. Per un'applicazione critica, prevedete una strategia adatta all'applicazione, una seconda istanza o una piattaforma di orchestrazione.

Tornare indietro

Per un rollback applicativo, riportate in .env il riferimento di immagine precedente, poi rilanciate pull e up -d. Questo metodo non annulla una migrazione di schema. Se la nuova versione ha modificato il database in modo incompatibile, solo un piano di ritorno documentato dall'editore o un ripristino convalidato permettono di tornare indietro correttamente. È esattamente per questo che il backup precede l'aggiornamento.

Salvare lo stack, non solo il suo YAML

Copiare compose.yaml non salva alcun dato contenuto nei volumi. Al contrario, copiare a caldo i file interni di MariaDB non garantisce un database coerente. Per questo caso si combinano:

  • un dump logico di MariaDB, prodotto dallo strumento del database;
  • un archivio del volume applicativo mentre l'applicazione è ferma;
  • una copia di compose.yaml, .env e dell'elenco dei riferimenti di immagine;
  • un backup separato e cifrato dei file di segreti;
  • una copia fuori dal VPS, con conservazione e test di ripristino.

MariaDB documenta mariadb-dump nella sua guida ufficiale al backup dei contenitori.

Esempio di backup coerente

Eseguite questo blocco con Bash dalla cartella dello stack. L'applicazione è fermata per impedire scritture durante il dump e l'archiviazione. Il database resta attivo per la durata del dump logico. Il trap riavvia l'applicazione anche se un comando fallisce.

set -Eeuo pipefail

cd /opt/stacks/wordpress-prod
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
DEST="backups/$STAMP"
install -d -m 0700 "$DEST"

docker compose config --quiet
docker compose stop app
trap 'docker compose start app' EXIT

docker compose exec -T db sh -c '
  exec mariadb-dump \
    --user=root \
    --password="$(cat /run/secrets/db_root_password)" \
    --single-transaction \
    --routines \
    --triggers \
    --events \
    "$MARIADB_DATABASE"
' | gzip -9 > "$DEST/database.sql.gz"

docker compose run --rm --no-deps -T \
  --entrypoint tar app \
  -C /var/www/html -czf - . \
  > "$DEST/wordpress-data.tar.gz"

cp compose.yaml .env "$DEST/"
docker compose images > "$DEST/images.txt"

gzip -t "$DEST/database.sql.gz"
tar -tzf "$DEST/wordpress-data.tar.gz" > /dev/null
(cd "$DEST" && sha256sum database.sql.gz wordpress-data.tar.gz compose.yaml .env images.txt > SHA256SUMS)

docker compose start app
trap - EXIT

Questo script non copia deliberatamente secrets/ nello stesso archivio. Esportate questi file verso una cassaforte o un backup cifrato con un controllo di accesso distinto. Un backup presente solo sullo stesso VPS scompare insieme al disco, all'account o all'incidente che distrugge la produzione.

Il dump con --single-transaction è adatto alle tabelle transazionali. Un'applicazione che usa altri motori o più sistemi di archiviazione richiede una procedura di coerenza propria dell'editore. Per grandi volumi e obiettivi di ripristino stringenti, valutate anche gli strumenti di backup fisico, la replica e gli snapshot coordinati.

Se preferite non scrivere né sorvegliare da soli questa catena, l'opzione di backup automatici disponibile con i nostri VPS Linux realizza una copia quotidiana in un datacenter distinto, con una cronologia rotativa e un ripristino con un clic. Non esonera dal testare un ripristino applicativo, ma copre il caso in cui il VPS stesso venga perso.

Testare un ripristino

Un file il cui ripristino non è mai stato testato è solo un'ipotesi di backup. Il test va eseguito in un progetto isolato, con volumi vuoti e gli stessi riferimenti di immagine del backup:

  1. copiate compose.yaml, .env e i segreti di test su un altro VPS o in una cartella isolata;
  2. cambiate COMPOSE_PROJECT_NAME e la porta host per non toccare la produzione;
  3. create il volume applicativo lanciando un comando estemporaneo, poi estraete l'archivio;
  4. avviate solo db, attendete lo stato healthy, poi importate il dump nel database vuoto;
  5. avviate app, verificate le funzioni di business e annotate il tempo reale di ripristino.

Esempio di importazione in un database di test già inizializzato e vuoto:

gzip -dc backups/DATE/database.sql.gz | \
  docker compose exec -T db sh -c '
    exec mariadb \
      --user=root \
      --password="$(cat /run/secrets/db_root_password)" \
      "$MARIADB_DATABASE"
  '

Non eseguite questa importazione sul database di produzione esistente. Una procedura di ripristino completa deve precisare come ottenere un database vuoto, quale tempo di fermo è accettato e come tornare allo stato precedente se la convalida fallisce.

Leggere i log e diagnosticare un ciclo di riavvio

Il primo errore è spesso lanciare subito down. Questo elimina i contenitori e fa perdere una parte delle informazioni di stato utili. Cominciate osservando.

docker compose ps --all
docker compose logs --tail=200 --timestamps app
docker compose logs --tail=200 --timestamps db
docker compose logs --follow --since=10m app

Il comando logs permette di filtrare per servizio, seguire il flusso e limitare la cronologia con --tail o --since. Vedere il riferimento di docker compose logs.

Ispezionate poi lo stato esatto del contenitore:

CID="$(docker compose ps -q app)"

docker inspect "$CID" --format \
  'status={{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} restarts={{.RestartCount}} error={{.State.Error}}'

docker compose top
docker stats --no-stream
df -h
df -i
free -h
journalctl -u docker --since '30 minutes ago'

Le cause frequenti sono:

  • segreto assente, illeggibile o montato con un nome sbagliato;
  • database ancora non disponibile, credenziali incoerenti o migrazione fallita;
  • porta host già occupata;
  • volume montato nel posto sbagliato o permessi incompatibili con l'utente del contenitore;
  • processo terminato per mancanza di memoria, visibile con OOMKilled=true;
  • disco o inode saturi;
  • immagine incompatibile con l'architettura del VPS;
  • configurazione interpolata diversa da quella attesa.

Convalidate il modello senza mostrare i valori, poi consultate le variabili attese:

docker compose config --quiet
docker compose config --variables

Se il servizio si riavvia troppo in fretta per permettere exec, lanciate un contenitore estemporaneo senza le sue dipendenze e con una shell, a condizione che l'immagine ne fornisca una:

docker compose run --rm --no-deps --entrypoint sh app

Questo contenitore estemporaneo monta gli stessi volumi e segreti dichiarati per il servizio. Evitate qualsiasi modifica finché la causa non è compresa.

La rotazione json-file dichiarata nel file limita lo spazio consumato dai log locali. Docker precisa che max-size vale -1 per impostazione predefinita, quindi senza limite. Per una produzione importante, inviate i log anche a un sistema esterno con una conservazione adeguata. Vedere il driver di log json-file.

Controlli di esercizio da conservare

A ogni modifica

docker compose config --quiet
docker compose up -d
docker compose ps
docker compose logs --since=10m --timestamps

Prima di ogni aggiornamento

  • note di rilascio e percorso di migrazione letti;
  • spazio su disco controllato;
  • backup creato, esportato fuori dal VPS e ripristino già testato;
  • immagini attuali annotate;
  • finestra di interruzione annunciata se necessario.

Dopo ogni riavvio del VPS

docker compose ps
docker compose logs --since=30m --timestamps
ss -lntp

Verificate che l'applicazione sia sana, che il database non sia pubblicato, che la porta applicativa resti legata a 127.0.0.1 e che il reverse proxy serva il dominio in HTTPS.

Errori frequenti da evitare

  • iniziare il file con version: "3.8";
  • usare il binario storico docker-compose;
  • chiamare il file docker-compose.yml in un nuovo progetto;
  • scrivere 8080:80 pensando che il servizio resti locale;
  • pubblicare 3306:3306 quando solo l'applicazione deve raggiungere il database;
  • memorizzare le password in compose.yaml o nel repository Git;
  • usare un bind mount per un database senza padroneggiare proprietario, permessi, backup e contesto di sicurezza;
  • credere che eliminare un contenitore elimini o salvi il suo volume;
  • lanciare docker compose down -v per «ripartire puliti»;
  • eseguire docker compose restart sperando di applicare una nuova configurazione;
  • aggiornare un database senza leggere il suo percorso di migrazione né disporre di un ritorno indietro;
  • salvare unicamente sul disco del VPS;
  • confondere un contenitore running con un'applicazione realmente sana.

Domande frequenti

Bisogna ancora scrivere version in un file Compose?

No. La proprietà di primo livello version è documentata come obsoleta: è puramente informativa e produce un avviso. Compose interpreta il file con la specifica attuale, qualunque sia il valore scritto.

Che differenza c'è tra docker compose e docker-compose?

docker-compose con il trattino è lo strumento v1 in Python. docker compose in due parole è il plugin in Go, in v2 dal 2020 e in v5 dal 2025. Oggi va usata solo questa seconda forma.

docker compose down cancella i miei dati?

Non per impostazione predefinita: i volumi con nome vengono conservati. È l'opzione -v a cancellarli, così come docker volume prune su un volume di cui non esiste più alcun contenitore.

Come aggiornare uno stack senza perdere il database?

Fate un backup, cambiate il riferimento di immagine in .env, poi lanciate docker compose pull seguito da docker compose up -d. I contenitori vengono ricreati, i volumi con nome sono conservati. docker compose restart non basta: non rilegge la configurazione.

Quale configurazione VPS per uno stack Compose?

Un'applicazione web e il suo database stanno su 2 vCPU e 4 GB di RAM. Prevedete più memoria non appena aggiungete una cache, un motore di ricerca o più stack sulla stessa macchina, e sorvegliate soprattutto lo spazio su disco: immagini, volumi e log si accumulano più in fretta del previsto.

Conclusione

Uno stack Compose affidabile poggia meno sulla quantità di YAML che su alcuni invarianti: un file compose.yaml conforme alla specifica attuale, immagini scelte e aggiornate deliberatamente, volumi identificati, porte pubblicate solo dove strettamente necessario, segreti separati dal repository, un backup ripristinabile e una diagnosi fondata sullo stato reale dei contenitori.

Il ciclo normale diventa allora prevedibile: convalidare, salvare, scaricare le immagini, applicare con docker compose up -d, controllare la salute, poi conservare la possibilità di tornare indietro. È questa disciplina che trasforma un esempio Compose in una base di esercizio per gli stack successivi.

Articoli correlati