SSD Nodes Learn 🎉 VPS da $5.50/mese
Guide Matt ConnorDi Matt Connor · Aggiornato 2026-08-13

Docker Compose: differenza tra command ed entrypoint

ENTRYPOINT avvia il programma e command fornisce gli argomenti. Scopri le 4 combinazioni di override in Compose e perché entrypoint cancella CMD.

Comando e entrypoint in Docker Compose: una sola regola

In Docker Compose, entrypoint: imposta il programma da eseguire e command: imposta gli argomenti da passare a quel programma. Il processo del container è costituito dall'elenco dell'entrypoint seguito dall'elenco del comando. Ogni altro comportamento descritto in questa pagina deriva da questa regola.

Queste due chiavi corrispondono a due istruzioni Dockerfile. entrypoint: sostituisce ENTRYPOINT dell'immagine. command: sostituisce CMD dell'immagine. Non sono indipendenti. È questo il punto che crea più difficoltà: impostando entrypoint: si elimina anche CMD dell'immagine. La specifica di Compose lo dichiara esplicitamente. Se entrypoint è diverso da null, Compose ignora il comando predefinito dell'immagine.

Leggi ciò che l'immagine dichiara già

Prima di sovrascrivere qualsiasi impostazione, verifica cosa include l'immagine.

docker image inspect --format '{{json .Config.Entrypoint}}' postgres:16
docker image inspect --format '{{json .Config.Cmd}}' postgres:16

Ottieni ["docker-entrypoint.sh"] e ["postgres"], quindi il container esegue docker-entrypoint.sh postgres. Questo script crea la directory dei dati al primo avvio, legge le variabili POSTGRES_*, abbandona i privilegi passando all'utente postgres e infine esegue gli argomenti ricevuti. La decisione dipende interamente da quale parte vuoi modificare. Per passare un flag al database, sostituisci command:. Se sostituisci entrypoint:, tutta questa procedura di configurazione non viene eseguita.

Le quattro combinazioni, mostrate in una piccola immagine

Crea un'immagine il cui unico compito sia stampare l'elenco degli argomenti con cui è stata avviata.

FROM alpine:3.20
ENTRYPOINT ["/bin/echo", "ep"]
CMD ["cmd"]
docker build -t argdemo .
services:
  demo:
    image: argdemo

Esegui docker compose up dopo ogni modifica e leggi l'unica riga che registra.

  • Nessuna chiave impostata. Il processo è /bin/echo ep cmd e il log mostra ep cmd.
  • Solo command: ["cmd2"]. Il processo è /bin/echo ep cmd2. L'entrypoint non viene modificato; cambiano solo gli argomenti.
  • Solo entrypoint: ["/bin/echo", "ep2"]. Il processo è /bin/echo ep2 e il log mostra ep2. cmd dell'immagine scompare e non viene visualizzato alcun avviso.
  • Entrambe le chiavi impostate. Il processo è /bin/echo ep2 cmd2. Questo è l'unico caso in cui controlli l'intero elenco degli argomenti.

Perché impostare CMD cancella ENTRYPOINT dell'immagine

Il CMD di un'immagine viene scritto come elenco di argomenti predefinito per il ENTRYPOINT della stessa immagine. Se sostituisci l'entrypoint, quegli argomenti appartengono ora a un programma che non viene più eseguito. Compose li elimina invece di creare una riga di comando che l'autore dell'immagine non aveva mai previsto. docker run --entrypoint si comporta allo stesso modo. Si tratta quindi del comportamento di Docker, non di una particolarità di Compose.

La conseguenza è concreta. nginx:1.27 dichiara ENTRYPOINT ["/docker-entrypoint.sh"] e CMD ["nginx", "-g", "daemon off;"]. Imposta entrypoint: /custom-init.sh e lo script viene avviato con un elenco di argomenti vuoto. Uno script che termina con il consueto exec "$@" non ha quindi nulla da eseguire. exec non esegue alcuna operazione, lo script raggiunge l'ultima riga e il container termina con codice 0, senza messaggi di errore. Ripristina manualmente gli argomenti:

services:
  web:
    image: nginx:1.27
    entrypoint: /custom-init.sh
    command: ["nginx", "-g", "daemon off;"]

