Docker Compose: più file, override e ordine di merge
Scopri come viene caricato compose.override.yaml, come l’ordine dei file unisce i valori, perché ports può lasciare una porta aperta e come usare include tra dev e prod.
Cosa fa Compose con più di un file
Docker Compose può creare un singolo progetto a partire da più file. Li legge nell’ordine in cui li riceve e li unisce in un unico modello, quindi un file successivo prevale su qualsiasi valore in conflitto. Dalla riga di comando esistono due meccanismi per farlo: un file di override che Compose carica autonomamente e il flag -f che si specifica manualmente. Un terzo meccanismo è definito direttamente nel file, nell’elemento include, e funziona in modo diverso dagli altri due.
L’unione non consiste in una semplice sovrascrittura. Le mappe vengono unite chiave per chiave, le sequenze vengono accodate e un piccolo insieme di campi viene sostituito interamente. Le differenze producono comportamenti imprevisti; l’elenco ports è quello che causa problemi quasi a tutti.
Tutto quanto segue presuppone Compose v2, cioè il plugin docker compose e non il vecchio script docker-compose. Esegui docker compose version per verificare. Se non hai ancora scritto un file Compose, inizia dalla guida di base su Docker Compose e poi torna qui.
Il file di override caricato da Compose senza che venga specificato
Esegui docker compose up senza il flag -f. Compose cerca nella directory di lavoro e poi nelle directory padre compose.yaml o docker-compose.yaml. Se un file di override si trova accanto al file di base, Compose lo carica automaticamente come secondo file.
ls compose.yaml compose.override.yaml
docker compose up -dQuando entrambi i file sono presenti, il risultato è lo stesso che si ottiene specificandoli manualmente.
docker compose -f compose.yaml -f compose.override.yaml up -dI nomi riconosciuti da Compose sono compose.override.yaml, compose.override.yml e i nomi precedenti docker-compose.override.yml e docker-compose.override.yaml. Qualsiasi altro nome, ad esempio compose.dev.yaml, viene caricato solo se lo specifichi con -f.
Non appena specifichi un -f, il caricamento automatico si interrompe. docker compose -f compose.yaml up legge esattamente quel file e ignora il file di override. È questa la proprietà su cui si basa più avanti in questa guida il modello dev e prod.
Su un server, questo comportamento può avere conseguenze opposte. Un file di override lasciato nella directory di deploy viene caricato da ogni comando docker compose eseguito senza argomenti da quella directory, incluso quello eseguito dal tuo cron job. In questo modo uno stack di produzione può finire per montare tramite bind una directory sorgente che nessuno intendeva distribuire. Esegui docker compose config dopo ogni deploy e verifica l'output.
Ordine con -f e risoluzione dei percorsi relativi
Compose crea la configurazione nell'ordine in cui fornisci i file. I file successivi sovrascrivono e completano quelli precedenti. Da sinistra a destra, prevale l'ultimo.
docker compose -f compose.yaml -f compose.prod.yaml config
docker compose -f compose.yaml -f compose.prod.yaml up -dOgni comando del progetto deve usare lo stesso elenco di file. Se esegui up con due file e logs con uno solo, utilizzi un modello unito diverso. Questo è un modo rapido per ottenere un servizio che Compose considera inesistente. Imposta invece l'elenco una sola volta con la variabile d'ambiente COMPOSE_FILE.
export COMPOSE_FILE=compose.yaml:compose.prod.yaml
docker compose config
docker compose up -dIl separatore è : su Linux, mentre COMPOSE_PATH_SEPARATOR lo modifica. COMPOSE_FILE può anche essere contenuta nel file .env del progetto. In questo modo l'impostazione fa parte del checkout e non della cronologia della shell. Qualsiasi valore impostato esplicitamente sulla riga di comando ha la precedenza sulla variabile d'ambiente.
Questa è la regola che causa problemi con i bind mount. Quando usi più file con -f, tutti i percorsi relativi contenuti nei file vengono risolti rispetto alla directory del primo file, non rispetto al file che li contiene. Scrivi ./data:/var/lib/postgresql/data all'interno di deploy/prod/compose.prod.yaml e Compose cerca comunque ./data accanto al file di base. Docker crea quindi una directory vuota nel percorso errato e il container viene avviato senza alcun contenuto al suo interno. Questo sembra una perdita di dati, ma non lo è. Usa --project-directory per impostare manualmente il percorso di base oppure usa include, che risolve ogni file rispetto alla relativa directory.
Il nome del progetto deriva dalla stessa directory di base. Cambiare il file che viene indicato per primo può quindi rinominare il progetto. Un progetto rinominato usa nuovi nomi per i container e per i volumi, mentre il volume precedente rimane sul disco con il vecchio nome. Imposta invece in modo fisso un name: di primo livello nel file di base.
name: myappQuali campi vengono uniti e quali vengono sostituiti
Compose esegue l'unione in base al tipo del valore, non al nome del campo.
- I campi con un solo valore vengono sostituiti.
image,command,entrypointemem_limitassumono direttamente il valore successivo. Non è possibile aggiungere un argomento acommand, perché la sovrascrittura riscrive l'intera riga. - Le mappe vengono unite chiave per chiave.
environment,labels,volumesedevicesconservano tutte le chiavi presenti in entrambi i file; per le chiavi presenti in entrambi, prevale il file successivo. Perenvironmentelabels, la chiave è il nome della variabile o dell'etichetta. Pervolumesedevices, la chiave è il percorso del container. - Le sequenze vengono accodate.
dns,dns_search,expose,tmpfseexternal_linksvengono concatenate. Una configurazione di base contenenteexpose: ["3000"], unita a una configurazione di override contenente["4000", "5000"], produce["3000", "4000", "5000"].
Quattro sequenze hanno una chiave di identificazione, quindi le voci che corrispondono a tale chiave vengono unite invece di essere accodate. volumes, secrets e configs corrispondono in base a target. ports corrisponde alla combinazione di ip, target, published e protocol.
Leggere due volte la regola ports, perché è il punto più insidioso. Due voci di porta rappresentano la stessa voce solo quando tutte e quattro le parti corrispondono. Se se ne modifica anche una sola, Compose considera la voce come una seconda porta indipendente e le conserva entrambe.
Perché la porta è ancora pubblicata dopo l'override
Un file di base che pubblica un servizio su ogni interfaccia:
services:
web:
image: nginx:1.27
ports:
- "8080:80"Un override scritto per associarlo solo a localhost, perché davanti al servizio verrà posizionato un reverse proxy:
services:
web:
ports:
- "127.0.0.1:8080:80"Verificate il risultato prima di supporre che la modifica abbia funzionato.
docker compose -f compose.yaml -f compose.prod.yaml configEntrambe le voci sono presenti nell'output. La parte ip è diversa, 0.0.0.0 rispetto a 127.0.0.1, quindi per quanto riguarda il merge sono due porte diverse e l'associazione pubblica che avete tentato di rimuovere è ancora presente nel modello. Questo è più importante con Docker che in altri contesti, perché una porta pubblicata viene scritta in iptables prima delle regole del firewall. Il meccanismo è descritto in perché le porte pubblicate da Docker oltrepassano ufw.
Esistono due correzioni. Quella esplicita usa il tag !override, che sostituisce l'intero attributo e ignora le regole di merge:
services:
web:
ports: !override
- "127.0.0.1:8080:80"!override richiede Compose v2.24.4 o versione successiva. La correzione portabile non richiede alcun tag: lasciate ports completamente fuori dal file di base e dichiaratelo solo nei file specifici dell'ambiente. Se non c'è nulla da unire, non c'è nulla da esporre. Questo è il modello usato nell'esempio completo seguente.
Eliminare un valore nel file di base
!reset rimuove un attributo e lo ripristina al valore predefinito oppure a null. Accetta un valore e lo ignora, quindi specificare un valore valido e vuoto.
services:
web:
ports: !reset []
environment:
DEBUG: !reset null!reset richiede Compose v2.24 o una versione successiva. Utilizzarlo quando non è possibile modificare il file di base, ad esempio un frammento fornito da un vendor.
include, per gli stack assemblati da più componenti
include importa un'altra applicazione Compose nel modello. È un elemento di primo livello, non un flag.
include:
- path: ../commons/compose.yamlOgni percorso in include viene caricato come modello di applicazione Compose separato, con una propria directory del progetto. Di conseguenza, i percorsi relativi contenuti nel file vengono risolti rispetto alla directory del file stesso. Questa è la differenza effettiva rispetto a -f e il motivo per cui include è lo strumento corretto quando il frammento si trova in un'altra cartella o in un altro repository.
La sintassi estesa accetta opzioni secondarie.
include:
- path:
- ../monitoring/compose.yaml
- ../monitoring/compose.vps.yaml
project_directory: ../monitoring
env_file: ../monitoring/.envpath accetta un elenco e i relativi file vengono uniti secondo le regole normali prima che il risultato venga aggiunto al modello. project_directory imposta il percorso di base usato per risolvere i percorsi relativi nel file incluso. env_file assegna al file incluso le proprie variabili per l'interpolazione, impedendo che un frammento condiviso legga senza intenzione il file .env del progetto. include richiede Compose v2.20.0 o versioni successive.
I nomi delle risorse duplicati tra il file e un file incluso vengono segnalati come errore, invece di essere uniti senza avviso. Questo comportamento è intenzionale. Per modificare una dichiarazione contenuta in un file incluso, inserisci la modifica in compose.override.yaml: l'override viene applicato al modello assemblato e può quindi intervenire sulle risorse incluse senza creare conflitti.
In breve: include compone applicazioni separate, mentre -f aggiunge livelli di configurazione a una sola applicazione.
Separare sviluppo e produzione su un unico VPS
Ecco l'intero schema in tre file. Il file di base dichiara ciò che vale ovunque e non pubblica alcuna porta.
name: myapp
services:
app:
image: ghcr.io/example/app:1.4.2
environment:
DATABASE_URL: postgres://app:${POSTGRES_PASSWORD}@db:5432/app
LOG_LEVEL: info
depends_on:
db:
condition: service_healthy
restart: unless-stopped
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_DB: app
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
volumes:
db_data:La condizione depends_on fa attendere all'applicazione un database che risponde, invece di un container che esiste soltanto. Questo è spiegato in healthcheck e condizioni depends_on. POSTGRES_PASSWORD viene interpolato dal file .env del progetto, che non deve mai essere incluso in git. Per le varianti più sicure, vedere file env e segreti di Compose.
Poi c'è compose.override.yaml, che Compose carica automaticamente. Questo è il file dello sviluppatore.
services:
app:
build: .
command: npm run dev
environment:
LOG_LEVEL: debug
ports:
- "3000:3000"
volumes:
- ./src:/app/src
db:
ports:
- "127.0.0.1:5432:5432"Su un laptop, un semplice docker compose up unisce questi due file. command sostituisce il valore predefinito dell'immagine perché è un valore singolo. LOG_LEVEL sostituisce info perché environment esegue l'unione in base alla chiave. Il bind mount e le due porte pubblicate sono aggiunte pure, mentre la porta del database è associata solo a localhost. In questo modo, un laptop collegato a una rete condivisa non espone PostgreSQL agli altri utenti.
Infine, compose.prod.yaml. Il suo nome non corrisponde a un file che Compose cerca, quindi non viene mai caricato accidentalmente.
services:
app:
ports:
- "127.0.0.1:8000:3000"
deploy:
resources:
limits:
memory: 512MSul VPS si specificano entrambi i file. È proprio questa specifica a escludere l'override.
docker compose -f compose.yaml -f compose.prod.yaml config
docker compose -f compose.yaml -f compose.prod.yaml up -d
docker compose -f compose.yaml -f compose.prod.yaml psps dovrebbe elencare entrambi i servizi come in esecuzione, con db che mostra (healthy). Poiché è stato passato -f, compose.override.yaml non è stato letto. Di conseguenza, il comando di sviluppo, il bind mount del codice sorgente e la porta pubblica 3000 non possono raggiungere la produzione, anche se il file si trova nella stessa directory. La porta 8000 è disponibile solo su localhost ed è pronta per un proxy: vedere eseguire più applicazioni dietro Traefik quando si aggiunge il secondo servizio.
Impostare COMPOSE_FILE=compose.yaml:compose.prod.yaml nel file .env del server. Da quel momento, gli altri comandi torneranno a essere semplici docker compose logs -f app.
Leggi il modello unito prima della distribuzione
docker compose config stampa il modello completamente unito e interpolato. Non è un'anteprima. È l'input esatto su cui agirà Compose; se l'output non corrisponde alle aspettative, è l'output a essere corretto.
docker compose -f compose.yaml -f compose.prod.yaml config
docker compose -f compose.yaml -f compose.prod.yaml config --no-interpolate
docker compose -f compose.yaml -f compose.prod.yaml config --services--no-interpolate lascia ${VAR} non espansa. Usalo prima di incollare l'output altrove, perché il semplice config stampa ogni segreto risolto in testo non cifrato. --services elenca solo i nomi dei servizi; è un modo rapido per verificare che un include abbia incluso ciò che ti aspettavi.
Modalità di errore e comportamento osservato
no configuration file provided: not found. Compose non ha trovato nulla da leggere. Ti trovi al di fuori della directory del progetto oppure COMPOSE_FILE indica un percorso inesistente. Compose cerca nelle directory padre il file di base predefinito, ma non cerca in alcuna directory un file specificato manualmente.
WARN[0000] The "POSTGRES_PASSWORD" variable is not set. Defaulting to a blank string. L'interpolazione viene risolta in base al file .env del progetto e all'ambiente della shell. La directory del progetto è la directory del primo file -f. Eseguire il deploy da una directory diversa da quella che contiene .env genera questo avviso e lascia un database che rifiuta ogni connessione.
La modifica all'override non compare in docker compose config. Potresti avere passato -f, che disattiva il caricamento automatico dell'override, oppure Compose potrebbe avere trovato compose.yaml in una directory padre e il file di override non si trova accanto a esso. L'esecuzione di docker compose config senza altri argomenti mostra il modello che Compose sta realmente creando.
Un bind mount è vuoto e Docker ha creato una directory non richiesta. Il percorso relativo è stato risolto rispetto alla directory del primo file. Correggi il percorso, passa --project-directory oppure sposta il frammento dopo include.
I container tornano con nuovi nomi e un volume sembra vuoto. Il nome del progetto è cambiato, perché segue la directory del primo file. Aggiungi un name: di primo livello al file di base per impedire ulteriori modifiche ai nomi. Il vecchio volume esiste ancora con il vecchio prefisso e docker volume ls lo mostrerà.
Una porta rimossa nell'override è ancora aperta. L'unione di ports ha aggiunto il valore invece di sostituirlo. Verifica con docker compose config, quindi usa !override oppure sposta ports fuori dal file di base.
FAQ
Compose carica automaticamente compose.override.yaml?
Sì, quando esegui docker compose senza il flag -f. Compose cerca nella directory di lavoro e nelle directory padre compose.yaml o docker-compose.yaml. Se trova un file di override nella stessa directory, lo carica per secondo. I nomi riconosciuti sono compose.override.yaml, compose.override.yml, docker-compose.override.yml e docker-compose.override.yaml. Il passaggio di qualsiasi -f disabilita questo comportamento, quindi docker compose -f compose.yaml up legge un solo file.
In quale ordine vengono uniti più file -f?
Da sinistra a destra. Compose crea la configurazione nell'ordine in cui fornisci i file. Ogni file sostituisce e aggiunge elementi rispetto ai file precedenti. In caso di conflitto, prevale quindi l'ultimo file della riga di comando. Devi usare lo stesso elenco per ogni comando del progetto. A questo serve COMPOSE_FILE=compose.yaml:compose.prod.yaml.
Perché la mia porta è ancora pubblicata dopo l'override?
Perché le voci ports vengono identificate dall'insieme completo di ip, target, published e protocol. Un override di 127.0.0.1:8080:80 rispetto a una base 8080:80 differisce nella parte ip. Compose lo considera quindi una seconda porta e mantiene entrambe. Esegui docker compose config per visualizzare le due voci. Usa ports: !override con Compose v2.24.4 o versione successiva, oppure escludi ports dal file di base, così non ci sarà nulla da unire.
Qual è la differenza tra include e -f?
-f sovrappone più file per creare una singola applicazione. I percorsi relativi presenti in ogni file vengono risolti rispetto alla directory del primo file. include include un'applicazione Compose separata. Ogni file incluso conserva la propria directory del progetto, quindi i relativi percorsi vengono risolti rispetto a quella directory. Usa -f per i livelli di ambiente del tuo stack e include per un frammento gestito altrove. include richiede Compose v2.20.0 o versione successiva.
Come posso rimuovere un valore impostato dal file di base?
Usa il tag !reset con Compose v2.24 o versione successiva. Nel file di override, scrivi ports: !reset [] o MY_VAR: !reset null. L'attributo torna così al valore predefinito o a null. Il valore assegnato al tag è obbligatorio, ma viene ignorato. Se vuoi sostituire un attributo invece di cancellarlo, usa !override. Questa funzionalità richiede v2.24.4 o versione successiva.