SSD Nodes Learn Hosting plans →
Guide Matt ConnorDi Matt Connor · Aggiornato 2026-10-30

Docker Compose: usare più file senza sorprese

Scopri come compose.override.yaml viene caricato da solo, come l'ordine dei file unisce la configurazione, perché ports lascia porte aperte e come separare dev e prod con include.

Cosa fa Compose con più file

Docker Compose può creare un progetto a partire da più file. Li legge nell'ordine in cui li riceve e li unisce in un unico modello; se due valori sono in conflitto, prevale quello del file successivo. Dalla riga di comando esistono due meccanismi per ottenere questo risultato: un file di override che Compose carica automaticamente e il flag -f, specificato manualmente. Un terzo meccanismo è incluso nel file stesso, 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. È questa differenza a causare i comportamenti inattesi; l'elenco ports è quello che crea problemi quasi a tutti.

Tutto ciò che segue presuppone Compose v2 o versioni successive, con il plugin docker compose invece dello script docker-compose precedente. Le versioni attuali sono numerate v5 e il plugin funziona allo stesso modo. 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 che Compose carica automaticamente

Esegui docker compose up senza il flag -f: Compose cerca nella directory di lavoro e poi nelle directory superiori 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 -d

Se entrambi i file sono presenti, il risultato è lo stesso che si ottiene specificandoli manualmente.

docker compose -f compose.yaml -f compose.override.yaml up -d

I 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 soltanto 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 il modello dev e prod descritto più avanti in questa guida.

Su un server, questo comportamento può avere conseguenze positive o negative. 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 cron job. In questo modo uno stack di produzione può finire per montare con bind una directory sorgente che non era destinata alla distribuzione. Esegui docker compose config dopo ogni deploy e leggi l'output. Se il deploy è non interattivo, questo controllo è utile soltanto se qualcuno viene informato dell'errore. A questo serve un canale push come un server ntfy self-hosted, a cui un cron job o un'unità systemd OnFailure può inviare un messaggio.

Ordine con -f e risoluzione dei percorsi relativi

Compose crea la configurazione nell’ordine in cui fornisci i file. I file successivi sovrascrivono ed estendono 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 -d

Ogni comando eseguito in quel progetto deve usare lo stesso elenco di file. Esegui up con due file e logs con uno solo: stai operando su un modello unificato diverso. È un modo rapido per ottenere un servizio che Compose dichiara inesistente. Il rischio aumenta negli stack i cui aggiornamenti vengono eseguiti con comandi una tantum, come il passaggio di migrazione del database in un help desk Chatwoot self-hosted, dove un comando docker compose run eseguito con l’elenco di file errato opera silenziosamente su un modello diverso da quello già usato dai servizi. 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 -d

Su Linux, il separatore è :, mentre COMPOSE_PATH_SEPARATOR lo modifica. COMPOSE_FILE può essere inserito anche nel file .env del progetto, così 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.

Ora 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 dentro deploy/prod/compose.prod.yaml e Compose cercherà comunque ./data accanto al file di base. Docker crea quindi una directory vuota nel percorso errato e il container si avvia senza alcun contenuto. Questo sembra una perdita di dati, ma non lo è. Passa --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 compare per primo può quindi rinominare il progetto. Un progetto rinominato implica nuovi nomi per container e volumi; il volume precedente resta sul disco con il vecchio nome. Imposta invece in modo esplicito un name: di primo livello nel file di base.

name: myapp

Quali 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, entrypoint e mem_limit assumono direttamente il valore successivo. Non è possibile aggiungere un argomento a command, perché l’override riscrive l’intera riga.
  • Le mappe vengono unite chiave per chiave. environment, labels, volumes e devices conservano tutte le chiavi presenti in entrambi i file; per le chiavi presenti in entrambi, prevale il valore del file successivo. Per environment e labels la chiave è il nome della variabile o dell’etichetta. Per volumes e devices la chiave è il percorso nel container.
  • Le sequenze vengono concatenate. dns, dns_search, expose, tmpfs e external_links vengono concatenate. Una configurazione di base contenente expose: ["3000"], unita a un override contenente ["4000", "5000"], produce ["3000", "4000", "5000"].

Quattro sequenze usano una chiave di identità. Le voci che hanno la stessa chiave vengono quindi unite invece di essere accodate. volumes, secrets e configs fanno il confronto in base a target. ports usa la combinazione di ip, target, published e protocol.

Leggi due volte la regola ports, perché è il punto più insidioso. Due voci relative alle porte sono la stessa voce solo quando tutte e quattro le parti coincidono. Se ne modifichi anche una sola, Compose considera la voce una seconda porta indipendente e conserva entrambe.

Perché la porta è ancora pubblicata dopo l’override

Un file di base che pubblica un servizio su tutte le interfacce:

services:
  web:
    image: nginx:1.27
    ports:
      - "8080:80"

Un override scritto per associarlo solo a localhost, perché davanti al servizio verrà configurato un reverse proxy:

services:
  web:
    ports:
      - "127.0.0.1:8080:80"

Controllate il risultato prima di presumere che la modifica sia stata applicata.

docker compose -f compose.yaml -f compose.prod.yaml config

Entrambe le voci sono presenti nell’output. La parte ip è diversa: 0.0.0.0 rispetto a 127.0.0.1. Per il merge si tratta quindi di due porte diverse e il binding pubblico che si è tentato di rimuovere è ancora presente nel modello. Questo aspetto è più importante in Docker che in altri contesti, perché una porta pubblicata viene inserita in iptables prima delle regole del firewall. Il meccanismo è descritto in perché le porte Docker pubblicate bypassano ufw.

Esistono due soluzioni. 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 versioni successive. La soluzione portabile non richiede alcun tag: basta lasciare ports completamente fuori dal file di base e dichiararlo soltanto nei file specifici dell’ambiente. Se non c’è nulla da unire, non c’è nulla da esporre. Questo è il modello usato nell’esempio completo riportato di seguito.

Eliminare un valore impostato dal file di base

!reset rimuove un attributo, ripristinandolo al valore predefinito oppure a null. Richiede un valore, ma lo ignora: specificare quindi un valore valido e vuoto.

services:
  web:
    ports: !reset []
    environment:
      DEBUG: !reset null

!reset è disponibile in Compose v2.18.0 o versioni successive, diverse versioni prima dell'introduzione di !override. Usarlo quando non è possibile modificare il file di base, ad esempio nel caso di un frammento fornito da un vendor. Uno stack upstream pubblicato rientra esattamente in questo caso: il file Compose alla base di un workspace AFFiNE self-hosted dichiara quattro container che non sono stati scritti dall'utente e !reset consente di azzerare un attributo di uno di questi senza creare un fork del file e doversi occupare di mantenerlo aggiornato.

include, per stack assemblati a partire da componenti

include importa un'altra applicazione Compose nel modello. È un elemento di primo livello, non un flag.

include:
  - path: ../commons/compose.yaml

Ogni percorso in include viene caricato come modello di applicazione Compose distinto, con una propria directory di progetto. Di conseguenza, i percorsi relativi contenuti in quel file vengono risolti rispetto alla directory del file stesso. Questa è la differenza sostanziale 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. Questa è la struttura tipica di uno stack del fornitore che non hai scritto tu: il file Compose multi-servizio alla base di un'installazione self-hosted di Authentik SSO può trovarsi nella propria directory, mantenendo invariati i propri percorsi relativi, mentre il tuo file continua a riguardare soltanto i tuoi servizi.

La sintassi estesa accetta alcune opzioni secondarie.

include:
  - path:
      - ../monitoring/compose.yaml
      - ../monitoring/compose.vps.yaml
    project_directory: ../monitoring
    env_file: ../monitoring/.env

path accetta un elenco e quei file vengono uniti secondo le regole normali prima che il risultato venga aggiunto al tuo modello. project_directory imposta il percorso di base usato per risolvere i percorsi relativi nel file incluso. env_file indica il file dal quale il file Compose incluso ricava i valori predefiniti per l'interpolazione. Per impostazione predefinita, usa già il .env nella directory di progetto del file incluso. L'ambiente del progetto locale continua ad avere la precedenza, permettendoti di sovrascrivere una delle sue variabili senza modificare il frammento. include richiede Compose v2.20.0 o versioni successive. Le stesse opzioni sono adatte a un add-on con un singolo container aggiunto a uno stack già in esecuzione, ad esempio Halcyon, che trasforma l'interfaccia di una libreria Jellyfin in quella di un videonoleggio degli anni 90: il suo file mantiene il proprio tag dell'immagine e il proprio env_file, quindi aggiornarlo non richiede mai di modificare il file in cui risiede il tuo stack multimediale.

La documentazione di Docker afferma che il nome di una risorsa dichiarato sia nel tuo file sia in un file incluso viene rifiutato come conflitto. Il loader della release attuale non si comporta così: in Compose v5.6.1, verificato sul codice sorgente del loader distribuito con quella release il 2026-10-27, le due definizioni vengono unite. In caso di valori in conflitto, prevale il tuo file, invece di interrompere l'operazione con un errore. Non fare quindi affidamento sul fatto che un nome duplicato produca un errore esplicito. Per modificare un elemento dichiarato dal file incluso, inserisci la modifica in compose.override.yaml: l'override viene applicato al modello assemblato, quindi la modifica è esplicita e non dipende dal modo in cui una determinata release gestisce il conflitto. Questa pratica è particolarmente utile con uno stack il cui file upstream viene riscritto a ogni release, come i server fotografici multi-container analizzati in PhotoPrism e Immich a confronto, dove un binding su localhost o un volume aggiuntivo deve trovarsi nel tuo override, non nel file che il prossimo aggiornamento sostituirà.

In sintesi: include compone applicazioni separate, mentre -f applica una configurazione aggiuntiva a una singola applicazione.

