Docker Compose: differenza tra build e image su VPS
Scopri perché compose up ignora le modifiche al Dockerfile: image scarica un tag, build crea l'immagine localmente. Ecco la correzione su VPS.
Confronto tra build e image in Docker Compose: risposta breve
In un file Docker Compose, image: specifica un'immagine da scaricare da un registry, mentre build: indica a Compose di crearne una su questa macchina a partire da un Dockerfile. Se imposti solo image:, Compose scarica quel tag e lo avvia. Se imposti solo build:, Compose crea l'immagine localmente e le assegna un nome derivato dal nome del progetto e da quello del servizio. Se imposti entrambi, Compose crea l'immagine localmente e assegna al risultato il nome indicato in image:. Questo permette di creare un'immagine e pubblicarla con un nome scelto.
Questa è l'intera distinzione. Tutto ciò che segue spiega le conseguenze operative su un server. Si presuppone che Docker Engine e il plugin Compose siano già installati; eseguire Docker su un VPS tratta questo aspetto.
Le tre forme complete
Scarica un tag pubblicato ed eseguilo. Non viene usato alcun Dockerfile.
services:
web:
image: nginx:1.27
restart: unless-stopped
ports:
- "80:80"Esegui la build usando un Dockerfile nella directory corrente. Non viene scaricato nulla, tranne l'immagine di base indicata in FROM.
services:
web:
build: .
restart: unless-stopped
ports:
- "80:80"Esegui la build localmente e assegna un tag al risultato. docker compose push può quindi inviare quel tag esatto a un registry.
services:
web:
build:
context: .
dockerfile: Dockerfile
image: registry.example.com/acme/web:1.4.2
restart: unless-stopped
ports:
- "80:80"context è la directory inviata al builder. dockerfile viene risolto rispetto a quel contesto, quindi context: . con dockerfile: docker/prod.Dockerfile è normale e corretto. Esegui docker compose images per visualizzare il nome e l'ID dell'immagine associati a ogni container di servizio: è il modo più rapido per verificare quale di queste tre forme hai effettivamente scritto.
Perché docker compose up non ricrea l'immagine dopo una modifica al Dockerfile?
Perché up verifica se l'immagine esiste, non se è aggiornata.
Quando Compose avvia un servizio che contiene una sezione build:, cerca l'immagine nell'archivio locale delle immagini. Se è già presente un'immagine con quel nome, Compose la utilizza. Non legge il Dockerfile, non confronta i file sorgente e non controlla alcun timestamp. La specifica di Compose definisce questa regola tramite l'attributo pull_policy, mentre il comportamento predefinito prevede la creazione dell'immagine solo quando questa manca. Un'immagine presente viene considerata sufficiente.
Quindi modifichi app.py, esegui docker compose up -d, vedi Compose indicare che il container è in esecuzione e continui a servire il codice precedente. Non si è verificato alcun errore, quindi non viene visualizzato alcun avviso. Questa è la causa più comune del problema "la mia modifica non è stata applicata" con Compose. L'indicatore è la parola di stato che Compose visualizza accanto al nome del container: un container sostituito da Compose viene indicato come ricreato o avviato, mentre un container che Compose decide di non modificare viene indicato come in esecuzione.
Due controlli sono sufficienti. docker compose images visualizza l'ID dell'immagine utilizzata da ciascun container; annotalo prima del deployment e confrontalo dopo. docker image ls include una colonna CREATED: un'immagine creata prima dell'ultimo commit è obsoleta, indipendentemente da ciò che ha visualizzato lo script di deployment.
Quali flag forzano una ricompilazione
docker compose up -d --buildesegue prima la build, quindi ricrea ogni container la cui immagine è cambiata. È il flag che nella maggior parte dei casi serve.docker compose build webesegue la build di un servizio senza avviarne alcuno. Seguilo condocker compose up --no-deps -d webper sostituire soltanto quel container e lasciare in esecuzione il resto dello stack.docker compose build --no-cache webelimina tutti i layer memorizzati nella cache ed esegue la build dalla prima istruzione.docker compose build --pullprova a scaricare una versione più recente dell'immagine di base inFROM. In questo modo un tag variabile comenode:22usa il contenuto corrente invece della copia scaricata a marzo.docker compose up -d --force-recreatericrea i container a partire dall'immagine che stanno già usando. Non esegue mai la build. Usarlo quando invece serviva--buildè un errore comune.
Puoi anche spostare questa decisione nel file. Nella terminologia della specifica Compose, pull_policy: build indica che Compose esegue la build dell'immagine e la ricompila se è già presente. Ogni up comporta quindi una build, scelta appropriata su un laptop ma raramente su un server.
services:
web:
build: .
image: registry.example.com/acme/web:dev
pull_policy: buildÈ utile conoscere anche un'altra interazione. docker compose pull prova a scaricare le immagini anche per i servizi che hanno una sezione build. Se il download non riesce, indica che l'immagine deve essere compilata. Passa --ignore-buildable per ignorare questi servizi senza visualizzare messaggi.
Come la cache della build determina la durata del deploy
Ogni istruzione in un Dockerfile produce un layer e il builder riutilizza un layer memorizzato nella cache quando l'istruzione e i relativi input non sono cambiati. Per COPY, gli input sono i contenuti dei file copiati. Quando un layer non viene trovato nella cache, ogni layer successivo viene ricostruito, perché ogni layer viene creato sul filesystem prodotto da quello precedente.
Questa regola determina se il deploy richiede secondi o minuti. Ordina il Dockerfile dagli elementi che cambiano raramente a quelli che cambiano a ogni commit.
FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
CMD ["node", "server.js"]npm ci è collocato sopra COPY . ., quindi la modifica di un file sorgente lascia il layer di installazione nella cache e la build riprende dal passaggio di copia. Se inverti queste due righe, una modifica di un solo carattere reinstalla tutte le dipendenze, perché COPY . . invalida il layer su cui viene creato npm ci. La stessa struttura si applica a pip install -r requirements.txt e a go mod download.
--no-cache è lo strumento corretto quando sospetti che un layer obsoleto stia nascondendo la correzione. Non è una buona impostazione predefinita, perché elimina il riutilizzo che l'ordine del Dockerfile dovrebbe consentire.
Un elemento può essere impostato dall'immagine e sovrascritto da Compose: CMD del Dockerfile definisce ciò che l'immagine esegue per impostazione predefinita, mentre una chiave command: nel servizio lo sostituisce. Come interagiscono command ed entrypoint è importante in questo caso, perché una sovrascrittura in Compose può far comportare un'immagine appena compilata esattamente come quella precedente.
Contesto di build e .dockerignore
context: . indica che Compose crea un pacchetto con quella directory e lo invia al builder prima dell'esecuzione della prima istruzione. Viene incluso tutto il contenuto, compresi .git e qualsiasi directory di dati presente accanto al codice sorgente. Se una build si interrompe durante il trasferimento del contesto, mentre il progetto non è cambiato, significa che il contesto è troppo grande.
Un file .dockerignore nella radice del contesto esclude determinati percorsi dal trasferimento. La sintassi è simile a quella di .gitignore.
.git
node_modules
*.log
data/
.envI vantaggi sono due. Il trasferimento è più piccolo, quindi ogni build inizia più rapidamente. Inoltre, COPY . . non può più copiare .env nell'immagine, dove chiunque scarichi l'immagine può leggerlo.
Il caso di build che rallenta nel tempo riguarda un bind mount. Un volume denominato si trova all'esterno della directory del progetto, mentre un bind mount come ./data:/var/lib/postgresql/data si trova all'interno del contesto di build. Di conseguenza, le build diventano più lente ogni settimana, man mano che il database cresce. Una riga in .dockerignore risolve il problema. Bind mount rispetto ai volumi denominati illustra il compromesso più ampio.
Gli argomenti di build comportano una versione ridotta dello stesso rischio. I valori passati tramite args: sono visibili nella cronologia dell'immagine a chiunque disponga dell'immagine. Inserire quindi un numero di versione, mai un token. File env e secret in Compose spiega invece dove collocare le credenziali.
Conviene compilare sul VPS oppure altrove e poi eseguire il pull?
Compilare sul server che gestisce il traffico è l’impostazione predefinita perché segue il percorso più breve: git pull, quindi docker compose up -d --build. Va bene su un server piccolo da cui nessuno dipende ancora. Diventa una scelta inadeguata per due motivi misurabili e per un terzo che emerge soltanto nelle giornate peggiori.
Memoria. Una compilazione esegue compilatori e bundler insieme all’applicazione in produzione, e questi sono spesso i processi che consumano più memoria. Su un VPS da 1 GB, un bundler JavaScript o una compilazione Rust è normalmente il processo più grande del server. Quando il kernel esaurisce la memoria, termina il processo più grande: la compilazione si interrompe con Killed e codice di uscita 137 oppure viene terminato il database e il sito diventa irraggiungibile durante un deployment. dmesg -T | grep -i oom stampa la riga relativa alla terminazione con il nome del processo, così puoi capire quale dei due casi si è verificato senza procedere per ipotesi.
Disco. Ogni compilazione lascia dei layer e il builder mantiene la propria cache separatamente dalle immagini. docker system df mostra entrambi, e la riga relativa alla build cache continua a crescere. Recupera spazio con docker image prune per le immagini dangling e con docker builder prune per i layer memorizzati nella cache. Un disco pieno interrompe più della sola compilazione. Anche il database smette di scrivere, e questo guasto ha costi molto superiori a quelli di un deployment lento.
Riproducibilità. Un’immagine compilata sul server esiste soltanto su quel server. Per eseguire il rollback devi effettuare il checkout del commit precedente e compilare di nuovo, ma non è garantito che il risultato corrisponda a quello precedente, perché il tag dell’immagine di base è cambiato e anche i mirror dei pacchetti hanno subito modifiche. Compilare altrove e fare il push di un tag trasforma il rollback in una modifica: imposta image: sul tag precedente ed esegui docker compose up -d.
L’assetto che regge nel tempo è semplice. Il sistema di continuous integration esegue la compilazione e fa il push di registry.example.com/acme/web:<git-sha>, mentre il file Compose sul VPS contiene image: senza alcuna chiave build:. Il deployment richiede quindi due comandi e quasi nessuna memoria.
docker compose pull
docker compose up -dEsegui docker login registry.example.com una volta sul server. Da quel momento Compose potrà eseguire il pull dei tag privati.
Mantieni la sezione di build per lo sviluppo, invece di eliminarla, in un file a cui assegni tu il nome.
# compose.dev.yaml
services:
web:
build:
context: .
pull_policy: builddocker compose -f compose.yaml -f compose.dev.yaml up -d --buildAssegna a quel file il nome compose.dev.yaml, non compose.override.yaml. Compose carica automaticamente un file di override quando è presente, quindi un override copiato per errore sul server avvierebbe nuovamente la compilazione in locale senza segnalarlo chiaramente. Sovrapporre più file Compose spiega come viene risolta la combinazione di ogni chiave.
La trappola dell'architettura quando si esegue la build altrove
Un'immagine include l'architettura CPU per la quale è stata creata. Se esegui la build su un laptop Apple Silicon, esegui il push e poi fai il pull di quel tag su un VPS x86_64, Docker avvisa che la piattaforma dell'immagine richiesta non corrisponde a quella rilevata sull'host. Il processo termina quindi con exec format error, che sembra indicare un binario danneggiato, ma non lo è. Specifica esplicitamente la destinazione della build:
docker buildx build --platform linux/amd64 \
-t registry.example.com/acme/web:1.4.2 --push .Lo stesso problema si verifica al contrario se il laptop usa x86 ed esegui un VPS ARM anziché x86. Se lasci che sia la CI a eseguire la build sull'architettura su cui effettui il deployment, elimini il problema.
Cosa controllare dopo un deploy
docker compose imagesstampa l'immagine e il tag usati da ogni container in esecuzione. Un ID immagine diverso conferma che la nuova build è in servizio.docker compose configstampa il file risultante dopo la sostituzione delle variabili, così puoi leggere il nome finale dell'immagine che Compose userà prima di eseguire qualsiasi comando.docker compose logs -f webper il primo mezzo minuto dopo la sostituzione. Un container che si avvia e termina viene riavviato in un ciclo invece di restare attivo, e il ciclo passa inosservato se non lo controlli.docker image lsmostra una colonna CREATED. Un'immagine più vecchia dell'ultimo commit non è mai stata ricostruita.
Se stai ancora preparando il file a cui applicare questi controlli, le basi di un file Compose su un VPS descrive le chiavi correlate, mentre il riepilogo dei comandi Compose elenca gli altri sottocomandi.
FAQ
Posso usare build e image nello stesso servizio?
Sì, ed è la configurazione normale per un progetto che compili autonomamente. Compose esegue la build dalla sezione build: e assegna al risultato il tag specificato da image:. Questo tag è quello che docker compose push invia a un registry e che un altro computer scarica. Senza una chiave image:, Compose esegue comunque la build, ma assegna all'immagine un nome basato sul progetto e sul servizio e avvisa che l'attributo mancante impedisce il push dell'immagine.
Perché docker compose up non rileva la modifica al mio Dockerfile?
Perché up verifica soltanto se esiste un'immagine con quel nome. Se l'immagine esiste, Compose la avvia e non la confronta mai con il Dockerfile o con i file sorgente. Esegui docker compose up -d --build oppure esegui docker compose build web seguito da docker compose up --no-deps -d web per sostituire un singolo servizio. Impostando pull_policy: build sul servizio, ogni up esegue una nuova build, una configurazione adatta a una macchina di sviluppo.
Qual è la differenza tra --build e --force-recreate?
--build esegue nuovamente la build dell'immagine, quindi ricrea i container la cui immagine è cambiata. --force-recreate ricrea i container usando l'immagine già disponibile, quindi non può rilevare una modifica al codice. Se la modifica riguarda il codice sorgente o il Dockerfile, --build è il flag da usare. --force-recreate serve a reimpostare il container, ad esempio per svuotare il suo writable layer mantenendo la stessa immagine.
Devo eseguire la build delle immagini Docker sul VPS o altrove?
Esegui la build altrove e scarica un tag quando il server gestisce anche traffico. La build compete con l'applicazione per la memoria. Su un VPS di piccole dimensioni, il kernel risolve questa competizione terminando il processo che utilizza più memoria, che potrebbe essere la build oppure il database. Inoltre, le build lasciano nella cache dati sul disco che nessun processo elimina automaticamente. Eseguire la build sul server va bene per un progetto piccolo senza utenti. In seguito, la migrazione è semplice se mantieni la sezione build: in un file Compose usato soltanto in sviluppo.
Come posso evitare che la build cache di Docker riempia il disco?
Esegui docker system df per verificare quanto spazio occupano le immagini e la build cache. docker builder prune rimuove i layer memorizzati nella cache e docker image prune rimuove le immagini dangling lasciate dalle build precedenti. Aggiungere -a a uno dei due comandi rende l'operazione più aggressiva e obbliga la build successiva a partire senza cache. Non pianificare docker system prune -af --volumes su un server, perché --volumes elimina qualsiasi volume che nessun container sta utilizzando. Uno stack arrestato per manutenzione conserva il database esattamente in un volume di questo tipo.