SSD Nodes Learn 🎉 VPS da $5.50/mese
Guide Matt ConnorDi Matt Connor

Docker Compose: aprire una shell interattiva nel servizio

Usa docker compose exec per aprire una shell in un servizio attivo. Se il servizio è fermo o non vuoi toccarlo, scegli docker compose run --rm.

Aprire una shell interattiva con docker compose exec

docker compose exec web bash apre una shell interattiva all'interno del container già in esecuzione come servizio web. Il nome dopo exec è il nome del servizio definito nel file compose.yaml, non il nome del container. Se l'immagine non include bash, richiedere sh.

docker compose ps
docker compose exec web bash

Eseguire prima docker compose ps. Dovrebbe elencare web con lo stato running. Il secondo comando apre quindi un prompt all'interno del container; exit o Ctrl-D consente di tornare all'host. Il servizio continua a essere in esecuzione dopo l'uscita, perché exec avvia un secondo processo accanto a quello principale. La chiusura della shell non modifica PID 1 (process ID 1), cioè il processo per cui il container è stato creato.

Questo è uno dei due modi per accedere al container. exec si collega a un container già esistente. docker compose run crea un nuovo container dalla stessa definizione del servizio. Quasi tutto il resto di questa guida deriva da questa singola differenza.

Perché -it è facoltativo in Compose ma obbligatorio con plain docker

Due flag controllano la parte interattiva di una sessione. -i mantiene aperto stdin, quindi ciò che si digita viene passato al processo. -t alloca un terminale pseudografico, chiamato TTY, in modo che la shell visualizzi il prompt e gestisca i tasti freccia. plain docker exec lascia entrambe le opzioni disabilitate per impostazione predefinita; per questo tutti gli esempi precedenti riportano docker exec -it. docker compose exec le abilita entrambe automaticamente, quindi docker compose exec -it web bash e docker compose exec web bash producono lo stesso risultato. Compose accetta comunque -it, così la consuetudine di usarlo continua a funzionare.

La mancanza di un TTY è evidente in pochi secondi. La shell viene eseguita, ma non visualizza alcun prompt e Ctrl-C non raggiunge mai il processo. Il caso opposto, in cui è necessario chiedere a Compose di non allocare un TTY, ha un flag specifico e una sezione dedicata più avanti.

Cosa fare quando l'immagine non include bash

Se si chiede bash a un'immagine basata su Alpine, exec fallisce in questo modo:

OCI runtime exec failed: exec failed: unable to start container process: exec: "bash": executable file not found in $PATH: unknown

Questo messaggio non indica un problema di exec. Indica che il binario richiesto non è presente nell'immagine. Alpine include BusyBox, che fornisce ash come /bin/sh e non include bash. È quindi necessario chiedere sh:

docker compose exec web sh

Le immagini basate su Debian e Ubuntu, inclusi i tag -slim, includono bash. bash offre la cronologia dei comandi e un completamento migliore. Provare quindi prima bash e usare sh come fallback. sh è presente in quasi tutte le immagini general purpose.

Alcune immagini non includono alcuna shell. Le immagini Distroless e quelle compilate FROM scratch contengono soltanto il binario dell'applicazione e le relative librerie. È una scelta intenzionale: una shell assente non può essere utilizzata contro l'applicazione. In queste immagini, sh fallisce con lo stesso messaggio e non resta alcun comando da provare. Sono disponibili due approcci. Le immagini Distroless di Google pubblicano tag :debug che aggiungono una shell BusyBox. È quindi possibile cambiare temporaneamente tag per accedere al container. In alternativa, avviare un container separato all'interno dei namespace del target:

CID=$(docker compose ps -q web)
docker run --rm -it --network "container:$CID" --pid "container:$CID" nicolaka/netshoot

A questo punto gli strumenti di netshoot operano sulla rete dell'applicazione. Di conseguenza, curl localhost:8080 e ss -lntp si comportano come se fossero eseguiti all'interno del container dell'applicazione. Il filesystem visualizzato appartiene a netshoot, non all'applicazione. Poiché il namespace dei processi è condiviso, ls /proc/1/root/ consente di accedere ai file del target quando si dispone dei privilegi di root.