La regola da ricordare è questa: ogni volta che imposti entrypoint:, decidi nello stesso aggiornamento quale valore deve avere command:.

Sintassi exec e sintassi shell, e differenze rispetto a Compose

Un Dockerfile accetta due sintassi. CMD ["nginx", "-g", "daemon off;"] è la sintassi exec: il binario viene eseguito direttamente, senza usare una shell. CMD nginx -g "daemon off;" è la sintassi shell: Docker la riscrive come /bin/sh -c 'nginx -g "daemon off;"', quindi prima viene avviata una shell e il programma diventa un processo figlio.

Compose non applica la stessa regola, e questo può sorprendere. Una stringa in command: viene suddivisa in argomenti ed eseguita direttamente, senza un wrapper /bin/sh -c. La documentazione di riferimento di Compose lo specifica chiaramente: il campo command non viene eseguito nel contesto SHELL definito nell'immagine. Se servono funzionalità della shell, è quindi necessario invocarla esplicitamente.

Per questo command: echo "hello $$HOSTNAME" stampa il testo letterale hello $HOSTNAME. Nessuna shell ha interpretato la stringa, quindi nulla l'ha espansa. Se serve una shell, invocatela esplicitamente:

services:
  demo:
    image: alpine:3.20
    command: /bin/sh -c 'echo "hello $$HOSTNAME"'

Segnali, PID 1 e un arresto pulito con docker compose down

docker compose stop e docker compose down inviano SIGTERM a PID 1 all'interno di ogni container, attendono stop_grace_period, quindi inviano SIGKILL. Il periodo di tolleranza predefinito è di 10 secondi.

PID 1 è speciale in Linux. Il kernel non applica l'azione predefinita di un segnale a PID 1. Di conseguenza, un processo che non installa alcun gestore SIGTERM ignora semplicemente SIGTERM quando viene eseguito come PID 1. Rimane in esecuzione per l'intero periodo di tolleranza e poi viene terminato immediatamente. Questo interrompe qualsiasi connessione aperta o transazione non sottoposta a commit.

La presenza di una shell davanti al programma aumenta la probabilità che si verifichi questo problema, perché la shell è PID 1 e la maggior parte delle shell non inoltra i segnali a un processo figlio. Alcune shell sostituiscono se stesse con il comando finale contenuto in una stringa -c. In questi casi, talvolta il programma raggiunge comunque PID 1. Il comportamento dipende dalla shell e dalla stringa esatta, quindi non fare supposizioni. Verificalo:

docker compose exec -T web cat /proc/1/cmdline | tr '\0' ' '; echo

Se PID 1 viene visualizzato come /bin/sh -c ... invece che come il programma, esistono due soluzioni. Usa la forma exec nell'immagine oppure mantieni la shell e trasferisci il processo con exec:

services:
  web:
    image: myapp:1.4
    command: /bin/sh -c 'exec myapp --config /etc/myapp.toml'

exec sostituisce il processo della shell con il programma invece di creare un processo figlio. Il programma eredita quindi PID 1 e riceve il segnale.

Alcuni programmi creano processi figli e non li raccolgono mai. Questo lascia processi zombie, perché PID 1 svolge anche il ruolo di processo di raccolta. Compose dispone di un'opzione specifica:

services:
  web:
    image: myapp:1.4
    init: true
    stop_grace_period: 30s

init: true esegue un processo init leggero come PID 1. Questo processo inoltra i segnali al programma e raccoglie i processi figli. stop_grace_period concede più tempo a un arresto effettivamente lento. Se il programma si aspetta un segnale diverso, stop_signal: SIGQUIT modifica il segnale inviato da Compose. Per verificare cosa richiede già un'immagine, usa docker image inspect --format '{{.Config.StopSignal}}' nginx:1.27.

Se docker compose down impiega sempre dieci secondi per ogni servizio, significa che nulla gestisce SIGTERM. Risolvi il problema prima di attribuirlo agli strumenti. Consulta anche la differenza tra docker compose down e stop per sapere cosa rimuove ciascun sottocomando.

