Database in Docker o sull’host? Cosa conviene
PostgreSQL, MySQL, MongoDB e Redis in Docker funzionano anche in produzione: verifica volumi, upgrade, backup e limiti di memoria per evitare problemi operativi.
Il database deve essere eseguito in Docker o sull’host?
Esegui il database in Docker. Per uno stack applicativo su un singolo VPS, PostgreSQL, MySQL, MongoDB o Redis eseguito in un container è una scelta normale anche in produzione. Il dibattito su questo tema parte spesso dalla premessa sbagliata. Un container è un processo Linux isolato tramite namespace e cgroups, non una macchina virtuale. Tra il database e il disco non c’è quindi un hypervisor. Con un bind mount o un volume locale denominato, le operazioni di lettura e scrittura usano il filesystem dell’host, lo stesso che verrebbe usato da un’installazione tramite pacchetto.
Il vero costo è operativo. Quattro aspetti determinano se questa configurazione funzionerà correttamente o causerà problemi difficili da gestire: dove risiedono i dati, chi è il proprietario della directory, come viene eseguito l’aggiornamento a una versione principale e se hai mai ripristinato un backup. Se questi aspetti sono gestiti correttamente, il container è solo un dettaglio. Se vengono trascurati, sarà il container a cui attribuirai la colpa.
La decisione è la stessa per ogni database server. Gli esempi seguenti usano PostgreSQL, MySQL, MongoDB e Redis; le differenze specifiche dei singoli prodotti vengono indicate nei punti in cui sono rilevanti.
Cosa modifica realmente un container
Non il percorso di archiviazione, se si monta un volume. Il kernel, la page cache e il filesystem restano gli stessi.
Esiste un solo problema concreto di prestazioni, ed è il caso in cui non si monta nulla. Senza un volume, la directory dei dati viene scritta nel writable layer del container, cioè in un overlay filesystem sovrapposto all'immagine. Le scritture sono più lente e l'intero layer viene eliminato quando si rimuove il container. È questa la causa di «questa mattina il mio database era vuoto».
Cosa cambia realmente:
- Il ciclo di vita.
docker compose downelimina il container. Tutto ciò che non si trovava in un volume viene eliminato con esso. - La versione. Il tag dell'immagine identifica la versione. In un container del database non esiste alcun
apt upgradeche resti disponibile dopo ildocker compose pullsuccessivo. - La contabilizzazione della memoria. Un limite cgroup è una soglia rigida applicata dal kernel e il database non sa che esiste.
- L'utente. Il processo viene eseguito con un user id numerico all'interno del container, che potrebbe non essere proprietario di alcun file sull'host.
Dove risiedono i dati determina tutto
Esistono due opzioni valide e un errore comune.
- Un volume denominato:
pgdata:/var/lib/postgresql/data. Docker crea la directory in/var/lib/docker/volumes/<project>_pgdata/_datae l'entrypoint dell'immagine imposta la proprietà al primo avvio. Questa è l'opzione predefinita. - Un bind mount:
/srv/appname/pg:/var/lib/postgresql/data. Scegli tu il percorso, quindi la gestione dei permessi è a tuo carico. - Nessun mount. Vedi sopra. I dati si trovano nel container.
L'analisi completa dei compromessi richiede una trattazione separata, descritta in bind mount e volumi denominati. Per un database, la regola sintetica è: usa un volume denominato, salvo che tu abbia un motivo specifico per conoscere il percorso sull'host. Se usi un bind mount, collocarlo in un percorso stabile come /srv/appname/pg invece che nella directory del progetto, dove un git clean potrebbe raggiungerlo.
Esiste un limite importante: non collocare la directory dei dati di un database su NFS (network file system) o su un mount di rete il cui locking e comportamento fsync non siano stati verificati. I database presuppongono che un fsync completato con successo significhi che i byte siano stati scritti su uno storage stabile. Quando questa condizione non è soddisfatta, si verificano corruzioni che possono manifestarsi dopo settimane.
Fissa il nome del volume prima che scompaia
Compose assegna un nome al volume <project>_<volume> e il nome del progetto corrisponde per impostazione predefinita al nome della directory. L'identità del volume dipende quindi dal nome di una directory, che può essere modificato facilmente senza accorgersene.
Sposta /srv/app in /srv/app-old oppure rinomina la chiave pgdata nel file Compose. Il comando docker compose up -d successivo crea un volume nuovo e vuoto. Postgres inizializza al suo interno un cluster nuovo. Il container è in stato healthy, l'applicazione si avvia e tutte le tabelle sono scomparse. Il volume precedente è ancora presente sul disco con il vecchio nome, e questa è la buona notizia.
docker volume ls
docker volume inspect app_pgdataFissa i nomi per impedire che questo accada. Imposta esplicitamente il nome del progetto e quello del volume:
name: myapp
services:
db:
image: postgres:17
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
name: myapp_pgdataSe un volume non previsto contiene già i dati, copiali mentre il database è arrestato:
docker compose stop db
docker run --rm -v app_pgdata:/from -v myapp_pgdata:/to alpine sh -c 'cp -a /from/. /to/'
docker compose start dbSe lo copi mentre il database è in esecuzione, ottieni una copia incoerente dei file che erano in fase di scrittura. Arrestalo prima.
Chi è il proprietario della directory dei dati
Le immagini ufficiali di Postgres, MySQL e MongoDB eseguono il server con un user id non privilegiato, in genere 999. Quando il container viene avviato come root, l'entrypoint modifica il proprietario della directory dei dati assegnandola a quell'utente, quindi riduce i privilegi. Per questo motivo, un bind mount vuoto di solito funziona al primo avvio.
Il problema si verifica non appena imposti user: nel file Compose, perché a quel punto l'entrypoint non dispone più dei privilegi necessari per correggere la situazione. Postgres lo indica esplicitamente:
initdb: error: could not change permissions of directory "/var/lib/postgresql/data": Operation not permittedUna directory dei dati esistente con permessi errati produce un messaggio diverso. È importante riconoscerlo, perché la correzione è chmod, non chown:
FATAL: data directory "/var/lib/postgresql/data" has invalid permissions
DETAIL: Permissions should be u=rwx (0700) or u=rwx,g=rx (0750).Con un bind mount di proprietà di root, MongoDB non riesce a creare il file di lock:
Unable to create/open the lock file: /data/db/mongod.lock (Permission denied). Ensure the user executing mongod is the owner of the lock file and has the appropriate permissions.La correzione consiste nell'eseguire chown sulla directory dell'host usando l'id numerico, non un nome:
sudo chown -R 999:999 /srv/appname/pg
sudo chmod 700 /srv/appname/pg
ls -ldn /srv/appname/pgls -ldn visualizza i numeri invece dei nomi e dovrebbe mostrare 999 999. L'account chiamato postgres sull'host e quello chiamato postgres all'interno dell'immagine non sono correlati: il kernel confronta i numeri e i nomi vengono risolti separatamente sui due lati. come PUID e PGID associano gli utenti dell'host a un container descrive correttamente questa mappatura. Con Docker rootless o con la rimappatura dei namespace utente, i numeri cambiano nuovamente. Leggi quindi gli id dal container in esecuzione invece di assumere che siano 999.
I volumi nominati eliminano questo problema al primo avvio, perché Docker crea una directory vuota e l'entrypoint ne diventa il proprietario.
Aggiornamenti: un aggiornamento dei pacchetti rispetto alla modifica di un tag dell'immagine
Sul sistema host, apt upgrade consente di passare a una versione minor. La distribuzione non aggiorna automaticamente una versione major del database e, quando si decide di farlo, entrambi i set di binari possono essere installati contemporaneamente. È esattamente ciò di cui ha bisogno pg_upgrade.
In un container, il tag identifica la versione. Un aggiornamento consiste quindi nella modifica di una riga. Gli aggiornamenti minor diventano semplici, mentre quelli major richiedono una procedura.
Modifica postgres:16 in postgres:17, esegui docker compose up -d e il container si arresta immediatamente:
PostgreSQL Database directory appears to contain a database; Skipping initialization
FATAL: database files are incompatible with server
DETAIL: The data directory was initialized by PostgreSQL version 16, which is not compatible with this version 17.Non viene danneggiato nulla. I nuovi binari non possono leggere il vecchio formato del catalogo su disco, che cambia tra le versioni major. Ripristina il tag postgres:16 e il servizio riparte. Questo rollback è l'unico vantaggio concreto che i container offrono per gli aggiornamenti.
La procedura supportata consiste nell'eseguire un dump e un restore. PostgreSQL preferisce che il dump venga creato dal client più recente. Eseguilo quindi dalla nuova immagine, collegandoti al server precedente ancora in esecuzione sulla rete Compose:
docker run --rm --network myapp_default -e PGPASSWORD="$POSTGRES_PASSWORD" \
postgres:17 pg_dumpall -h db -U postgres > /srv/backups/all.sql
ls -lh /srv/backups/all.sql
tail -n 2 /srv/backups/all.sqlIl file deve avere almeno alcune decine di kilobyte e terminare con una riga contenente PostgreSQL database cluster dump complete. Un file di poche centinaia di byte indica che il dump non è riuscito e che si sta per eliminare inutilmente un volume. Solo dopo questo controllo:
docker compose down
docker volume rm myapp_pgdata
# edit the compose file: image: postgres:17
docker compose up -d db
docker compose exec -T db psql -U postgres -f /dev/stdin < /srv/backups/all.sqlGli altri engine si comportano in modo diverso:
- MySQL 8 aggiorna il proprio dizionario dei dati all'avvio. Di conseguenza, la modifica a un tag minor richiede in genere soltanto un riavvio. Prima di passare a un'altra serie di release, consulta le note di rilascio ed esegui comunque prima un dump.
- MariaDB richiede l'esecuzione di
mariadb-upgradedopo l'avvio del server con la nuova versione. - MongoDB deve essere aggiornato di una sola versione major alla volta. Dopo ogni passaggio, imposta la feature compatibility version prima di procedere. Se salti una versione,
mongodnon si avvia e registra una rigaUPGRADE PROBLEMche indicafeatureCompatibilityVersion. A partire da MongoDB 7.0, il comando richiede un flag di conferma esplicito:db.adminCommand({ setFeatureCompatibilityVersion: "8.0", confirm: true }). - Redis carica senza problemi i file snapshot delle versioni precedenti, ma non quelli delle versioni successive. Un aggiornamento consiste quindi in un riavvio, mentre un downgrade può non riuscire a caricare i dati.
La regola generale è questa: un container semplifica il downgrade, ma non rende più semplice l'upgrade.
Perché il container del database termina con codice 137?
Perché il kernel lo ha terminato tramite l'OOM killer, cioè il meccanismo che interviene quando la memoria è esaurita. 137 equivale a 128 più il segnale 9.
docker compose ps
docker inspect myapp-db-1 | grep -i oomkilled
journalctl -k | tail -n 20docker compose ps mostra Exited (137), la riga di ispezione riporta "OOMKilled": true e il log del kernel contiene una voce corrispondente:
Memory cgroup out of memory: Killed process 4711 (postgres) total-vm:2170416kBQuesto è il meccanismo, e spesso sorprende. PostgreSQL e MySQL dimensionano i buffer in base alla memoria totale rilevata sull'host. Per loro, un limite cgroup non modifica quel valore. Su un host con 16 GB e un limite di 2 GB, il database pianifica le proprie risorse come se avesse 16 GB e il cgroup lo termina molto prima che l'host inizi a essere sotto pressione. Un semplice limite di memoria, quindi, non è sufficiente. Devi anche indicare al database quanta memoria può utilizzare:
- PostgreSQL: imposta
shared_bufferse presta attenzione awork_mem.work_memviene allocato per ogni operazione di ordinamento e per ogni connessione, quindi un valore elevato moltiplicato per cinquanta connessioni è la causa tipica di un container che termina sotto carico anziché all'avvio. - MySQL e MariaDB: imposta
innodb_buffer_pool_size, che per impostazione predefinita è 128M. In un container lascia disattivatoinnodb_dedicated_server, perché il suo unico scopo è dimensionarsi in base alla memoria rilevata sulla macchina. - MongoDB: imposta esplicitamente la dimensione della cache WiredTiger invece di lasciarla stimare in base alla memoria dell'host.
- Redis:
maxmemoryè illimitato per impostazione predefinita, quindi Redis continua a crescere finché il cgroup non lo termina. Impostamaxmemorya un valore adeguatamente inferiore al limite del container e scegli unmaxmemory-policy.
Postgres segnala l'evento anche dal proprio lato. Nel log troverai questa coppia di righe:
LOG: server process (PID 123) was terminated by signal 9: Killed
LOG: terminating any other active sessions due to crash of another server processLa terminazione di un backend obbliga tutti gli altri backend a riavviarsi, perché la memoria condivisa potrebbe essere ormai incoerente. Per l'applicazione questo provoca un picco di connessioni, non un evento trascurabile. impostazione dei limiti di memoria in Docker Compose descrive la sintassi e la differenza tra mem_limit e la forma deploy.resources.
Nulla di tutto questo scompare sull'host. Cambia soltanto livello. Senza un cgroup, il database compete con tutti gli altri processi del server e l'OOM killer dell'host sceglie una vittima in base al punteggio assegnato; potrebbe essere sshd. Un limite che termina il database in modo prevedibile è più semplice da gestire rispetto a un OOM dell'host che ti impedisce di accedere al server.
Backup: dump all'interno, backup all'esterno
Non eseguire il backup di un database in esecuzione copiandone la directory dei dati. Una copia a livello di file effettuata mentre il server sta scrivendo è incoerente. Te ne accorgi solo al momento del ripristino.
Esistono due metodi corretti: eseguire il dump con lo strumento del database mentre il database è in esecuzione e creare il backup del dump, oppure arrestare il container e copiare il volume a freddo.
docker compose exec -T db pg_dump -U postgres -Fc appdb > /srv/backups/appdb.dump
docker compose exec -T db mysqldump -u root -p"$MYSQL_ROOT_PASSWORD" --single-transaction --all-databases > /srv/backups/mysql.sql
docker compose exec -T db mongodump --archive --gzip --db appdb > /srv/backups/appdb.archive.gz
docker compose exec -T redis redis-cli BGSAVEIl parametro -T è importante. Senza di esso, docker compose exec può collegare un terminale al comando e il livello del terminale aggiunge ritorni a capo al flusso di output. Un dump testuale viene quindi ripristinato con errori anomali, mentre un dump binario risulta semplicemente danneggiato. Il problema passa inosservato durante il backup e si manifesta chiaramente un mese dopo.
--single-transaction fornisce a mysqldump uno snapshot coerente delle tabelle InnoDB senza bloccare l'intero server.
Questi comandi scrivono un solo file ciascuno. Non costituiscono un sistema di backup: non prevedono conservazione delle versioni, una copia fuori dal server né verifica. Affida la directory dei dump a uno strumento che svolga tutte e tre le attività, come i backup restic da un VPS. Esegui il backup di /srv/backups, non di /var/lib/docker/volumes.
Esegui quindi il ripristino, perché un backup che non hai mai ripristinato non è un backup:
docker compose exec -T db createdb -U postgres restore_test
docker compose exec -T db pg_restore -U postgres -d restore_test < /srv/backups/appdb.dump
docker compose exec -T db psql -U postgres -d restore_test -c '\dt'\dt dovrebbe elencare le tabelle dell'applicazione. Un risultato vuoto o Did not find any relations. indica che il dump non è quello previsto. Elimina restore_test al termine.
Il comando che elimina tutto
docker compose down -v.
Il comando down rimuove i container e la rete. -v rimuove anche ogni volume denominato dichiarato in quel file Compose, oltre a ogni volume anonimo collegato a quei container. Non viene richiesta alcuna conferma e non è possibile annullare l'operazione. È il modo più comune con cui viene eliminato un database self-hosted. Di solito accade durante la risoluzione di un problema non correlato, perché una risposta su un forum suggerisce di eseguire quel comando.
Quattro misure riducono l'impatto:
- Dichiarare il volume del database come
external: true. Compose non rimuove un volume che non gestisce, quindi-vnon può raggiungerlo. Crearlo una volta condocker volume create myapp_pgdata. - Usare
docker compose stopedocker compose startper i riavvii ordinari. differenza tra down e stop in Compose spiega che cosa rimuove ciascun comando. - Conservare i dump in un percorso dell'host esterno a tutti i volumi gestiti da Compose.
- Non incollare mai
-v, copiato da una risposta per la risoluzione di un problema, in uno stack che contiene dati importanti.
Non pubblicare la porta del database
Questa riga espone il database su Internet:
ports:
- "5432:5432"Esegue il binding su tutte le interfacce. Docker pubblica una porta riscrivendo la destinazione del pacchetto prima che le regole di input del firewall possano esaminarlo. Le regole di ufw si trovano nella catena input, quindi ufw deny 5432 non ha alcun effetto. perché le porte pubblicate da Docker ignorano ufw mostra il percorso nella catena.
Un'applicazione dello stesso progetto Compose raggiunge il database tramite il nome del servizio sulla rete Compose, quindi non ha bisogno di una porta pubblicata. Elimina il blocco. Se vuoi consentire l'accesso a un client sull'host, esegui il binding soltanto sull'interfaccia loopback:
ports:
- "127.0.0.1:5432:5432"Verifica cosa è effettivamente in ascolto:
sudo ss -ltnp | grep 5432127.0.0.1:5432 è il risultato desiderato. 0.0.0.0:5432 significa che chiunque può tentare di indovinare la password.
Cosa eseguire e dove
Una sola applicazione su un VPS. Usa un container. Configura un volume denominato con un nome fisso, senza porte pubblicate, un limite di memoria coerente con le impostazioni del database e un dump notturno in un percorso dell’host che restic possa raccogliere. Inizia da un’installazione Docker pulita su un VPS e mantieni lo stack in un unico file compose sottoposto a commit. Il vantaggio è concreto: la versione del database diventa una riga verificabile in git.
Un host che esegue più servizi. Usa container e un database per ogni applicazione, non un unico server condiviso da tutte. Un server condiviso vincola ogni applicazione allo stesso calendario di aggiornamento, e una query fuori controllo causa un’interruzione del servizio per tutti. Assegna a ogni container un proprio limite di memoria, in modo che una query errata resti confinata all’applicazione che l’ha generata. Diverse istanze Postgres di piccole dimensioni richiedono un po’ più di spazio su disco, ma molta meno coordinazione.
Il database è il prodotto. Eseguilo sull’host usando il repository dei pacchetti del fornitore, oppure scegli un servizio gestito. pg_upgrade richiede che entrambe le versioni principali dei binari siano installate contemporaneamente: è una configurazione fornita dai pacchetti, ma non da un’immagine con una sola versione. La replica e il point-in-time recovery con l’archiviazione WAL (write-ahead log) sono entrambe più semplici quando il database gestisce direttamente la macchina e i relativi dischi. Scegli la soluzione più semplice per il sistema che alle 03:00 dovrà inviarti una notifica.
L’applicazione è di piccole dimensioni. Valuta di non usare affatto un database server. Un’applicazione web con un solo writer su un VPS è spesso più adatta a SQLite in produzione su un VPS, dove il backup consiste in un unico file e il percorso di aggiornamento passa da una versione della libreria.
FAQ
È sicuro eseguire un database di produzione in Docker?
Sì, per uno stack applicativo su un singolo server. Un container è un processo Linux isolato tramite namespace e cgroup. Con un volume montato, il database scrive nello stesso filesystem dell'host che userebbe se fosse installato tramite pacchetto. I rischi sono operativi, non legati alla velocità: un volume il cui nome non è fissato, un bind mount assegnato all'ID utente errato, un ripristino mai testato e docker compose down -v. Se risolvi questi quattro aspetti, il container è adeguato. Passa a un'installazione sull'host quando il database è il carico di lavoro principale e ti servono pg_upgrade, la replica o il point-in-time recovery.
Devo usare un bind mount o un volume denominato per i dati del database?
Usa un volume denominato, salvo che tu debba conoscere esattamente il percorso sull'host. Docker crea la directory e l'entrypoint dell'immagine imposta la proprietà al primo avvio, quindi il problema dei permessi non si presenta. Fissa il volume con un name: esplicito oppure contrassegnalo come external: true. In caso contrario, la rinomina della directory del progetto crea in modo silenzioso un nuovo volume vuoto e un database vuoto. Un bind mount va bene se assegni con chown la directory dell'host all'ID utente numerico con cui viene eseguita l'immagine. Per le immagini ufficiali di Postgres, MySQL e MongoDB questo valore è 999. Verificalo con ls -ldn, perché ls -l mostra il nome assegnato dall'host a quel numero e tale nome non ha significato all'interno del container.
Che cosa elimina docker compose down -v?
Rimuove i container e la rete, come un normale down. Inoltre, -v rimuove ogni volume denominato dichiarato in quel file Compose e ogni volume anonimo collegato ai container. Questo include il database. Non viene mostrata alcuna richiesta di conferma e non è possibile recuperare i dati. I volumi contrassegnati come external: true non vengono rimossi. Questo è il motivo principale per cui conviene contrassegnare come esterno un volume del database. Per un riavvio ordinario usa invece docker compose stop e docker compose start.
Come aggiorno PostgreSQL a una nuova versione major in Docker?
Esegui un dump e un ripristino. La modifica di postgres:16 in postgres:17 e il riavvio producono FATAL: database files are incompatible with server con una riga DETAIL che indica entrambe le versioni, perché i nuovi binari non possono leggere il vecchio layout del catalogo. Non viene danneggiato nulla: ripristina il tag precedente e il database si avvia. Crea un pg_dumpall usando il client della nuova versione contro il container precedente ancora in esecuzione. Verifica che il file termini con PostgreSQL database cluster dump complete. Avvia quindi il nuovo tag su un volume vuoto e carica il dump. Gli aggiornamenti minor all'interno della stessa versione major richiedono soltanto un pull e un riavvio.
Perché il container del database termina con il codice 137?
137 equivale a 128 più il segnale 9. Questo significa che un processo lo ha terminato forzatamente. Esegui docker inspect <container> | grep -i oomkilled. Un valore true indica che il container ha raggiunto il limite di memoria del relativo cgroup. La causa più comune è che PostgreSQL e MySQL leggono la memoria totale dall'host e non rilevano il limite del container. Di conseguenza, pianificano l'uso di 16 GB anche se dispongono di 2 GB. Imposta shared_buffers e work_mem, oppure innodb_buffer_pool_size, in modo compatibile con il limite assegnato al container. Controlla journalctl -k per trovare la riga Memory cgroup out of memory corrispondente e confermare quale processo ha terminato il kernel.