Quando il servizio non è in esecuzione, usa docker compose run --rm

exec richiede un container in esecuzione. Se lo esegui su un servizio arrestato, restituisce un errore:

service "web" is not running

Non avvia nulla automaticamente. docker compose run invece:

docker compose run --rm web bash

run crea un nuovo container dalla definizione del servizio web, usando la stessa immagine, lo stesso ambiente, gli stessi volumi e le stesse reti, quindi sostituisce il comando del servizio con quello specificato. --rm elimina il container quando esci. Se ometti --rm, i container residui si accumulano con nomi come myproject-web-run-4f1c2b. docker compose ps -a li mostra, ma nessun altro comando li elimina automaticamente.

Due comportamenti di run possono sorprendere. Non pubblica le porte del servizio, a meno che tu non aggiunga --service-ports. Questo è intenzionale: un secondo container che tenta di associare la porta host 8080 mentre il primo la utilizza ancora fallirebbe con bind: address already in use. Inoltre, avvia tutto ciò che il servizio elenca sotto depends_on prima di mostrare la shell. Anche un controllo rapido può quindi avviare un database e una cache. --no-deps evita questo comportamento.

run passa attraverso l'ENTRYPOINT dell'immagine, mentre exec no. exec avvia direttamente il comando nel container esistente, quindi lo script dell'entrypoint non lo riceve. Con run, il tuo bash arriva allo script come argomento. Molte immagini ufficiali terminano l'entrypoint con exec "$@", quindi il comando passa direttamente e ottieni la shell. Uno script che interpreta autonomamente i propri argomenti si comporterà invece in modo diverso. In questo caso puoi sostituire l'entrypoint per una singola esecuzione:

docker compose run --rm --entrypoint sh web

Questo è il motivo più comune per cui un comando che funziona con exec si comporta diversamente con run. La distinzione tra comando ed entrypoint spiega quale parte della configurazione dell'immagine sostituisci ogni volta.

exec o run: come scegliere

  • exec richiede un container in esecuzione. run no e può avviare le dipendenze.
  • exec vede l’elenco dei processi attivi e i file nello stato corrente, inclusi quelli scritti dall’applicazione dopo l’avvio. run crea una copia pulita dell’immagine, quindi questi elementi non sono presenti.
  • exec non esegue l’entrypoint. run lo esegue.
  • run lascia un container, a meno che non si passi --rm.

Usa exec per verificare che cosa sta accadendo realmente. Usa run --rm per una copia temporanea dello stesso ambiente, per eseguire una migrazione una tantum oppure quando il servizio reale non rimane attivo abbastanza a lungo da consentire l’uso di exec.

Flag utili per l'esecuzione: utente, directory di lavoro e repliche

La maggior parte delle immagini passa a un utente non root. Per questo, l'installazione di uno strumento diagnostico nella shell exec si interrompe a questo punto:

E: Could not open lock file /var/lib/dpkg/lock-frontend - open (13: Permission denied)

-u root apre una shell root nello stesso container:

docker compose exec -u root web sh

-w /srv/app imposta la directory di lavoro solo per quel comando. -e KEY=value aggiunge una variabile d'ambiente alla sessione, non al servizio. Quando un servizio esegue più repliche, --index 2 determina in quale container si accede. Se si sta verificando la proprietà dei file in una directory montata, PUID e PGID nelle immagini container spiega perché sono gli ID numerici, non i nomi utente, a determinare chi può scrivere nella directory.

Aprire una shell psql o mysql nel container del database

Il client è già presente nell'immagine del database. Non serve installarlo sull'host e non è necessario pubblicare la porta:

docker compose exec db psql -U postgres -d app
docker compose exec db mariadb -u root -p

Le immagini Postgres includono psql, quelle MySQL includono mysql e quelle MariaDB includono mariadb. La connessione viene aperta dall'interno del container. Funziona quindi anche quando il file Compose non pubblica alcuna porta del database. Questa è la configurazione più sicura: nulla su Internet può raggiungere una porta che non è mai stata pubblicata.