Separare sviluppo e produzione su un unico VPS

Ecco il modello completo 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; il funzionamento è spiegato in healthcheck e condizioni depends_on. POSTGRES_PASSWORD viene interpolato dal file di progetto .env, che non deve mai essere inserito in git. Per le varianti più sicure, vedere file env e secret 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 unisce i valori in base alla chiave. Il bind mount e le due porte pubblicate sono aggiunte pure. La porta del database è associata a localhost, così un laptop connesso a una rete condivisa non espone PostgreSQL agli altri dispositivi.

Infine, compose.prod.yaml. Il suo nome non corrisponde a uno dei nomi cercati da Compose, quindi non viene mai caricato per errore.

services:
  app:
    ports:
      - "127.0.0.1:8000:3000"
    deploy:
      resources:
        limits:
          memory: 512M

Sul VPS specifichi entrambi i file. Questa scelta esclude esattamente 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 ps

ps dovrebbe elencare entrambi i servizi come in esecuzione, con db che mostra (healthy). Poiché hai 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. Quando aggiungi il secondo servizio, vedere eseguire più applicazioni dietro Traefik.

Imposta COMPOSE_FILE=compose.yaml:compose.prod.yaml nel file .env del server. Da quel momento, gli altri comandi tornano a essere semplici docker compose logs -f app.

Anche uno stack con un solo servizio mantiene la stessa struttura, perché un tracker di allenamenti self-hosted openGym deve rispondere tramite TLS dietro un proxy prima che tu registri la prima passkey. Un file di base senza ports impedisce a un'associazione pubblica imprevista di precedere il proxy.

Leggi il modello unito prima del deploy

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 a ciò che ti aspetti, è corretto l'output.

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 espanso. Usalo prima di incollare l'output in qualsiasi posizione, perché il semplice config stampa ogni secret risolto in chiaro. --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 relativi riscontri

no configuration file provided: not found. Compose non ha trovato alcun file da leggere. Ti trovi fuori dalla directory del progetto oppure COMPOSE_FILE indica un percorso inesistente. Compose cerca il file di base predefinito nelle directory padre, ma non cerca ovunque un file specificato manualmente.

WARN[0000] The "POSTGRES_PASSWORD" variable is not set. Defaulting to a blank string. L'interpolazione viene risolta rispetto al file .env del progetto e all'ambiente della shell. In questo caso, la directory del progetto è quella del primo file -f. Eseguire il deployment da una directory diversa da quella che contiene .env genera questo avviso e lascia un database che rifiuta ogni connessione.

La modifica al file override non compare in docker compose config. Hai passato -f, che disabilita il caricamento automatico dei file override, oppure Compose ha trovato compose.yaml in una directory padre e il file override non si trova nella stessa directory. Eseguire docker compose config senza altri argomenti mostra il modello che Compose sta effettivamente creando.

Un bind mount è vuoto e Docker ha creato una directory che non avevi richiesto. Il percorso relativo è stato risolto rispetto alla directory del primo file. Correggi il percorso, passa --project-directory oppure sposta il frammento dietro include.

I container vengono ricreati con nomi nuovi e un volume appare vuoto. Il nome del progetto è cambiato, perché deriva dalla directory del primo file. Aggiungi name: a livello superiore nel file di base per impedire che i nomi cambino. Il vecchio volume esiste ancora con il vecchio prefisso e docker volume ls lo mostrerà.

Una porta rimossa nel file override è ancora aperta. L'unione 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 si esegue docker compose senza il flag -f. Compose cerca nella directory di lavoro e nelle relative directory padre compose.yaml o docker-compose.yaml. Se accanto trova un file di override, 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 vengono forniti i file. Ogni file sostituisce e integra quelli precedenti, quindi l'ultimo file nella riga di comando prevale in caso di conflitto. Lo stesso elenco deve essere usato per ogni comando del progetto. È questo lo scopo di 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 configurazione di base 8080:80 differisce nella parte ip. Compose lo tratta quindi come una seconda porta e mantiene entrambe. Eseguendo docker compose config verranno visualizzate le due voci. Usare ports: !override con Compose v2.24.4 o versioni successive, oppure non inserire ports nel file di base, così non ci sarà alcun valore 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 mantiene la propria directory del progetto, quindi i relativi percorsi vengono risolti rispetto a quella directory. Usare -f per i livelli di ambiente del proprio stack e include per un frammento gestito altrove. include richiede Compose v2.20.0 o versioni successive.

Come si rimuove un valore impostato dal file di base?

Usare il tag !reset con Compose v2.18.0 o versioni successive. Nel file di override, scrivere ports: !reset [] oppure MY_VAR: !reset null. L'attributo tornerà al valore predefinito o a null. Il valore fornito al tag è obbligatorio, ma viene ignorato. Per sostituire un attributo invece di cancellarlo, usare !override. Questa funzione richiede v2.24.4 o versioni successive.

Questo articolo ti è stato utile?