SSD Nodes Learn Hosting plans →
Guide Matt ConnorDi Matt Connor · Aggiornato 2026-08-28

Docker Compose: più file, override e include

Scopri come Compose carica 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ù file

Docker Compose può creare un unico progetto a partire da più file. Li legge nell'ordine in cui li riceve e li unisce in un unico modello; perciò, per ogni valore in conflitto, prevale il file successivo. Dalla riga di comando esistono due meccanismi: un file di override che Compose carica automaticamente e il flag -f passato manualmente. Il terzo meccanismo è interno al file: l'elemento include, che 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 insieme limitato di campi viene sostituito completamente. Le differenze causano i comportamenti inattesi; l'elenco ports è quello che crea problemi quasi a tutti.

Tutto quanto segue presuppone Compose v2, il plugin docker compose, non il vecchio script docker-compose. Esegui docker compose version per verificare la versione. Se non hai ancora scritto un file Compose, inizia dalla guida di base a Docker Compose e poi torna qui.

Il file di override caricato automaticamente da Compose

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 carica automaticamente anche quello.

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

Con entrambi i file presenti, il risultato è lo stesso che si otterrebbe 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 solo se lo specifichi con -f.

Quando specifichi anche un solo -f, il caricamento automatico si interrompe. docker compose -f compose.yaml up legge esattamente quel file e ignora l'override. È questa la caratteristica su cui si basa il modello dev e prod descritto più avanti in questa guida.

Su un server, questo comportamento può avere conseguenze in entrambi i sensi. Un file di override lasciato nella directory di deploy viene caricato da ogni comando docker compose eseguito senza argomenti espliciti da quella directory, compreso quello eseguito dal job cron. È così che uno stack di produzione finisce per montare tramite bind una directory sorgente che nessuno intendeva distribuire. Esegui docker compose config dopo ogni deploy e controlla l’output. Quando il deploy è non presidiato, il controllo è utile soltanto se qualcuno viene informato dell’errore. A questo serve un canale push come un server ntfy self-hosted, al quale un job cron o un’unità systemd OnFailure può inviare una notifica.

Ordinamento con -f e risoluzione dei percorsi relativi

Compose crea la configurazione nell'ordine in cui fornisci i file. I file successivi sovrascrivono e integrano quelli precedenti. Da sinistra a destra, prevale l'ultimo file.

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

Ogni comando del progetto deve usare lo stesso elenco di file. Esegui up con due file e logs con uno solo: stai usando un modello unito diverso. È un modo rapido per ottenere un servizio che Compose segnala come inesistente. Il problema è ancora più rilevante per uno stack i cui aggiornamenti vengono eseguiti con comandi una tantum, come il passaggio di migrazione del database in un help desk self-hosted Chatwoot, dove un docker compose run eseguito con l'elenco di file errato utilizza in modo silenzioso 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 è : e COMPOSE_PATH_SEPARATOR lo modifica. COMPOSE_FILE può anche essere presente nel file di progetto .env, così l'impostazione resta nel checkout e non nella 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 presenti in questi 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 cercherà comunque ./data accanto al file di base. Docker crea quindi una directory vuota nel percorso errato e il container si avvia senza contenuti al suo interno. Questo può sembrare 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 viene indicato per primo può quindi rinominare il progetto. Un progetto rinominato implica nuovi nomi per container e volumi. Il vecchio volume resta sul disco con il nome precedente. Imposta invece un nome fisso con name: al livello superiore 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 una chiave presente 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 accodate. 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 hanno una chiave di identità, quindi le voci che coincidono per quella chiave vengono unite invece di essere accodate. volumes, secrets e configs coincidono in base a target. ports coincide in base alla combinazione di ip, target, published e protocol.

Leggi due volte la regola ports, perché è il punto più insidioso. Due voci relative alle porte rappresentano la stessa voce solo quando tutte e quattro le parti coincidono. Se ne modifichi anche solo una, Compose considera la seconda voce 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 soltanto a localhost, perché davanti al servizio verrà collocato un reverse proxy:

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

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

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

Entrambe le voci compaiono nell'output. La parte ip è diversa: 0.0.0.0 rispetto a 127.0.0.1. Per il merge sono quindi due porte diverse e l'associazione pubblica 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: è sufficiente 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 accidentalmente. Questo è il modello usato nell'esempio completo riportato di seguito.

Eliminare un valore definito nel file di base

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

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

!reset richiede Compose v2.24 o versioni successive. Usarlo quando non è possibile modificare il file di base, ad esempio quando si integra un frammento fornito da un vendor. Uno stack upstream pubblicato è esattamente 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 cancellare un attributo di uno di questi senza creare un fork del file e doversi occupare di mantenerlo aggiornato.

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.yaml

Ogni percorso in include viene caricato come modello di un'applicazione Compose distinta, con una propria directory del progetto. I percorsi relativi contenuti in quel file vengono quindi 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 directory o in un altro repository. È la struttura tipica di uno stack fornito da un vendor e non scritto da te: il file Compose multi-servizio alla base di un'installazione self-hosted di Authentik SSO può rimanere nella propria directory, mantenendo invariati i relativi percorsi, mentre il tuo file continua a descrivere i tuoi servizi.

La sintassi estesa accetta alcune sotto-opzioni.

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