Un errore comune può far perdere un pomeriggio. La shell espande le variabili sull'host prima che Docker riceva il comando. Di conseguenza, -U "$POSTGRES_USER" invia una stringa vuota quando la variabile esiste soltanto nel container. Le virgolette singole e una shell eseguita nel container espandono la variabile nel contesto corretto:

docker compose exec db sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB"'

Qui non usare docker compose run --rm db senza un comando. Avvia un secondo server Postgres sullo stesso volume dati, che non può essere avviato:

FATAL:  lock file "postmaster.pid" already exists

Il file di lock sta svolgendo il suo compito, perché due server che scrivono nella stessa directory dati la danneggerebbero. Mentre il database è in esecuzione, usa exec per accedere al container attivo. Decidere se il database debba essere gestito da Compose è un aspetto separato. eseguire il database in Docker o sull'host illustra i compromessi.

Servizi che richiedono una console all'avvio: stdin_open e tty

exec e run gestiscono le shell aperte manualmente. Un servizio il cui processo principale è interattivo per natura richiede due chiavi nel file compose:

services:
  console:
    image: python:3.12-slim
    command: python
    stdin_open: true
    tty: true

stdin_open: true è docker run -i e tty: true è docker run -t. Senza queste impostazioni, il container si avvia e termina immediatamente con codice 0, mentre docker compose ps -a mostra Exited (0). Non si è verificato alcun crash. python, in assenza di un terminale sullo standard input, rileva immediatamente la fine del file e termina normalmente. Questo è il comportamento corretto per un programma con cui nessuno sta interagendo tramite tastiera.

Con entrambe le chiavi impostate, collegarsi al processo in esecuzione:

docker attach $(docker compose ps -q console)

Scollegarsi con Ctrl-P seguito da Ctrl-Q. Il processo resta in esecuzione. Questa sequenza funziona solo quando il container ha una TTY e lo standard input aperto. Ctrl-C invia invece un interrupt al PID 1 e arresta il servizio.

Lasciare entrambe le chiavi disattivate per i servizi ordinari. Un server web non legge lo standard input e tty: true induce molti programmi ad attivare l'output a colori e il line buffering perché ritengono che qualcuno stia osservando. Di conseguenza, docker compose logs si riempie di codici di escape.

Perché l'esecuzione tramite script non funziona in cron e CI: il flag -T

Un comando exec che funziona nel terminale non riesce in un job cron o in un runner di continuous integration (CI):

the input device is not a TTY

Compose richiede per impostazione predefinita un pseudo-terminale, ma cron non assegna alcun terminale al job. La richiesta quindi non riesce prima ancora che il comando venga eseguito. -T disabilita questa richiesta:

0 3 * * * docker compose -f /srv/app/compose.yaml exec -T db pg_dump -U postgres -Fc app > /srv/backups/app.dump

-T è importante anche per un secondo motivo. Un TTY modifica il flusso di byte durante l'output, quindi un dump compresso che lo attraversa arriva danneggiato. Qualsiasi output reindirizzato o inviato tramite pipe richiede -T.

Sono necessari altri due accorgimenti per cron. Passa -f con un percorso assoluto, perché cron esegue il job dalla directory home, dove non esiste alcun file Compose. Compose si interrompe quindi con no configuration file provided: not found. Inoltre, exec restituisce il codice di uscita del comando eseguito. Un pg_dump non riuscito fa quindi fallire lo script con set -e invece di creare un backup vuoto e segnalare un esito positivo. Gli altri comandi di uso quotidiano sono raccolti in un promemoria dei comandi Compose da tenere accanto a questi script.

Perché le modifiche apportate all'interno di un container scompaiono

Installi uno strumento con exec, modifichi un file di configurazione, risolvi il problema e, una settimana dopo, la correzione è scomparsa. Questo è il comportamento previsto del livello scrivibile del container. docker compose up -d dopo ogni modifica al tag dell'immagine o alla definizione del servizio elimina il vecchio container e ne crea uno nuovo a partire dall'immagine; tutte le modifiche manuali vengono quindi perse insieme al vecchio container.