La stessa distinzione tra exec e shell ricorre in un altro punto. Un healthcheck scritto come test: ["CMD", "curl", "-f", "http://localhost/"] esegue direttamente il file binario, mentre test: ["CMD-SHELL", "curl -f http://localhost/ || exit 1"] viene eseguito tramite una shell, affinché || abbia effetto. Scrivere healthcheck Compose che rilevano correttamente i problemi illustra il resto di quel campo.

Aggiungere un flag a un'immagine ufficiale

Questo è il motivo principale per cui molti lettori sono arrivati qui. Vuoi aggiungere un flag a postgres senza interferire con lo script di inizializzazione.

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data
    command: postgres -c max_connections=200 -c shared_buffers=256MB

volumes:
  pgdata:

È cambiato solo command:, quindi docker-entrypoint.sh continua a essere eseguito e continua a eseguire ciò che gli hai assegnato. Verifica il risultato invece di darlo per scontato:

docker compose up -d db
docker compose exec -T db psql -U postgres -c 'show max_connections;'

L'output dovrebbe mostrare 200. Se mostra ancora 100, esegui docker compose config e verifica che command previsto sia presente nell'output risultante. Compose unisce i file di override sostituendo command completamente, non aggiungendovi contenuto. Di conseguenza, un secondo file che imposta anch'esso command: prevale senza generare errori.

Il ${POSTGRES_PASSWORD} precedente viene espanso da Compose sull'host a partire dal file .env, prima che il container venga creato. File di ambiente e secret in Compose spiega dove conservare questo valore in modo sicuro.

Eseguire una migrazione una tantum con docker compose run

docker compose run crea un nuovo container dalla stessa definizione del servizio e sostituisce il comando con quello specificato dopo il nome del servizio. Viene comunque eseguito l'entrypoint dell'immagine, quindi il container viene preparato esattamente come quello a esecuzione continua.

docker compose run --rm app python manage.py migrate
  • --rm elimina il container al termine del comando. Senza questa opzione, ogni esecuzione lascia un container arrestato, visibile in docker compose ps -a.
  • Le porte non vengono pubblicate. Un container run ignora ports: del servizio, a meno che non si aggiunga --service-ports, quindi non può entrare in conflitto con il servizio già attivo.
  • Le dipendenze vengono avviate prima. Tutto ciò che è definito in depends_on viene avviato prima del comando, mentre --no-deps disabilita questo comportamento.
  • Al container viene assegnato un nome generato, ad esempio myproject-app-run-9f2c1a, quindi non può entrare in conflitto con il container del servizio.

Per sostituire anche l'entrypoint, è disponibile un'opzione specifica:

docker compose run --rm --entrypoint /bin/sh app -c 'python manage.py migrate'

L'elenco degli argomenti risultante è /bin/sh -c 'python manage.py migrate', perché le parole dopo il nome del servizio restano il comando. docker compose exec è l'altro strumento e funziona in modo diverso: esegue un processo all'interno di un container già attivo e ignora completamente entrypoint: e command:. Usare run per un'attività che richiede un container nuovo e exec per esaminare un container in esecuzione. Il promemoria dei comandi Compose mette a confronto gli altri sottocomandi.

Perché il mio container termina immediatamente?

Inizia dal codice di uscita, perché consente di restringere rapidamente la causa.

docker compose ps -a
docker compose logs app

Codice di uscita 0 e nessun output. Il comando è stato eseguito e completato. La causa più comune è un override entrypoint: che ha sostituito anche il CMD dell'immagine. Di conseguenza, l'entrypoint è stato eseguito con un elenco di argomenti vuoto e non aveva nulla da passare al processo successivo.

Un errore che termina con permission denied. Lo script non dispone del bit di esecuzione nell'immagine. Di solito il bit non è mai stato impostato sul file nel repository. Impostalo durante la build con COPY --chmod=0755 entrypoint.sh /entrypoint.sh.

Un errore che termina con no such file or directory per un file chiaramente presente nell'immagine. Lo script usa terminatori di riga Windows. La prima riga viene quindi letta come #!/bin/sh più un byte di ritorno a capo. Il kernel cerca un interprete il cui nome contenga quel byte e non lo trova. Esegui dos2unix entrypoint.sh, quindi aggiungi * text eol=lf a .gitattributes per impedire che il problema si ripresenti.