path accetta un elenco e unisce questi file applicando le regole normali prima di integrare il risultato nel tuo 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 in modo implicito il .env del tuo progetto. include richiede Compose v2.20.0 o versioni successive. Le stesse opzioni sono adatte a un componente aggiuntivo con un solo container per uno stack già in esecuzione, ad esempio Halcyon, che trasforma l'aspetto di una libreria Jellyfin in quello di un videonoleggio degli anni 90: il relativo file mantiene il proprio tag dell'immagine e il proprio env_file, quindi aggiornarlo non richiede mai di modificare il file usato dal tuo stack multimediale.

I nomi duplicati delle risorse presenti nel tuo file e in un file incluso vengono segnalati come errore invece di essere uniti in modo silenzioso, e questo comportamento è intenzionale. Per modificare una risorsa dichiarata in un file incluso, inserisci la modifica in compose.override.yaml: l'override viene applicato al modello assemblato, quindi può intervenire sulle risorse incluse senza creare conflitti con esse. Questa pratica è particolarmente utile con gli stack il cui file upstream viene riscritto a ogni release, come i server fotografici multi-container confrontati in PhotoPrism e Immich, dove un binding su localhost o un volume aggiuntivo deve essere definito nell'override e non nel file che il prossimo aggiornamento sostituirà.

In breve: include compone applicazioni separate, mentre -f applica livelli di configurazione a un'unica applicazione.

Separare sviluppo e produzione su un singolo 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 comportamento è spiegato in healthcheck e condizioni di 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 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 gli elementi in base alla chiave. Il bind mount e le due porte pubblicate sono aggiunte pure, mentre la porta del database è associata a localhost. In questo modo, un laptop collegato a una rete condivisa non espone PostgreSQL agli altri dispositivi.

Infine, compose.prod.yaml. Il suo nome non è tra quelli cercati da Compose, quindi non viene mai caricato accidentalmente.

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

Sul VPS si specificano entrambi i file. Proprio questa indicazione esclude 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 mostrare entrambi i servizi in esecuzione, con db che visualizza (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 soltanto 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 .env del server. Da quel momento, gli altri comandi possono tornare a essere semplici docker compose logs -f app.

Anche uno stack con un solo servizio segue la stessa struttura, perché un tracker di allenamento self-hosted openGym deve rispondere tramite TLS dietro un proxy prima che tu registri la prima passkey. Un file di base privo di ports impedisce a un binding pubblico accidentale 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 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 espanso. Usalo prima di incollare l'output in altri contesti, perché config semplice 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 risultati osservabili

no configuration file provided: not found. Compose non ha trovato nulla da leggere. Non ci si trova nella directory del progetto oppure COMPOSE_FILE indica un percorso inesistente. Compose cerca nelle directory padre il file di base predefinito, 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 in base al file .env del progetto e all'ambiente della shell. La directory del progetto, in questo caso, è quella del primo file -f. Se si esegue il deployment da una directory diversa da quella che contiene .env, viene visualizzato questo avviso e il database rifiuta quindi ogni connessione.

La modifica all'override non compare in docker compose config. È stato passato -f, che disabilita il caricamento automatico dell'override, oppure Compose ha trovato compose.yaml in una directory padre e il file di override non si trova accanto a questo file. L'esecuzione di docker compose config senza altri argomenti mostra il modello che Compose sta realmente compilando.

Un bind mount è vuoto e Docker ha creato una directory non richiesta. Il percorso relativo è stato risolto rispetto alla directory del primo file. Correggere il percorso, passare --project-directory oppure spostare il frammento dopo include.

I container vengono ricreati con nuovi nomi e un volume risulta vuoto. Il nome del progetto è cambiato perché deriva dalla directory del primo file. Aggiungere un campo name: di primo livello al file di base per mantenere stabile la denominazione. Il vecchio volume esiste ancora con il vecchio prefisso e docker volume ls lo mostrerà.

Una porta rimossa nell'override è ancora aperta. L'unione ports ha aggiunto il valore invece di sostituirlo. Verificare con docker compose config, quindi usare !override oppure spostare 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 superiori compose.yaml o docker-compose.yaml e, 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 costruisce la configurazione nell'ordine in cui vengono forniti i file. Ogni file esegue l'override dei file precedenti e vi aggiunge elementi, quindi in caso di conflitto prevale l'ultimo file della riga di comando. Lo stesso elenco deve essere usato per ogni comando del progetto; a questo serve COMPOSE_FILE=compose.yaml:compose.prod.yaml.

Perché la 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 con 8080:80 differisce nella parte ip. Compose lo tratta quindi come una seconda porta e mantiene entrambe. Esegui docker compose config per visualizzare le due voci. Usa ports: !override con Compose v2.24.4 o versioni successive, oppure non inserire ports nel file di base, così non ci sarà nulla da unire.

Qual è la differenza tra include e -f?

-f sovrappone più file a una singola applicazione e tutti i percorsi relativi presenti nei file vengono risolti rispetto alla directory del primo file. include incorpora un'applicazione Compose separata e ogni percorso incluso mantiene la propria directory del progetto; i relativi percorsi vengono quindi 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 versioni successive.

Come posso rimuovere un valore impostato dal file di base?

Usa il tag !reset con Compose v2.24 o versioni successive. Nel file di override, scrivi ports: !reset [] o MY_VAR: !reset null: l'attributo viene ripristinato al valore predefinito o a null. Il valore assegnato al tag è obbligatorio, ma viene ignorato. Se vuoi sostituire un attributo invece di cancellarlo, !override esegue questa operazione e richiede la versione v2.24.4 o successive.