docker compose restart è diverso. Arresta e riavvia lo stesso container, quindi le modifiche manuali restano disponibili. Per questo una correzione manuale può sembrare persistente per settimane e poi scomparire durante un aggiornamento non correlato. I volumi denominati e i bind mount persistono con entrambe le operazioni, perché i relativi dati risiedono all'esterno del container. bind mount e volumi denominati spiega quale scegliere per i dati che si desidera conservare.

Considera quindi una shell exec come un ambiente per leggere e verificare. Quando hai individuato la correzione, scrivila in un punto persistente: installa un pacchetto nel Dockerfile e inserisci un'impostazione nel file compose. Poi docker compose up -d per applicarla e verifica con un altro exec che il nuovo container la contenga effettivamente.

FAQ

Qual è la differenza tra docker compose exec e docker compose run?

exec esegue un comando all'interno di un container già in esecuzione, accanto al processo principale, e ignora l'entrypoint dell'immagine. run crea un nuovo container dalla stessa definizione del servizio, con la stessa immagine, lo stesso ambiente, gli stessi volumi e le stesse reti, passa il comando attraverso l'entrypoint e avvia prima i servizi depends_on. Inoltre, run non pubblica le porte del servizio, a meno che non si aggiunga --service-ports. Usa exec per ispezionare il servizio attivo. Usa run --rm quando il servizio è arrestato o quando non vuoi interferire con esso.

Perché docker compose exec segnala che il servizio non è in esecuzione?

exec si collega a un container esistente e non può crearne uno. Di conseguenza, un servizio arrestato o terminato in modo anomalo restituisce service "web" is not running. Controlla docker compose ps -a, che elenca i container terminati con uno stato come Exited (1), e leggi docker compose logs web per conoscere il motivo dell'arresto. Per ottenere comunque una shell, esegui docker compose run --rm --entrypoint sh web. Questo crea un nuovo container dalla stessa definizione del servizio senza eseguire il comando di avvio che causa il problema.

Come apro una shell quando l'immagine non contiene bash?

Se docker compose exec web bash restituisce exec: "bash": executable file not found in $PATH, significa che bash non è presente nell'immagine. È normale per le immagini basate su Alpine. Usa docker compose exec web sh, perché BusyBox fornisce /bin/sh. Le immagini Distroless e scratch non contengono alcuna shell, quindi nessun comando exec funzionerà. Passa al tag :debug dell'immagine, se il publisher lo fornisce, oppure avvia un container di debug nei namespace del target con docker run --rm -it --network "container:$CID" --pid "container:$CID" nicolaka/netshoot, dove $CID proviene da docker compose ps -q web.

Perché il comando exec non funziona con "the input device is not a TTY" in cron?

docker compose exec richiede per impostazione predefinita un terminale pseudoterminale, mentre cron non ne fornisce uno. La richiesta quindi non riesce prima dell'esecuzione del comando. Aggiungi -T per disabilitare questa opzione: docker compose exec -T db pg_dump -U postgres app. Usa -T anche per l'output reindirizzato o passato tramite pipe, perché un TTY modifica il flusso di byte e può danneggiare un dump binario. In cron, specifica anche -f con il percorso assoluto del file Compose, altrimenti Compose termina con no configuration file provided: not found.

Le modifiche apportate all'interno di un container con exec persistono dopo un riavvio?

Persistono dopo docker compose restart, che riutilizza lo stesso container. Vengono perse dopo docker compose up -d in seguito a una modifica dell'immagine o della configurazione, perché il container viene ricreato dall'immagine e il relativo layer scrivibile viene eliminato. I dati scritti nei volumi denominati o nei bind mount persistono in entrambi i casi, perché risiedono all'esterno del container. Usa exec per le modifiche diagnostiche, quindi inserisci la configurazione permanente nel Dockerfile o nel file Compose.