Docker Compose: bind mount o volume denominato?
Scopri quando usare bind mount o named volume in Docker Compose, evita problemi di permessi e impara a ispezionare, fare backup e migrare i dati.
Bind mount o named volume: risposta breve
I volumi Docker Compose sono di due tipi. La scelta dipende da chi gestisce i file. Usa un bind mount per i file che leggi e scrivi direttamente, come file di configurazione, template e siti statici. Usa un named volume per i dati gestiti dall'applicazione, come file di database, indici di ricerca e contenuti multimediali caricati dagli utenti. Un bind mount punta a un percorso sull'host che puoi aprire in un editor. Un named volume è uno spazio di archiviazione creato e gestito da Docker, a cui accedi tramite Docker.
Entrambi vengono definiti con la stessa chiave volumes: all'interno di un servizio, per questo vengono spesso confusi. La differenza è il lato sinistro dei due punti. Se il lato sinistro inizia con . o /, indica un percorso sull'host, quindi si tratta di un bind mount. Qualsiasi altro valore è un nome, quindi si tratta di un named volume; questo nome deve essere dichiarato anche nel blocco volumes: di livello superiore.
Le due sintassi in un file Compose
services:
db:
image: postgres:17
environment:
POSTGRES_PASSWORD: changeme
volumes:
- pgdata:/var/lib/postgresql/data
web:
image: nginx:1.27
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./site:/usr/share/nginx/html:ro
volumes:
pgdata:pgdata:/var/lib/postgresql/data è un volume denominato. ./nginx.conf:/etc/nginx/nginx.conf è un bind mount e :ro lo monta in sola lettura: è l'impostazione predefinita corretta per una configurazione che il container non deve mai riscrivere. Se si dimentica la voce di primo livello volumes:, Compose si interrompe con service "db" refers to undefined volume pgdata.
Avviare lo stack e elencare gli elementi creati da Docker:
docker compose up -d
docker volume lsIl volume non si chiama pgdata. Si chiama <project>_pgdata, perché per impostazione predefinita il nome del progetto corrisponde al nome della directory che contiene il file Compose. Una directory denominata myapp produce myapp_pgdata. Questo è importante perché rinominando la directory si ottiene un nuovo volume vuoto e l'applicazione sembra aver perso i dati. Non è così: il vecchio volume è ancora elencato da docker volume ls. Impostare esplicitamente il nome con name: nel file Compose oppure definire COMPOSE_PROJECT_NAME se la directory potrebbe essere spostata. Impostazioni di questo tipo vanno inserite insieme agli altri file di ambiente e secret di Compose.
Perché gli errori di autorizzazione colpiscono soprattutto i bind mount
Questa è la differenza pratica principale e dipende soprattutto da una regola: un named volume vuoto al primo utilizzo viene inizializzato a partire dall'immagine, mentre un bind mount non lo viene mai.
Quando Docker monta un named volume vuoto su una directory che contiene già dati nell'immagine, copia quei dati nel volume mantenendo il proprietario e i permessi impostati dall'immagine. L'immagine ufficiale di Postgres distribuisce /var/lib/postgresql/data, di proprietà del proprio utente postgres, quindi il volume risulta di proprietà dello stesso ID numerico e il database si avvia.
Questa inizializzazione è l'unico vantaggio iniziale di un named volume. Docker gestisce la posizione del volume, ma i container lo leggono e vi scrivono usando normali operazioni del filesystem; di conseguenza, proprietario e permessi vengono applicati esattamente come in un bind mount. Un named volume montato in un percorso che l'immagine non contiene, oppure inizializzato per primo da un'immagine o da un utente diverso, non eredita alcun proprietario dall'immagine. Anche in quel caso un processo non-root può ricevere un errore di autorizzazione. Il controllo id riportato sotto funziona per entrambi i tipi.
Un bind mount si comporta nel modo opposto. Il container vede esattamente ciò che si trova sull'host, compreso il proprietario, mentre il contenuto dell'immagine in quel percorso viene nascosto. Se la directory sull'host non esiste, il Docker daemon la crea e il daemon viene eseguito come root; si ottiene quindi una directory di proprietà di root:root. Un processo del container eseguito come utente non-root non può scrivervi:
PermissionError: [Errno 13] Permission denied: '/data/app.db'La soluzione consiste nel rendere uguali i numeri. In un bind mount il proprietario viene confrontato tramite l'ID numerico dell'utente, non tramite il nome, perché il container ha il proprio /etc/passwd. Un utente chiamato app all'interno del container non ha alcun significato sull'host. Uid 1000 significa uid 1000 su entrambi i lati, a meno che il daemon non utilizzi il remapping degli user namespace (userns-remap, configurato in /etc/docker/daemon.json o nella riga di comando dockerd). Il remapping trasla ogni ID del container in un intervallo subordinato sull'host. Nell'intervallo di esempio di Docker, che inizia da 231072, il container uid 0 corrisponde all'host uid 231072; quindi il container uid 1000 corrisponde all'host uid 232072.
id -u
mkdir -p ./data
docker compose exec web id
sudo chown -R 1000:1000 ./datadocker compose exec web id stampa l'uid con cui viene effettivamente eseguito il processo del container. Senza remapping, assegna alla directory sull'host lo stesso numero oppure imposta user: "1000:1000" nel servizio per vincolare il container al tuo numero. Con il remapping attivo, user: non può vincolare il container al tuo numero sull'host, perché anche l'ID impostato viene traslato: in quell'intervallo di esempio, user: "1000:1000" corrisponde comunque all'host uid 232072. Cambia invece il proprietario della directory sull'host impostando l'ID host risultante dal mapping; il tuo intervallo è indicato in /etc/subuid e /etc/subgid. La documentazione di Docker indica soltanto --userns=host su docker container create, run o exec come metodo per disattivare il remapping per un singolo container. Impostare user: è più semplice per un'applicazione scritta da te. Cambiare il proprietario della directory sull'host è più sicuro per un'immagine che non hai scritto tu, perché alcune immagini avviano l'entrypoint come root, riducono quindi i privilegi e si aspettano proprietari specifici nelle directory sottostanti.
È utile conoscere altri due casi problematici. Su Fedora, RHEL e altri sistemi con SELinux (security-enhanced Linux) in modalità enforcing, un bind mount viene negato finché non viene rietichettato. Aggiungi quindi :z per un percorso condiviso tra container oppure :Z per un percorso che deve essere usato da un solo container, nella forma - ./data:/data:Z. Inoltre, un bind mount di un singolo file, invece di una directory, si interrompe quando un editor sostituisce il file anziché modificarlo direttamente, perché il mount segue l'inode originale. Il container continua a vedere il contenuto precedente finché non viene riavviato. Monta la directory padre quando il file viene modificato spesso.
Prestazioni: dove la differenza è reale
Su un server Linux, entrambi i tipi seguono lo stesso percorso nel kernel. La differenza di throughput è quindi abbastanza ridotta da non dover scegliere in base a questo criterio. I volumi denominati che usano il driver predefinito local risiedono sullo stesso filesystem del resto di Docker, in /var/lib/docker/volumes/. Un bind mount risiede invece nel percorso indicato.
La differenza emerge in Docker Desktop per macOS e Windows, dove i container vengono eseguiti all'interno di una macchina virtuale. In questo caso, un bind mount attraversa il filesystem dell'host e raggiunge la macchina virtuale tramite un livello di condivisione dei file. I carichi di lavoro con molte operazioni su file di piccole dimensioni, come un albero delle dipendenze Node.js o la cache di un framework PHP, risultano sensibilmente più lenti. I volumi denominati restano all'interno della macchina virtuale e non sostengono questo costo. Per questo molti file Compose di sviluppo montano la directory del codice sorgente con un bind mount, ma dichiarano un volume denominato su node_modules.
L'altra differenza effettiva riguarda la posizione in cui vengono scritti i dati. Un bind mount su /mnt/backup salva i dati su quel disco. Un volume denominato viene salvato nel filesystem che contiene /var/lib/docker, solitamente il disco root nel caso di un VPS. Un database che cresce all'interno di un volume denominato riempie lo stesso disco su cui risiedono i log di sistema. Verificalo prima che si verifichi un incidente:
docker system df -v
df -h /var/lib/dockerdocker system df -v elenca tutti i volumi con le relative dimensioni e contrassegna quelli a cui nessun container fa più riferimento.
Ispezione di un volume denominato
Un volume denominato non è una scatola nera. Chiedi a Docker dove si trova:
docker volume inspect myapp_pgdataIl campo Mountpoint indica un percorso reale sull'host, normalmente /var/lib/docker/volumes/myapp_pgdata/_data. Puoi leggerlo con sudo ls, utile per un controllo rapido. Non usarlo per modificare i file. Scrivervi come root ripropone il problema di ownership descritto sopra e il percorso è un dettaglio del driver local che gli altri driver di volume non condividono.
Il modo sicuro per esaminarne il contenuto consiste nell'usare un container temporaneo che monta il volume:
docker run --rm -v myapp_pgdata:/vol alpine ls -la /volFunziona con qualsiasi driver, applica le stesse autorizzazioni visibili al container reale e non lascia nulla dopo l'esecuzione grazie a --rm.
Backup di ogni tipo
Un bind mount è una directory ordinaria, quindi qualsiasi strumento di backup a livello di file lo gestisce già. Indicate come destinazione del backup il percorso sull'host e l'operazione è completata. Un volume denominato richiede un passaggio aggiuntivo, perché lo strumento deve accedervi. Montate il volume e una directory dell'host nello stesso container temporaneo, quindi create un archivio:
docker run --rm \
-v myapp_pgdata:/data:ro \
-v "$PWD":/backup \
alpine tar czf /backup/pgdata.tar.gz -C /data .Ripristinate i dati eseguendo l'operazione inversa in un volume nuovo:
docker volume create myapp_pgdata_restored
docker run --rm \
-v myapp_pgdata_restored:/data \
-v "$PWD":/backup \
alpine tar xzf /backup/pgdata.tar.gz -C /datatar preserva la proprietà numerica quando viene eseguito come root all'interno del container. Questo mantiene il volume ripristinato utilizzabile dall'applicazione.
Un'avvertenza vale per entrambi i tipi. Copiare i file di un database mentre il database è in esecuzione produce un archivio di dati che possono cambiare durante la copia e il ripristino può risultare corrotto. Arrestate prima il servizio oppure eseguite un dump tramite lo strumento del database, come in docker compose exec -T db pg_dump -U postgres appdb > appdb.sql. In questo modo ottenete un file semplice, che potete includere in una normale procedura di backup cifrato con restic insieme ai file Compose.
Migrazione di un bind mount a un volume denominato
Lo spostamento è una copia, non una ridenominazione, e richiede circa un minuto.
docker compose down
docker volume create myapp_pgdata
docker run --rm \
-v "$PWD/data":/from \
-v myapp_pgdata:/to \
alpine sh -c 'cp -a /from/. /to/'cp -a mantiene proprietario, permessi e timestamp. L'utente del container che poteva leggere la directory precedente può quindi continuare a leggere il nuovo volume. Modifica quindi il servizio per usare pgdata:/var/lib/postgresql/data, aggiungi pgdata al blocco volumes: di primo livello, esegui docker compose up -d e controlla i log dell'applicazione prima di eliminare la directory precedente. Per eseguire l'operazione inversa, usa lo stesso comando scambiando /from e /to.
Durante i test, ricorda un aspetto importante. docker compose down lascia invariati i volumi denominati, mentre docker compose down -v elimina tutti i volumi denominati dichiarati dal progetto e non è possibile annullare l'operazione. Un bind mount sopravvive a entrambi i comandi, perché Docker non è proprietario di quella directory. Se i comandi del ciclo di vita non ti sono ancora familiari, la guida di base a Docker Compose per un VPS li illustra passo per passo.
Scelta, servizio per servizio
Chiediti chi scrive il file. La configurazione che modifichi in un editor di testo e sottoponi a commit in git appartiene a un bind mount, montato :ro, perché vuoi che sia visibile e sottoposto a controllo versione. Lo stato dell'applicazione che non apri mai manualmente appartiene a un volume denominato, perché Docker imposta i permessi in base all'immagine e i dati non dipendono da un percorso dell'host.
Il caso misto riguarda i contenuti multimediali. Una libreria di foto viene scritta dall'applicazione, ma viene anche gestita da te e spesso è abbastanza grande da richiedere un disco dedicato. Montala tramite bind mount in un percorso su quel disco e imposta esplicitamente la proprietà una sola volta. Questo è il modello adottato dalla maggior parte degli stack self-hosted: volumi denominati per database e cache, bind mount per la configurazione e per la directory di grandi dimensioni che vuoi gestire direttamente. Un help desk come Chatwoot in esecuzione su un VPS rientra esattamente in questo modello, con Postgres in un volume denominato e gli allegati caricati in un percorso che puoi indicare come origine per un backup.
FAQ
Qual è la differenza tra un bind mount e un named volume?
Un bind mount mappa un percorso dell'host nel container, quindi entrambi vedono la stessa directory e puoi modificarla con gli strumenti consueti. Un named volume è uno spazio di archiviazione creato e gestito da Docker, identificato tramite un nome e dichiarato nel blocco di primo livello volumes:. La distinzione pratica riguarda il proprietario: usa i bind mount per la configurazione che gestisci tu e i named volume per i dati gestiti dall'applicazione.
Perché ricevo "permission denied" con un bind mount ma non con un named volume?
Un named volume vuoto viene inizializzato a partire dall'immagine, quindi in genere eredita il proprietario impostato dall'immagine e l'utente del container può scrivervi. I permessi vengono comunque applicati, quindi può verificarsi lo stesso errore se l'immagine non contiene nulla in quel percorso. Un bind mount mostra la directory dell'host esattamente nello stato in cui si trova e, se Docker ha dovuto crearla, l'ha resa di proprietà di root. Esegui docker compose exec <service> id per vedere l'ID numerico usato dal container, quindi sudo chown -R <uid>:<gid> sulla directory dell'host oppure, quando il daemon non rimappa gli user namespace, imposta user: "1000:1000" nel servizio. Se il daemon utilizza il remapping degli user namespace, cambia invece il proprietario impostando l'ID host risultante dal mapping, perché gli ID del container vengono traslati sull'host e user: non modifica questo comportamento.
Dove archivia Docker i named volume sul disco?
Con il driver predefinito local vengono archiviati in /var/lib/docker/volumes/<volume>/_data e docker volume inspect <volume> stampa il Mountpoint esatto. Leggilo se devi verificare qualcosa, ma scrivici solo tramite un container, perché modificare i dati come root sull'host cambia i proprietari in modi che il container non si aspetta.
Come posso eseguire il backup di un named volume?
Avvia un container temporaneo con il volume e una directory dell'host montati entrambi, quindi archivia i dati da uno all'altro con docker run --rm -v myvol:/data:ro -v "$PWD":/backup alpine tar czf /backup/myvol.tar.gz -C /data .. Per un database, esegui il dump con lo strumento del database anziché copiare i file mentre il database è in esecuzione, perché una copia effettuata durante operazioni di scrittura può essere ripristinata in uno stato corrotto.
docker compose down elimina i miei volumi?
docker compose down rimuove i container e le reti, lasciando inalterati i named volume. docker compose down -v elimina anche ogni named volume dichiarato dal progetto, in modo permanente. I bind mount non vengono mai rimossi da nessuno dei due comandi, perché la directory appartiene all'host e non a Docker.