executable file not found in $PATH. Il binario indicato in command: non è presente nell'immagine oppure hai scritto un costrutto interno della shell, come cd, in un punto in cui è richiesto un programma reale.

Ottenere una shell in un'immagine il cui entrypoint non si avvia

Quando l'entrypoint termina prima che tu possa eseguire controlli, sostituiscilo:

docker compose run --rm --entrypoint /bin/sh app

Se restituisce executable file not found in $PATH, l'immagine non contiene alcuna shell. Le immagini Distroless e quelle basate su scratch spesso non ne includono una. Puoi comunque leggere il filesystem dall'esterno senza avviare l'entrypoint:

docker create --name probe myapp:1.4
docker export probe | tar -tv | head -40
docker rm probe

Quando devi mantenere il container in esecuzione per poterti collegare più volte, esegui un processo che non termina. Inserisci questa configurazione in un file di override che non devi sottoporre a commit:

services:
  app:
    entrypoint: ["tail", "-f", "/dev/null"]
    command: []

command: [] non è strettamente necessario, perché l'impostazione di entrypoint: ha già cancellato il CMD dell'immagine, ma scriverlo registra l'intento per chi leggerà il file in seguito. Avvialo e accedi alla shell:

docker compose -f compose.yaml -f compose.debug.yaml up -d app
docker compose exec app /bin/sh

Ora esegui manualmente l'entrypoint reale e osserva dove si interrompe. In questo modo visualizzi il messaggio di errore nel terminale, invece che in un container terminato mezzo secondo prima. Se stai ancora preparando il tuo primo stack, un primo stack Compose su un VPS descrive la struttura dei file presupposta da tutto quanto sopra.

FAQ

Perché il container termina subito dopo docker compose up?

Controllare docker compose ps -a per il codice di uscita. L'uscita 0 senza output indica in genere che è stato impostato entrypoint: sul servizio. Questa impostazione ha anche rimosso CMD dell'immagine, quindi l'entrypoint è stato eseguito con un elenco di argomenti vuoto e il processo è terminato. Aggiungere nuovamente gli argomenti con command:. Un errore che termina con permission denied indica che lo script dell'entrypoint non ha il bit di esecuzione. Un errore che termina con no such file or directory per un file esistente indica che lo script usa terminatori di riga Windows. Di conseguenza, la riga shebang specifica un interprete che non esiste nel container.

L'impostazione di entrypoint in Compose rimuove il CMD dell'immagine?

Sì. Se entrypoint non è null, Compose ignora il comando predefinito dichiarato dall'immagine. Questo comportamento è documentato e corrisponde a docker run --entrypoint. Il motivo è che CMD di un'immagine è scritto come elenco di argomenti per ENTRYPOINT di quella stessa immagine. Quando l'entrypoint viene sostituito, i vecchi argomenti non sono più associati ad alcun comando. Impostare command: nello stesso servizio se il nuovo entrypoint richiede ancora degli argomenti.

Una stringa in Compose command viene eseguita tramite una shell?

No. A differenza di CMD in un Dockerfile, una stringa in Compose command: viene suddivisa in argomenti ed eseguita direttamente, senza un wrapper /bin/sh -c. Di conseguenza, $VARIABLE non viene mai espansa da una shell all'interno del container. Quando serve una shell, invocarla esplicitamente, come in command: /bin/sh -c 'echo "hello $$HOSTNAME"'. Il doppio $$ esegue l'escape del simbolo del dollaro, così Compose lo passa al container invece di espanderlo sull'host.

Perché docker compose down impiega dieci secondi per un solo container?

Compose invia SIGTERM al PID 1, attende stop_grace_period (10 secondi per impostazione predefinita), quindi invia SIGKILL. Il kernel non applica le azioni predefinite dei segnali al PID 1. Un programma senza un gestore SIGTERM ignora quindi il segnale e attende sempre la scadenza completa dell'intervallo. Verificare quale processo è realmente il PID 1 con docker compose exec -T app cat /proc/1/cmdline | tr '\0' ' '. Se è una shell, modificare l'immagine usando la forma exec oppure inserire exec nella stringa della shell. Se il processo crea processi figli che non esegue mai il reap, impostare init: true sul servizio.