Articolo originale: The Docker Handbook – Learn Docker for Beginners
Il concetto di containerizzazione è piuttosto risalente, ma la nascita del Docker Engine nel 2013 ha reso molto più semplice la sua applicazione.
Secondo un sondaggio del 2020 di Stack Overflow, Docker è la 1ᵃ piattaforma più richiesta, la 2ᵃ piattaforma più amata e la 3ᵃ piattaforma più popolare.
Per quanto possa essere richiesta, i primi passi possono essere complicati. In questo manuale, impareremo tutto dalle basi della containerizzazione fino a un livello più intermedio. Dopo aver letto l'intero manuale, dovresti essere in grado di:
- Containerizzare (quasi) qualsiasi applicazione
- Caricare immagini Docker personalizzate su registri online
- Lavorare con container multipli usando Docker Compose
Prerequisiti
- Familiarità con il terminale Linux
- Familiarità con JavaScript (alcuni progetti richiedono l'uso di JavaScript)
Sommario
- Introduzione alla containerizzazione e a Docker
- Come installare Docker
- Hello World in Docker - Introduzione a Docker
- Le basi della manipolazione dei container in Docker
- Come eseguire un container
- Come pubblicare una porta
- Come usare l'opzione detach
- Come elencare i container
- Come dare un nome a un container o rinominarlo
- Come fermare un container in esecuzione
- Come riavviare un container
- Come creare un container senza eseguirlo
- Come rimuovere dei container sospesi
- Come eseguire un container in modalità interattiva
- Come eseguire comandi all'interno di un container
- Come lavorare con immagini eseguibili
- Manipolazione base delle immagini Docker
- Come creare un'immagine Docker
- Come taggare le immagini Docker
- Come elencare e rimuovere immagini Docker
- Come comprendere i livelli di un'immagine Docker
- Come eseguire il build di NGINX dalla sorgente
- Come ottimizzare le immagini Docker
- Alpine Linux
- Come creare immagini Docker eseguibili
- Come condividere le tue immagini Docker online
- Come containerizzare una app JavaScript
- Manipolazione base delle reti in Docker
- Come containerizzare un'applicazione JavaScript multi-container
- Come eseguire il server del database
- Come lavorare con i volumi in Docker
- Come accedere ai log da un container in Docker
- Come creare una rete e collegare il server del database in Docker
- Come scrivere il Dockerfile
- Come eseguire i comandi in un container in esecuzione
- Come scrivere degli script di gestione in Docker
- Come comporre i progetti usando Docker Compose
- Le basi di Docker Compose
- Come avviare i servizi in Docker Compose
- Come elencare i servizi in Docker Compose
- Come eseguire dei comandi all'interno di un servizio in esecuzione in Docker Compose
- Come accedere ai log da un servizio in esecuzione in Docker Compose
- Come fermare dei servizi in Docker Compose
- Come comporre un'applicazione full-stack in Docker Compose
- Conclusione
Il codice dei progetti
Il codice dei progetti di esempio è disponibile nel seguente repository:
Puoi trovare il codice completo nel branch completed
.
Contributi
Questo libro è completamente open-source e le contribuzioni di qualità sono ben accette. Puoi trovare il contenuto integrale nel seguente repository:
Di solito, apporto le modifiche prima alla versione GitBook del manuale e poi la pubblico su freeCodeCamp. Puoi trovare la versione sempre aggiornata e spesso instabile del libro al seguente link:

Se desideri una versione statica ma stabile, freeCodeCamp è miglior posto dove trovarla:

Qualsiasi versione finirai per leggere, non dimenticarmi di dirmi cosa ne pensi. Le critiche costruttive sono sempre ben accette.
Introduzione alla containerizzazione e a Docker
Secondo IBM,
La containerizzazione implica l'incapsulamento o l'impacchettamento del codice di un software e di tutte le sue dipendenze, in modo che possa essere eseguito in modo uniforme e coerente su qualsiasi infrastruttura.
In altre parole, la containerizzazione ti consente di impacchettare un software con tutte le sue dipendenze in un pacchetto autonomo, così da poterlo eseguire senza passare per un fastidioso processo di configurazione.
Consideriamo un caso pratico. Immagina di aver sviluppato una fantastica applicazione per la gestione di libri, che può contenere informazioni su tutti i libri che possiedi e può anche servire come sistema di prestito per i tuoi amici.
Se fai una lista delle dipendenze, dovrebbe essere qualcosa del genere:
- Node.js
- Express.js
- SQLite3
Bene, teoricamente dovrebbe essere tutto. Ma in pratica ci sono anche altre cose. Viene fuori che Node.js usa uno strumento di build chiamato node-gyp
per il build degli add-on. E secondo le istruzioni di installazione nel repository ufficiale, questo strumento di build richiede Python 2 o 3 e una appropriata tool-chain per il compilatore C/C++.
Considerando tutto ciò, la lista finale delle dipendenze è la seguente:
- Node.js
- Express.js
- SQLite3
- Python 2 o 3
- C/C++ tool-chain
Installare Python 2 o 3 è piuttosto semplice indipendentemente dalla piattaforma, mentre configurare la tool-chain di C/C++ è abbastanza facile su Linux, ma su Windows e Mac è un'attività piuttosto spiacevole.
Su Windows, il pacchetto degli strumenti di build di C/C++ è nell'ordine dei gigabyte e richiede del tempo per essere installato. Su un Mac, puoi installare l'enorme applicazione Xcode oppure il pacchetto più piccolo di strumenti per la riga di comando per Xcode.
Indipendentemente da ciò che installi, potrebbero esserci comunque problemi con gli aggiornamenti del sistema operativo. È così comune che ci sono delle note d'installazione per macOS Catalina disponibili sul repository ufficiale.
Ipotizziamo che hai superato tutte queste noie, configurando tutte le dipendenze e hai iniziato a lavorare sul progetto. Significa che sei fuori pericolo? Naturalmente no.
E se un tuo collega utilizza Windows mentre tu usi Linux? Adesso devi considerare tutte le incongruenze relative a come i due sistemi operativi gestiscono i percorsi. Oppure il fatto che tecnologie popolari come nginx non sono ottimizzate per Windows. Alcune tecnologie come Redis non sono neanche pre-configurate per Windows.
Anche se dovessi affrontare l'intera fase di sviluppo, cosa accade se la persona responsabile della gestione dei server esegue una procedura di deployment errata?
Tutti questi problemi possono essere risolti se soltanto potessi:
- Sviluppare ed eseguire l'applicazione all'interno di un ambiente isolato (detto container) che corrisponde all'ambiente finale di distribuzione.
- Mettere l'applicazione all'interno di un singolo file (detto immagine) insieme a tutte le sue dipendenze e le configurazioni necessarie per il deployment.
- E condividere l'immagine a un server centrale (detto registro), accessibile da chiunque abbia l'autorizzazione appropriata.
I tuoi colleghi saranno in grado di scaricare l'immagine dal registro, eseguire l'applicazione, dato che è all'interno di un ambiente isolato e libero dalle incongruenze dovute a una specifica piattaforma, o anche distribuire direttamente da un server, visto che l'immagine è provvista di tutte le configurazioni di produzione appropriate.
Questa è l'idea alla base della containerizzazione: inserire un'applicazione all'interno di un pacchetto autonomo, renderla portabile e riproducibile in diversi ambienti.
E adesso la domande è "che ruolo svolge Docker?".
Come già menzionato, la containerizzazione è un'idea che risolve una miriade di problemi nello sviluppo di software inserendo le cose in una scatola.
Questa idea può essere messa in atto in vari modi. Docker è uno di questi. È una piattaforma open-source di containerizzazione che ti consente di containerizzare le tue applicazioni, condividerle usando dei registri pubblici o privati, e anche di orchestrarle.
Docker non è l'unico strumento di containerizzazione sul mercato, è soltanto il più conosciuto. Un altro motore di containerizzazione che amo si chiama Podman, sviluppato da Red Hat. Altri strumenti come Kaniko di Google, rkt di CoreOS sono fantastici, eppure non sono ancora pronti rimpiazzare Docker.
Se vuoi una lezione di storia, potresti leggere A Brief History of Containers: From the 1970s Till Now (risorsa in inglese) che parla dei momenti decisivi di questa tecnologia.
Come installare Docker
L'installazione di Docker varia largamente a seconda del sistema operativo in uso. Ma è comunque generalmente semplice.
Docker gira alla perfezione su tutte e tre le piattaforme principali, Mac, Windows e Linux. Tra le tre, il processo di installazione su Mac è più semplice, quindi partiremo da qui.
Come installare Docker su macOS
Su un mac, tutto ciò che devi fare è andare sulla pagina ufficiale di download e cliccare il pulsante Download for Mac (stable).
Otterrai un file Apple Disk Image dall'aspetto normale e al suo interno ci sarà l'applicazione. Tutto ciò che devi fare è trascinare il file e rilasciarlo nella cartella Applicazioni.

Puoi avviare Docker facendo semplicemente doppio click sull'icona dell'applicazione. Una volta che l'applicazione si avvia, vedrai l'icona di Docker apparire nella barra del menu.

Adesso, apri il terminale ed esegui docker --version
e docker-compose --version
per assicurarti che l'installazione sia andata a buon fine.
Come installare Docker su Windows
Su Windows, la procedura è quasi la stessa, eccetto che ci sono alcuni passaggi aggiuntivi da svolgere. I passaggi per l'installazione sono i seguenti:
- Vai su questo sito e segui le istruzioni per installare WSL2 su Windows 10.
- Vai sulla pagina ufficiale di download e clicca sul pulsante Download for Windows (stable).
- Fai doppio click sull'installer scaricato e segui l'installazione predefinita.
Una volta terminata, avvia Docker Desktop dal menu start o dal desktop. L'icona di Docker apparirà nella barra delle applicazioni.

Apri Ubuntu o qualsiasi distribuzione hai installato dal Microsoft Store. Esegui i comandi docker --version
e docker-compose --version
per assicurarti che l'installazione abbia avuto successo.

Puoi accedere a Docker anche dal prompt dei comandi o da PowerShell. È solo che preferisco utilizzare WSL2 rispetto ad altre righe di comando su Windows.
Come installare Docker su Linux
L'installazione di Docker su Linux è un processo leggermente diverso e, in base alla distribuzione, può cambiare ancora di più. Ma, onestamente, l'installazione è comunque semplice come sulle altre due piattaforme (se non di più).
Il pacchetto Docker Desktop su Windows o Mac è un insieme di strumenti come Docker Engine
, Docker Compose
, Docker Dashboard
, Kubernetes
e qualche altra chicca.
Su Linux invece, non avrai questa collezione, ma dovrai installare manualmente tutti gli strumenti necessari. Le procedure di installazione per le diverse distribuzioni sono le seguenti:
- Se sei su Ubuntu, puoi seguire la sezione Install Docker Engine on Ubuntu della documentazione ufficiale.
- Per le altre distribuzioni, sono disponibili guide nella documentazione ufficiale.
- Install Docker Engine on Debian
- Install Docker Engine on Fedora
- Install Docker Engine on CentOS
- Se sei su una distribuzione non elencata nella documentazione, puoi seguire la guida Install Docker Engine from binaries.
- Indipendentemente dalla procedura che segui, dovrai svolgere degli importanti passaggi post-installazione per Linux.
- Una volta terminata l'installazione, dovrai installare un altro strumento chiamato Docker Compose. Puoi seguire la guida Install Docker Compose dalla documentazione ufficiale.
Quando hai concluso l'installazione, apri il terminale ed esegui docker --version
e docker-compose --version
per assicurarti che l'installazione sia avvenuta con successo.

Sebbene le prestazioni di Docker siano piuttosto buone indipendentemente dalla piattaforma, preferisco Linux rispetto alle altre. In tutto il manuale, mi alternerò tra le mie postazioni Ubuntu 20.10 e Fedora 33.
Un'altra cosa che vorrei chiarire fin dall'inizio è che non utilizzerò nessuno strumento GUI per lavorare con Docker in questo manuale.
Sono consapevole che ci sono disponibili dei validi strumenti GUI per varie piattaforme, ma imparare i comandi docker comuni è uno degli obiettivi primari di questo libro.
Hello World in Docker – Introduzione a Docker
Ora che hai Docker sulla tua macchina, è tempo per eseguire il tuo primo container. Apri il terminale ed esegui il seguente comando:
docker run hello-world
# Unable to find image 'hello-world:latest' locally
# latest: Pulling from library/hello-world
# 0e03bdcc26d7: Pull complete
# Digest: sha256:4cf9c47f86df71d48364001ede3a4fcd85ae80ce02ebad74156906caff5378bc
# Status: Downloaded newer image for hello-world:latest
#
# Hello from Docker!
# This message shows that your installation appears to be working correctly.
#
# To generate this message, Docker took the following steps:
# 1. The Docker client contacted the Docker daemon.
# 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
# (amd64)
# 3. The Docker daemon created a new container from that image which runs the
# executable that produces the output you are currently reading.
# 4. The Docker daemon streamed that output to the Docker client, which sent it
# to your terminal.
#
# To try something more ambitious, you can run an Ubuntu container with:
# $ docker run -it ubuntu bash
#
# Share images, automate workflows, and more with a free Docker ID:
# https://hub.docker.com/
#
# For more examples and ideas, visit:
# https://docs.docker.com/get-started/
L'immagine hello-world è un esempio minimale di containerizzazione con Docker. Contiene un singolo programma compilato da un file hello.c, responsabile di stampare sul terminale il messaggio che stai vedendo.
Nel terminale, puoi usare il comando docker ps -a
per dare un'occhiata a tutti i container attualmente in esecuzione o eseguiti in passato:
docker ps -a
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# 128ec8ceab71 hello-world "/hello" 14 seconds ago Exited (0) 13 seconds ago exciting_chebyshev
Nell'output, un container chiamato exciting_chebyshev
è stato eseguito con il container id 128ec8ceab71
usando l'immagine hello-world
. In Exited (0) 13 seconds ago
, il codice di uscita (0)
sta a indicare che non sono stati prodotti errori durante il runtime del container.
Per capire cosa è accaduto dietro le quinte, dovrai acquisire familiarità con l'architettura di Docker e tre dei concetti fondamentali della containerizzazione in generale, che sono:
- Container
- Immagine
- Registro
Li ho elencati in ordine alfabetico e inizierò le mie spiegazioni dal primo della lista.
Cos'è un container?
Nella parola containerizzazione non può esserci nulla di più fondamentale del concetto di container.
Il sito delle risorse ufficiali di Docker afferma che:
Un contenitore è un'astrazione al livello dell'applicazione che impacchetta insieme il codice e le dipendenze. Invece di virtualizzare l'intera macchina fisica, i container virtualizzano solo il sistema operativo host.
Puoi considerare i container come la prossima generazione di macchine virtuali.
Proprio come le macchine virtuali, i container sono ambienti completamente isolati dal sistema che li ospita, così come tra di loro. Sono anche molto più leggeri rispetto alle macchine virtuali tradizionali, quindi un gran numero di container può essere eseguito simultaneamente senza influenzare le prestazioni del sistema che li ospita.
I container e le macchine virtuali, in realtà, sono dei modi differenti di virtualizzare l'hardware fisico. La differenza principale tra i due è il metodo di virtualizzazione.
Le macchine virtuali sono solitamente create e gestite da un programma detto hypervisor (o ipervisore), come Oracle VM VirtualBox, VMware Workstation, KVM, Microsoft Hyper-V e via dicendo. Il programma hypervisor di solito è posto tra il sistema operativo e le macchina virtuale e agisce come mezzo di comunicazione.
Ogni macchina virtuale possiede il suo sistema operativo guest, che è pesante tanto quanto un sistema operativo host.
L'applicazione all'interno di una macchina virtuale comunica con il sistema operativo guest, che parla con l'hypervisor, che a sua volta comunica con il sistema operativo host per allocare le risorse necessarie dall'infrastruttura fisica per eseguire l'applicazione.
Come puoi vedere, si tratta di una lunga catena di comunicazione tra le applicazioni in esecuzione all'interno della macchina virtuale e le infrastrutture fisiche. Le applicazioni in esecuzione all'interno della macchina virtuale potrebbero richiedere solo una piccola quantità di risorse, ma il sistema operativo guest aggiunge un notevole carico.
A differenza di una macchina virtuale, un container svolge il lavoro di virtualizzazione in un modo più intelligente. Invece di avere un sistema operativo guest completo all'interno del contenitore, utilizza il sistema operativo host attraverso il runtime del container, mantenendo al contempo l'isolamento – come una macchina virtuale tradizionale.
Il runtime del container, ovvero Docker, si trova tra il container e il sistema operativo host al posto dell'hypervisor. I container comunicano con il runtime del container, che poi comunica con il sistema operativo host per ottenere le risorse necessarie dall'infrastruttura fisica.
Come risultato dell'eliminazione dell'intero livello del sistema operativo guest, i container sono molto più leggeri e meno avidi di risorse rispetto alle tradizionali macchine virtuali.
Come dimostrazione di questo punto, guarda il seguente blocco di codice:
uname -a
# Linux alpha-centauri 5.8.0-22-generic #23-Ubuntu SMP Fri Oct 9 00:34:40 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
docker run alpine uname -a
# Linux f08dbbe9199b 5.8.0-22-generic #23-Ubuntu SMP Fri Oct 9 00:34:40 UTC 2020 x86_64 Linux
Nel blocco di codice qui sopra, ho eseguito il comando uname -a
sul mio sistema operativo host per stampare i dettagli sul kernel. Poi, nella riga successiva, ho eseguito lo stesso comando all'interno del container di Alpine Linux.
Come puoi vedere nell'output, il container sta effettivamente usando il kernel del sistema operativo host. Questo prova che i container virtualizzano il sistema operativo host invece di avere un proprio sistema operativo.
Se sei su una macchina Windows, scoprirai che tutti i container usano il kernel WSL2. Questo accade perché WSL2 agisce come back-end per Docker su Windows. Su macOS il back-end predefinito è una macchina virtuale con un hypervisor HyperKit.
Cos'è un'immagine Docker?
Le immagini sono dei file autonomi a più livelli che agiscono come modello per la creazione di container. Sono come una copia congelata, di sola lettura di un container. Le immagini possono essere scambiate attraverso dei registri.
Nel passato, diversi motori di container avevano formati immagine diversi. Successivamente, L'OCI (Open Container Initiative) ha definito delle specifiche standard per le immagini di container a cui aderiscono i principali motori di containerizzazione. Ciò significa che un'immagine creata con Docker può essere usata con un altro runtime come Podman senza altre seccature.
I container sono semplicemente immagini nello stato di esecuzione. Quando ottieni un'immagine da internet ed esegui un container usando un'immagine, crei essenzialmente un altro livello scrivibile temporaneo sopra a quelli di sola lettura.
Questo concetto diventerà molto più chiaro nelle prossime sezioni di questo libro, ma per ora tieni a mente che le immagini sono file a più livelli e di sola lettura che portano un'applicazione nello stato desiderato al loro interno.
Cos'è un registro Docker?
Hai già imparato cosa sono due importanti pezzi del puzzle, i container e le immagini. Il pezzo finale è il registro.
Un registro di immagini è un luogo centralizzato in cui puoi caricare le tue immagini e scaricare le immagini create da altri. Docker Hub è il registro pubblico predefinito per Docker. Un altro registro di immagini molto popolare è Quay di Red Hat.
In questo manuale ho scelto di utilizzare Docker Hub come registro.

Puoi condividere gratuitamente un qualsiasi numero di immagini pubbliche su Docker Hub. Le persone in tutto il modo saranno in grado di scaricarle e usarle liberamente. Le immagini che ho caricato sono disponibili sulla pagina del mio profilo (fhsinchy).

Oltre a Docker Hub o Quay, puoi anche creare il tuo registro di immagini in cui ospitare immagini private. Esiste anche un registro locale in esecuzione sul tuo computer che memorizza nella cache le immagini scaricate dai registri remoti.
Una panoramica dell'architettura Docker
Ora che hai un'idea dei concetti fondamentali della containerizzazione e di Docker, è tempo di capire come è stato progettato Docker come software.
Il motore consiste di tre componenti principali:
- Docker Daemon: il demone (
dockerd
) è un processo che continua a essere eseguito in background e attende i comandi dal client. Il demone è in grado di gestire vari oggetti Docker. - Docker Client: Il client (
docker
) è un'interfaccia da riga di comando principalmente responsabile del trasporto dei comandi lanciati dagli utenti. - REST API: l'API REST agisce come un ponte tra il demone e il client. Ogni comando lanciato usando il client passa per l'API per raggiungere il demone alla fine.
Secondo la documentazione ufficiale:
"Docker utilizza un'architettura client-server. Il client di Docker comunica con il demone, che svolge il lavoro sporco di fare il build, eseguire e distribuire i container Docker".
Come utente, eseguirai i comandi usando il componente client, il quale userà l'API REST per comunicare con il demone in esecuzione prolungata e svolgere il lavoro.
Il quadro completo
Ok, basta chiacchiere. È il momento di capire come lavorano in armonia tutti i pezzi del puzzle che hai appena conosciuto. Prima di tuffarci nelle spiegazioni di ciò che accade realmente quando esegui il comando docker run hello-world
, ecco un piccolo diagramma che ho creato:
Questa immagine è una versione leggermente modificata di quella presente nella documentazione ufficiale. Gli eventi che si verificano quando esegui il comando sono i seguenti:
- Esegui il comando
docker run hello-world
dovehello-world
è il nome dell'immagine. - Il client di Docker comunica con il demone, dicendogli di prendere l'immagine
hello-world
ed eseguire un container per questa immagine. - Docker daemon cerca l'immagine nel tuo repository locale e si rende conto che non c'è, restituendo
Unable to find image 'hello-world:latest' locally
stampato sul terminale. - Il demone poi comunica con il registro pubblico, Docker Hub, e scarica l'ultima copia dell'immagine
hello-world
, indicata dalla rigalatest: Pulling from library/hello-world
nel terminale. - Docker daemon poi crea un nuovo container dall'immagine appena scaricata.
- Infine Docker daemon esegue il container creato usando l'immagine
hello-world
generando come output il muro di testo sul tuo terminale.
Cercare nell'hub le immagini non presenti localmente è il comportamento predefinito di Docker. Ma una volta che l'immagine è stata recuperata, starà nella cache locale. Quindi, se esegui ancora il comando, non vedrai le seguenti righe di output:
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
0e03bdcc26d7: Pull complete
Digest: sha256:d58e752213a51785838f9eed2b7a498ffa1cb3aa7f946dda11af39286c3db9a9
Status: Downloaded newer image for hello-world:latest
Se c'è una nuova versione dell'immagine disponibile nel registro pubblico, il demone recupererà di nuovo l'immagine. :latest
è un tag. Le immagini hanno solitamente dei tag significativi per indicare le versioni o i build. Ne riparleremo in dettaglio più avanti.
Le basi della manipolazione dei container in Docker
Nelle sezioni precedenti, hai imparato gli elementi costitutivi di Docker e hai anche eseguito un container usando il comando docker run
.
In questa sezione, apprenderai come manipolare i container nel dettaglio. La manipolazione dei container è l'attività più comune che ti troverai a svolgere quotidianamente, quindi capire bene i vari comandi è cruciale.
Tieni a mente che questo non è un elenco esaustivo di tutti i comandi che puoi eseguire su Docker. Riporterò solo quelli più comuni. In qualsiasi momento tu voglia imparare di più sui comandi disponibili, visita i riferimenti ufficiali per la riga di comando Docker.
Come eseguire un container
Precedentemente hai usato docker run
per creare e avviare un container usando l'immagine hello-world
. La sintassi generica di questo comando è la seguente:
docker run <nome immagine>
Sebbene questo sia un comando perfettamente valido, c'è un modo migliore di inviare comandi al demone.
Prima della versione 1.13
, Docker aveva soltanto la sintassi precedentemente menzionata per quel comando. Successivamente, la riga di comando è stata ristrutturata per avere la seguente sintassi:
docker <oggetto> <comando> <opzioni>
In questa sintassi:
oggetto
indica il tipo di oggetto Docker da manipolare. Può essere un oggettocontainer
,image
,network
ovolume
.comando
indica l'azione che deve essere svolta dal demone, cioè il comandorun
.opzioni
può essere qualsiasi parametro valido che può sovrascrivere il comportamento predefinito del comando, come l'opzione--publish
per il port mapping.
Seguendo questa sintassi, il comando run
può essere scritto come segue:
docker container run <nome immagine>
nome immagine
può appartenere a qualsiasi immagine da un registro online o sul tuo sistema locale. Come esempio, puoi provare a eseguire il container usando l'immagine fhsinchy/hello-dock. Questa immagine contiene una semplice applicazione Vue.js che viene eseguita sulla porta 80 all'interno del container.
Per eseguire un container usando questa immagine, esegui il seguente comando sul terminale:
docker container run --publish 8080:80 fhsinchy/hello-dock
# /docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
# /docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
# /docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
# 10-listen-on-ipv6-by-default.sh: Getting the checksum of /etc/nginx/conf.d/default.conf
# 10-listen-on-ipv6-by-default.sh: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
# /docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
# /docker-entrypoint.sh: Configuration complete; ready for start up
Il comando è piuttosto esplicito. L'unica porzione che potrebbe richiedere qualche spiegazione è --publish 8080:80
, di cui parleremo nella prossima sezione.
Come pubblicare una porta
I container sono ambienti isolati. Il tuo sistema host non sa nulla di ciò che accade in un container. Dunque le applicazioni all'interno di un container restano inaccessibili dall'esterno.
Per consentire l'accesso dall'esterno di un container, devi pubblicare la porta appropriata all'interno del container su una porta sulla tua rete locale. La sintassi comune per l'opzione --publish
o -p
è la seguente:
--publish <porta host>:<porta container>
--publish 8080:80
nella sezione precedente, vuol dire che ogni richiesta inviata alla porta 8080 del sistema host viene inoltrata alla porta 80 all'interno del container.
Per accedere all'applicazione sul browser, visita http://127.0.0.1:8080
.

Puoi stoppare il container semplicemente premendo la combinazione di tasti ctrl + c
mentre la finestra del terminale è in focus, oppure chiudere completamente la finestra del terminale.
Come usare l'opzione detach
Un'altra opzione molto popolare del comando run
è --detach
o -d
. Nell'esempio precedente, per mantenere il container in esecuzione hai tenuto aperta la finestra del terminale. Chiudere la finestra del terminale avrebbe interrotto l'esecuzione del container.
Questo perché, per impostazione predefinita, i container vengono eseguiti in primo piano e si collegano al terminale come qualsiasi altro normale programma invocato del terminale.
Per sovrascrivere questo comportamento e mantenere il container in esecuzione in background, puoi includere l'opzione --detach
con il comando run
come segue:
docker container run --detach --publish 8080:80 fhsinchy/hello-dock
# 9f21cb77705810797c4b847dbd330d9c732ffddba14fb435470567a7a3f46cdc
A differenza dell'esempio precedente, stavolta non otterrai un muro di testo. Invece, otterrai l'ID del container appena creato.
L'ordine delle opzioni che fornisci non è davvero importante. Se metti l'opzione --publish
prima dell'opzione --detach
, funziona allo stesso modo. Una cosa da tenere a mente per il comando run
è che il nome dell'immagine deve essere inserito per ultimo. Se aggiungi qualsiasi cosa dopo il nome dell'immagine, sarà passato come argomento all'entry-point del container (vedi sezione Come eseguire comandi all'interno di un container) e potrebbe creare situazioni inaspettate.
Come elencare i container
Il comando container ls
può essere usato per elencare i container attualmente in esecuzione:
docker container ls
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# 9f21cb777058 fhsinchy/hello-dock "/docker-entrypoint.…" 5 seconds ago Up 5 seconds 0.0.0.0:8080->80/tcp gifted_sammet
Un container chiamato gifted_sammet
è in esecuzione. È stato creato 5 secondi fa (5 seconds ago
) e lo status è Up 5 seconds,
che indica che il container è stato correttamente in esecuzione dal momento della sua creazione.
CONTAINER ID
è 9f21cb777058
, i primi 12 caratteri dell'ID completo del container. L'ID completo è 9f21cb77705810797c4b847dbd330d9c732ffddba14fb435470567a7a3f46cdc
, che è lungo 64 caratteri ed è stato stampato come output del comando docker container run
nella sezione precedente.
La porta 8080 della tua rete locale, elencata nella colonna PORTS
, sta puntando alla porta 80 all'interno del container. Il nome gifted_sammet
è generato da Docker e può essere completamente diverso nel tuo computer.
Il comando container ls
elenca solo i container attualmente in esecuzione sul tuo sistema. Per elencare tutti i container eseguiti in passato puoi usare l'opzione --all
o -a
.
docker container ls --all
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# 9f21cb777058 fhsinchy/hello-dock "/docker-entrypoint.…" 2 minutes ago Up 2 minutes 0.0.0.0:8080->80/tcp gifted_sammet
# 6cf52771dde1 fhsinchy/hello-dock "/docker-entrypoint.…" 3 minutes ago Exited (0) 3 minutes ago reverent_torvalds
# 128ec8ceab71 hello-world "/hello" 4 minutes ago Exited (0) 4 minutes ago exciting_chebyshev
Come puoi vedere il secondo container nella lista, reverent_torvalds
, è stato creato prima e ha il codice di stato 0, che indica che non è stato prodotto nessun errore durante il runtime del container.
Come dare un nome a un container o rinominarlo
Di default, ogni container ha due identificatori:
- ID - una stringa composta da un numero casuale di 64 caratteri.
- Nome - una combinazione di due parole casuali unite da un trattino basso.
Fare riferimento a un container sulla base di due identificatori casuali è piuttosto sconveniente. Sarebbe ottimale poter riferirsi ai container usando un nome definito da te.
È possibile dare un nome a un container usando l'opzione --name
. Per eseguire un altro container usando l'immagine fhsinchy/hello-dock
con il nome hello-dock-container
puoi eseguire il seguente comando:
docker container run --detach --publish 8888:80 --name hello-dock-container fhsinchy/hello-dock
# b1db06e400c4c5e81a93a64d30acc1bf821bed63af36cab5cdb95d25e114f5fb
La porta 8080 sulla rete locale è occupata dal container gifted_sammet
(creato nella sezione precedente). Ecco perché devi utilizzare un numero di porta diverso, come 8888. Ora fai una verifica con il comando container ls
:
docker container ls
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# b1db06e400c4 fhsinchy/hello-dock "/docker-entrypoint.…" 28 seconds ago Up 26 seconds 0.0.0.0:8888->80/tcp hello-dock-container
# 9f21cb777058 fhsinchy/hello-dock "/docker-entrypoint.…" 4 minutes ago Up 4 minutes 0.0.0.0:8080->80/tcp gifted_sammet
È stato avviato un nuovo container con il nome hello-dock-container
.
Puoi rinominare vecchi container usando il comando container rename
. La sintassi per questo comando è la seguente:
docker container rename <identificatore container> <nuovo nome>
Per rinominare il container gifted_sammet
in hello-dock-container-2
, esegui il seguente comando:
docker container rename gifted_sammet hello-dock-container-2
Il comando non dà nessun output ma puoi verificare che i cambiamenti hanno avuto luogo usando il comando container ls
. Il comando rename
funziona sia per i container in esecuzione che per quelli stoppati.
Come fermare un container in esecuzione
I container in esecuzione in primo piano possono essere bloccati semplicemente chiudendo la finestra del terminale o premendo ctrl + c
. I container in esecuzione in background, tuttavia, non possono essere stoppati nello stesso modo.
Ci sono due comandi relativi a questa azione. Il primo è container stop
, la cui sintassi generica è riportata di seguito:
docker container stop <identificatore container>
Dove identificatore container
può essere sia l'id che il nome del container.
Spero ti ricordi del container che hai avviato nella sezione precedente. È ancora in esecuzione in background. Ottieni l'identificatore del container usando docker container ls
(utilizzerò il container hello-dock-container
per questa demo). Ora esegui il seguente comando per fermare l'esecuzione del container:
docker container stop hello-dock-container
# hello-dock-container
Se usi il nome come identificatore, otterrai il nome come output. Il comando stop
ferma l'esecuzione di un container con grazia inviando un segnale SIGTERM
. Se il container non si ferma entro un certo periodo, viene inviato un segnale SIGKILL
che blocca immediatamente il container.
Nei casi in cui vuoi inviare un segnale SIGKILL
invece di un segnale SIGTERM
, puoi usare il comando container kill
, che segue la stessa sintassi del comando stop
.
docker container kill hello-dock-container-2
# hello-dock-container-2
Come riavviare un container
Parlando di riavviare, intendo specificamente due scenari:
- Riavviare un container che è stato precedentemente stoppato.
- Riavviare un container in esecuzione.
Come hai già imparato da una sezione precedente, i container stoppati restano nel tuo sistema. Se vuoi puoi riavviarli. Il comando container start
può essere usato per avviare qualsiasi container stoppato o "ucciso". La sintassi è la seguente:
docker container start <identificatore container>
Puoi ottenere la lista di tutti i container eseguendo il comando container ls --all
. Poi guarda i contained con lo status Exited
.
docker container ls --all
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# b1db06e400c4 fhsinchy/hello-dock "/docker-entrypoint.…" 3 minutes ago Exited (0) 47 seconds ago hello-dock-container
# 9f21cb777058 fhsinchy/hello-dock "/docker-entrypoint.…" 7 minutes ago Exited (137) 17 seconds ago hello-dock-container-2
# 6cf52771dde1 fhsinchy/hello-dock "/docker-entrypoint.…" 7 minutes ago Exited (0) 7 minutes ago reverent_torvalds
# 128ec8ceab71 hello-world "/hello" 9 minutes ago Exited (0) 9 minutes ago exciting_chebyshev
Ora per riavviare il container hello-dock-container
, puoi eseguire il seguente comando:
docker container start hello-dock-container
# hello-dock-container
Puoi assicurarti che il contenitore sia in esecuzione guardando la lista dei container in esecuzione usando il comando container ls
.
Il comando container start
avvia qualsiasi container in modalità detached di default e mantiene ogni configurazione di porte fatta in precedenza. Quindi se visiti http://127.0.0.1:8080
, dovresti essere in grado di accedere all'applicazione hello-dock
proprio come prima.

In una situazione in cui desideri riavviare un container in esecuzione puoi usare il comando container restart
, che segue la stessa sintassi del comando container start
.
docker container restart hello-dock-container-2
# hello-dock-container-2
La differenza principale tra i due comandi è che il comando container restart
tenta di fermare il container selezionato e poi lo riavvia, mentre il comando start riavvia un container già bloccato.
Nel caso di un container stoppato, entrambi i comandi vanno bene. Ma se il container è in esecuzione devi usare il comando container restart
.
Come creare un container senza eseguirlo
In questa sezione, finora, hai avviato container usando il comando container run
, che in realtà è la combinazione di due comandi separati:
container create
crea un container da un'immagine.container start
avvia un container che è stato già creato.
Per svolgere la dimostrazione mostrata in precedenza usando questi due comandi, puoi fare qualcosa del genere:
docker container create --publish 8080:80 fhsinchy/hello-dock
# 2e7ef5098bab92f4536eb9a372d9b99ed852a9a816c341127399f51a6d053856
docker container ls --all
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# 2e7ef5098bab fhsinchy/hello-dock "/docker-entrypoint.…" 30 seconds ago Created hello-dock
Come si evince dall'output del comando container ls --all
, è stato creato un container con il nome hello-dock
usando l'immagine fhsinchy/hello-dock
. Al momento lo STATUS
del container è Created
e, dato che non è in esecuzione, non verrà messo in elenco senza usare l'opzione --all
.
Una volta che il container è stato creato, può essere avviato usando il comando container start
.
docker container start hello-dock
# hello-dock
docker container ls
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# 2e7ef5098bab fhsinchy/hello-dock "/docker-entrypoint.…" About a minute ago Up 29 seconds 0.0.0.0:8080->80/tcp hello-dock
Lo STATUS
del container è cambiato da Created
a Up 29 seconds
, che indica che il container è in esecuzione. La configurazione delle porte viene mostrata nella colonna PORTS
che era precedentemente vuota.
Sebbene tu possa farla franca con il comando container run
nella maggior parte delle situazioni, più avanti nel manuale ci saranno alcuni casi che richiedono di usare il comando container create
.
Come rimuovere dei container sospesi
Come hai già visto, i container che hai stoppato o terminato restano nel sistema. Questi container sospesi possono occupare spazio o entrare in conflitto con dei nuovi container.
Per rimuovere un container stoppato puoi usare il comando container rm
, con la seguente sintassi:
docker container rm <identificatore container>
Per trovare quali container non sono in esecuzione, usa il comando container ls --all
e cerca i container con lo status Exited
.
docker container ls --all
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# b1db06e400c4 fhsinchy/hello-dock "/docker-entrypoint.…" 6 minutes ago Up About a minute 0.0.0.0:8888->80/tcp hello-dock-container
# 9f21cb777058 fhsinchy/hello-dock "/docker-entrypoint.…" 10 minutes ago Up About a minute 0.0.0.0:8080->80/tcp hello-dock-container-2
# 6cf52771dde1 fhsinchy/hello-dock "/docker-entrypoint.…" 10 minutes ago Exited (0) 10 minutes ago reverent_torvalds
# 128ec8ceab71 hello-world "/hello" 12 minutes ago Exited (0) 12 minutes ago exciting_chebyshev
Come puoi vedere nell'output, i container con gli ID 6cf52771dde1
e 128ec8ceab71
non sono in esecuzione. Per rimuovere il container 6cf52771dde1
esegui il seguente comando:
docker container rm 6cf52771dde1
# 6cf52771dde1
Puoi controllare se il container è stato cancellato oppure no usando il comando container ls
. Puoi anche rimuovere più container in un colpo solo passando i loro identificatori uno dopo l'altro separati da spazi.
Oppure, invece di rimuovere singoli container, se vuoi rimuovere tutti i container sospesi insieme, puoi usare il comando container prune
.
Puoi controllare la lista dei container con il comando container ls --all
per assicurarti che i container sospesi siano stati rimossi:
docker container ls --all
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# b1db06e400c4 fhsinchy/hello-dock "/docker-entrypoint.…" 8 minutes ago Up 3 minutes 0.0.0.0:8888->80/tcp hello-dock-container
# 9f21cb777058 fhsinchy/hello-dock "/docker-entrypoint.…" 12 minutes ago Up 3 minutes 0.0.0.0:8080->80/tcp hello-dock-container-2
Se finora hai seguito il libro esattamente, dovresti vedere in elenco soltanto hello-dock-container
e hello-dock-container-2
. Consiglio di fermare e rimuovere entrambi i container prima di proseguire con la prossima sezione.
Esiste anche l'opzione --rm
per i comandi container run
e container start
, che indica che vuoi che i container siano rimossi non appena vengono stoppati. Per avviare un altro container hello-dock
con l'opzione --rm
, esegui il seguente comando:
docker container run --rm --detach --publish 8888:80 --name hello-dock-volatile fhsinchy/hello-dock
# 0d74e14091dc6262732bee226d95702c21894678efb4043663f7911c53fb79f3
Puoi usare il comando container ls
per verificare che il container è in esecuzione:
docker container ls
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# 0d74e14091dc fhsinchy/hello-dock "/docker-entrypoint.…" About a minute ago Up About a minute 0.0.0.0:8888->80/tcp hello-dock-volatile
Se blocchi il container e poi verifichi di nuovo con il comando container ls --all
:
docker container stop hello-dock-volatile
# hello-dock-volatile
docker container ls --all
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
Il container è stato rimosso automaticamente. D'ora in poi, userò l'opzione --rm
per la maggior parte dei container. Menzionerò esplicitamente qualora non dovesse essere necessaria.
Come eseguire un container in modalità interattiva
Finora abbiamo eseguito solo container creati dalle immagini hello-world fhsinchy/hello-dock. Queste immagini sono realizzate per eseguire semplici programmi che non sono interattivi.
Beh, non tutte le immagini sono così semplici. Le immagini possono incapsulare un'intera distribuzione Linux al loro interno.
Distribuzioni popolari come Ubuntu, Fedora e Debian hanno tutte delle immagini Docker ufficiali disponibili nell'hub. Linguaggi di programmazione come python, php, go oppure runtime come node e deno hanno tutti delle immagini ufficiali.
Queste immagini non solo permettono di eseguire programmi pre-configurati. Sono anche configurate per eseguire una shell di default. Nel caso di immagini di sistemi operativi, può trattarsi di sh
o bash
e per linguaggi di programmazione o run-time, solitamente è il loro linguaggio shell predefinito.
Come puoi già sapere dalle tue esperienze con i computer, le shell sono programmi interattivi. Un'immagine configurata per eseguire un programma è un'immagine interattiva. Queste immagini richiedono un'opzione speciale -it
da passare al comando container run
.
Ad esempio, se esegui un container usando l'immagine di ubuntu
eseguendo docker container run ubuntu
, vedrai che non accade nulla. Ma se esegui lo stesso comando con l'opzione -it
, dovresti arrivare direttamente sul bash all'interno del container di Ubuntu.
docker container run --rm -it ubuntu
# root@dbb1f56b9563:/# cat /etc/os-release
# NAME="Ubuntu"
# VERSION="20.04.1 LTS (Focal Fossa)"
# ID=ubuntu
# ID_LIKE=debian
# PRETTY_NAME="Ubuntu 20.04.1 LTS"
# VERSION_ID="20.04"
# HOME_URL="https://www.ubuntu.com/"
# SUPPORT_URL="https://help.ubuntu.com/"
# BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
# PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
# VERSION_CODENAME=focal
# UBUNTU_CODENAME=focal
Come puoi vedere dall'output del comando cat /etc/os-release
, sto effettivamente interagendo con il bash in esecuzione nel container di Ubuntu.
L'opzione -it
prepara il terreno in modo che tu possa interagire con qualsiasi programma interattivo all'interno di un container. Questa opzione, in realtà, è composta da due opzioni fuse insieme.
- L'opzione
-i
o--interactive
ti connette al flusso di input del container, in modo da poter inviare input al bash. - L'opzione
-t
o--tty
fa sì che tu ottenga una buona formattazione e l'esperienza del terminale nativo allocando uno pseudo-terminale.
Devi usare l'opzione -it
ogni volta che vuoi eseguire un container in modalità interattiva. Un altro esempio può essere l'esecuzione dell'immagine di node
come segue:
docker container run -it node
# Welcome to Node.js v15.0.0.
# Type ".help" for more information.
# > ['farhan', 'hasin', 'chowdhury'].map(name => name.toUpperCase())
# [ 'FARHAN', 'HASIN', 'CHOWDHURY' ]
Qualsiasi codice JavaScript valido può essere eseguito nella shell di node. Invece di scrivere -it
puoi essere più verboso e scrivere separatamente --interactive --tty
.
Come eseguire comandi all'interno di un container
Nella sezione Hello World in Docker di questo manuale, mi hai visto eseguire un comando all'interno di un container di Alpine Linux. È andata più o meno così:
docker run alpine uname -a
# Linux f08dbbe9199b 5.8.0-22-generic #23-Ubuntu SMP Fri Oct 9 00:34:40 UTC 2020 x86_64 Linux
Ho eseguito il comando uname -a
all'interno di un container di Alpine Linux. Casi come questo (dove tutto ciò che desideri è eseguire un certo comando all'interno di un container) sono piuttosto comuni.
Immagina di voler codificare una stringa usando il programma base64
, disponibile su quasi ogni sistema operativo Linux o Unix (ma non su Windows).
In questa situazione, puoi avviare un container usando immagini come busybox e fargli fare il lavoro.
La sintassi generica per codificare una stringa usando base64
è la seguente:
echo -n my-secret | base64
# bXktc2VjcmV0
E la sintassi generica per passare un comando a un container che non è in esecuzione è la seguente:
docker container run <nome immagine> <comando>
Per svolgere la codifica base64 usando l'immagine, puoi eseguire il seguente comando:
docker container run --rm busybox sh -c "echo -n my-secret | base64
# bXktc2VjcmV0
In un comando container run
, qualsiasi cosa passi dopo il nome dell'immagine viene passata all'entry-point predefinito dell'immagine.
Un entry-point è come un gateway all'immagine. La maggior parte dell'immagini, eccetto le immagini eseguibili (spiegate nella sezione Come lavorare con immagini eseguibili), usano la shell o sh
come entry-point predefinito. Quindi ogni comando shell valido può essere passato come argomento.
Come lavorare con immagini eseguibili
Nella sezione precedente, ho menzionato brevemente le immagini eseguibili. Queste immagini sono progettate per comportarsi come programmi eseguibili.
Prendi come esempio il mio progetto rmbyext. Si tratta di un semplice script Python in grado di cancellare file di una data estensione in modo ricorsivo. Per saperne di più, dai un'occhiata al repository:
Se hai sia Git che Python installati, puoi installare questo script eseguendo il seguente comando:
pip install git+https://github.com/fhsinchy/rmbyext.git#egg=rmbyext
Assumendo che Python sia configurato correttamente nel tuo sistema, lo script dovrebbe essere disponibile ovunque tramite il terminale. La sintassi generica per questo script è la seguente:
rmbyext <estensione file>
Per fare una prova, apri il terminale all'interno di una cartella vuota e crea dei file con diverse estensioni. Per farlo puoi usare il comando touch
. Ora ho una cartella sul mio computer con i seguenti file:
touch a.pdf b.pdf c.txt d.pdf e.txt
ls
# a.pdf b.pdf c.txt d.pdf e.txt
Per cancellare tutti i file pdf
da questa cartella, puoi eseguire il seguente comando:
rmbyext pdf
# Removing: PDF
# b.pdf
# a.pdf
# d.pdf
Un'immagine eseguibile per questo programma dovrebbe essere in grado di prendere le estensioni dei file come argomenti e cancellarli proprio come ha fatto il programma rmbyext
.
L'immagine fhsinchy/rmbyext si comporta in modo simile. Questa immagine contiene una copia dello script rmbyext
ed è configurata per eseguire lo script in una cartella /zone
all'interno del container.
Ora il problema è che i container sono isolati dal sistema locale, quindi il programma rmbyext
in esecuzione all'interno del container non ha accesso al file system locale. Se in qualche modo potessi creare una corrispondenza tra la cartella locale contenente i file pdf
e la cartella /zone
all'interno del container, i file sarebbero accessibili al container.
Un modo per garantire a un container l'accesso diretto al file system locale è usare un bind mount.
Un bind mount ti consente di formare un collegamento a due vie tra il contenuto di un file system della cartella locale (sorgente) e un'altra cartella all'interno di un container (destinazione). In questo modo, ogni modifica effettuata nella cartella di destinazione avrà effetto sulla cartella sorgente e viceversa.
Vediamo un bind mount in azione. Per eliminare i file usando questa immagine, invece del programma stesso, puoi eseguire il seguente comando:
docker container run --rm -v $(pwd):/zone fhsinchy/rmbyext pdf
# Removing: PDF
# b.pdf
# a.pdf
# d.pdf
Come potresti aver già indovinato vedendo la parte -v $(pwd):/zone
del comando, l'opzione -v
o --volume
viene usata per creare un bind mount per un container. Questa opzione accetta tre campi separati da due punti (:
). La sintassi generica per l'opzione è:
--volume <percorso assoluto cartella file system locale>:<percorso assoluto cartella file system container>:<accesso read write>
Il terzo campo è opzionale ma occorre passare il percorso assoluto della directory locale e quello della directory nel container.
La cartella sorgente nel mio caso è /home/fhsinchy/the-zone
. Dato che il mio terminale è aperto nella cartella, $(pwd)
sarà sostituito con /home/fhsinchy/the-zone
, che contiene i file .pdf
e .txt
precedentemente menzionati.
Se vuoi saperne di più sulla sostituzione dei comandi puoi consultare questa risorsa (in inglese).
L'opzione --volume
o -v
è valida per container run
così come per il comando container create
. Esploreremo i volumi in maggiore dettaglio nelle prossime sezioni, quindi non ti preoccupare se non li comprendi ancora bene qui.
La differenza tra un'immagine normale e una eseguibile è che l'entry-point di un'immagine eseguibile è impostato su un programma personalizzato invece di sh
, in questo caso il programma rmbyext
. E come hai imparato nelle sezioni precedenti, tutto ciò che scrivi dopo il nome dell'immagine in un comando container run
viene passato all'entry-point dell'immagine.
Quindi, alla fine, il comando docker container run --rm -v $(pwd):/zone fhsinchy/rmbyext pdf
si traduce in rmbyext pdf
all'interno del container. Le immagini eseguibili non sono così comuni ma possono essere utili in certi casi.
Manipolazione base delle immagini Docker
Ora che hai una buona comprensione di come eseguire dei container usando immagini disponibili pubblicamente, è tempo di imparare come creare le tue immagini.
In questa sezione, imparerai i fondamenti di come creare immagini, usarle per eseguire container e condividerle online.
Consiglio di installare Visual Studio Code con l'estensione Docker ufficiale dal marketplace. È un grande aiuto per l'esperienza di sviluppo.
Come creare un'immagine Docker
Come ho già spiegato nella sezione Hello World in Docker, le immagini sono dei file indipendenti a più livelli che agiscono come modello per creare i container Docker. Sono come una copia congelata, di sola lettura di un container.
Per creare un'immagine usando uno dei tuoi programmi, devi avere una chiara visione di cosa vuoi dall'immagine. Prendi l'immagine ufficiale di nginx come esempio. Puoi avviare un container usando questa immagine eseguendo il seguente comando:
docker container run --rm --detach --name default-nginx --publish 8080:80 nginx
# b379ecd5b6b9ae27c144e4fa12bdc5d0635543666f75c14039eea8d5f38e3f56
docker container ls
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# b379ecd5b6b9 nginx "/docker-entrypoint.…" 8 seconds ago Up 8 seconds 0.0.0.0:8080->80/tcp default-nginx
Se vai su http://127.0.0.1:8080
nel browser, vedrai una pagina di risposta predefinita.

Tutto perfetto, ma se volessimo realizzare un'immagine personalizzata di NGINX che funzioni esattamente come quella ufficiale? Onestamente, è una situazione assolutamente plausibile. Quindi facciamolo.
Per creare un'immagine NGINX personalizzata, devi avere un'idea chiara dello stato finale dell'immagine. Secondo me, l'immagine dovrebbe essere come segue:
- L'immagine dovrebbe avere NGINX preinstallato, il che può essere fatto usando un gestore di pacchetti o può essere generato dalla sorgente.
- L'immagine dovrebbe avviare automaticamente NGINX quando in esecuzione.
Tutto semplice. Se hai clonato il repository del progetto linkato in questo libro, vai all'interno della radice del progetto e cerca una cartella chiamata custom-nginx
.
Ora crea un nuovo file chiamato Dockerfile
all'interno di quella cartella. Dockerfile
è un insieme di istruzioni che, una volta processate dal demone, danno come risultato un'immagine. Il contenuto di Dockerfile
è il seguente:
FROM ubuntu:latest
EXPOSE 80
RUN apt-get update && \
apt-get install nginx -y && \
apt-get clean && rm -rf /var/lib/apt/lists/*
CMD ["nginx", "-g", "daemon off;"]
Le immagini sono file a più livelli e in questo file, ogni riga (detta istruzione) che scrivi crea un livello dell'immagine.
- Ogni
Dockerfile
valido inizia con l'istruzioneFROM
. Questa istruzione imposta la base dell'immagine risultante. Impostandoubuntu:latest
come immagine base, ottieni tutte le qualità di Ubuntu già disponibili nella tua immagine personalizzata, così da poter usare cose come il comandoapt-get
per installare facilmente i pacchetti. - L'istruzione
EXPOSE
è usata per indicare la porta che deve essere pubblicata. Usare questa istruzione non vuol dire che non c'è bisogno di usare esplicitamente l'opzione--publish
. L'istruzioneEXPOSE
funziona esattamente come una documentazione per qualcuno che sta cercando di eseguire un container usando la tua immagine. Ha anche altri utilizzi che non discuterò qui. - L'istruzione
RUN
in unDockerfile
esegue un comando all'interno della shell. Il comandoapt-get update && apt-get install nginx -y
controlla le versioni aggiornate del pacchetto e installa NGINX. Il comandoapt-get clean && rm -rf /var/lib/apt/lists/*
è usato per pulire la cache del pacchetto, per non avere fardelli inutili nell'immagine. Questi due sono semplici comandi di Ubuntu, nulla di complesso. Le istruzioniRUN
qui sono scritte in formashell
. Possono anche essere scritte in formaexec
. Per saperne di più, puoi consultare i riferimenti ufficiali. - Infine, l'istruzione
CMD
definisce il comando di default per l'immagine. Questa istruzione è scritta in formaexec
, costituita da tre parti separate.nginx
si riferisce all'eseguibile NGINX.-g
edaemon off
sono opzioni per NGINX. Eseguire NGINX come singolo processo all'interno del container è considerata una buona pratica, da cui l'utilizzo di questa opzione. L'istruzioneCMD
Può ancora essere scritta in formashell
. Per saperne di più, puoi consultare i riferimenti ufficiali.
Ora che hai un Dockerfile
valido, da questo puoi costruire un'immagine. Proprio come i comandi relativi ai container, i comandi per le immagini possono essere lanciati usando la seguente sintassi:
docker image <comando> <opzioni>
Per costruire un'immagine usando il Dockerfile
che hai scritto, apri il terminale all'interno della cartella custom-nginx
ed esegui il seguente comando:
docker image build .
# Sending build context to Docker daemon 3.584kB
# Step 1/4 : FROM ubuntu:latest
# ---> d70eaf7277ea
# Step 2/4 : EXPOSE 80
# ---> Running in 9eae86582ec7
# Removing intermediate container 9eae86582ec7
# ---> 8235bd799a56
# Step 3/4 : RUN apt-get update && apt-get install nginx -y && apt-get clean && rm -rf /var/lib/apt/lists/*
# ---> Running in a44725cbb3fa
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container a44725cbb3fa
# ---> 3066bd20292d
# Step 4/4 : CMD ["nginx", "-g", "daemon off;"]
# ---> Running in 4792e4691660
# Removing intermediate container 4792e4691660
# ---> 3199372aa3fc
# Successfully built 3199372aa3fc
Per eseguire il build di un'immagine, il demone ha bisogno di due informazioni molto specifiche, ovvero il nome del Dockerfile
e il contesto di build. Nel comando precedente:
docker image build
è il comando per eseguire il build dell'immagine. Il demone trova qualsiasi file chiamatoDockerfile
all'interno del contesto.- Il
.
alla fine definisce il contesto per questo build. Il contesto corrisponde alla cartella accessibile al demone durante il processo di build.
Adesso, per eseguire un container usando questa immagine, puoi usare il comando container run
abbinato con l'ID dell'immagine che hai ottenuto come risultato del processo di build. Nel mio caso, l'id è 3199372aa3fc
, visibile nella riga Successfully built 3199372aa3fc
del precedente blocco di codice.
docker container run --rm --detach --name custom-nginx-packaged --publish 8080:80 3199372aa3fc
# ec09d4e1f70c903c3b954c8d7958421cdd1ae3d079b57f929e44131fbf8069a0
docker container ls
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# ec09d4e1f70c 3199372aa3fc "nginx -g 'daemon of…" 23 seconds ago Up 22 seconds 0.0.0.0:8080->80/tcp custom-nginx-packaged
Per controllare, visita http://127.0.0.1:8080
, dove dovresti vedere la pagina di risposta predefinita.

Come taggare le immagini Docker
Proprio come per i container, puoi assegnare identificatori personalizzati alle tue immagini invece di fare affidamento sugli ID generati casualmente. Nel caso di un'immagine, l'identificatore viene detto tag. L'opzione --tag
o -t
viene usata in tali casi.
La sintassi generica per questa opzione è la seguente:
--tag <repository immagine>:<tag immagine>
Il repository è solitamente conosciuto come il nome dell'immagine e il tag indica un certo build o versione.
Prendi l'immagine ufficiale di mysql come esempio. Se vuoi eseguire un container usando una specifica versione di MySQL, come la 5.7, puoi eseguire docker container run mysql:5.7
dove mysql
è il repository dell'immagine e 5.7
è il tag.
Per taggare l'immagine personalizzata NGINX con custom-nginx:packaged
puoi eseguire il seguente comando:
docker image build --tag custom-nginx:packaged .
# Sending build context to Docker daemon 1.055MB
# Step 1/4 : FROM ubuntu:latest
# ---> f63181f19b2f
# Step 2/4 : EXPOSE 80
# ---> Running in 53ab370b9efc
# Removing intermediate container 53ab370b9efc
# ---> 6d6460a74447
# Step 3/4 : RUN apt-get update && apt-get install nginx -y && apt-get clean && rm -rf /var/lib/apt/lists/*
# ---> Running in b4951b6b48bb
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container b4951b6b48bb
# ---> fdc6cdd8925a
# Step 4/4 : CMD ["nginx", "-g", "daemon off;"]
# ---> Running in 3bdbd2af4f0e
# Removing intermediate container 3bdbd2af4f0e
# ---> f8837621b99d
# Successfully built f8837621b99d
# Successfully tagged custom-nginx:packaged
Nulla cambierà eccetto il fatto che adesso puoi fare riferimento all'immagine con custom-nginx:packaged
invece di una lunga stringa casuale.
Nei casi in cui ti dimentichi di taggare un'immagine durante il tempo di build, o magari vuoi cambiare il tag, puoi usare il comando image tag
per farlo:
docker image tag <id immagine> <repository immagine>:<tag immagine>
## oppure ##
docker image tag <repository immagine>:<tag immagine> <nuovo repository immagine>:<nuovo tag immagine>
Come elencare e rimuovere immagini Docker
Proprio come il comando container ls
, puoi usare il comando image ls
per elencare tutte le immagini nel tuo sistema locale:
docker image ls
# REPOSITORY TAG IMAGE ID CREATED SIZE
# <none> <none> 3199372aa3fc 7 seconds ago 132MB
# custom-nginx packaged f8837621b99d 4 minutes ago 132MB
Le immagini elencate qui possono essere cancellate usando il comando image rm
, la cui sintassi generica è:
docker image rm <identificatore immagine>
L'identificatore può essere l'ID dell'immagine o il repository dell'immagine. Se usi il repository, devi specificare anche il tag. Per cancellare l'immagine personalizzata custom-nginx:packaged
, puoi eseguire il seguente comando:
docker image rm custom-nginx:packaged
# Untagged: custom-nginx:packaged
# Deleted: sha256:f8837621b99d3388a9e78d9ce49fbb773017f770eea80470fb85e0052beae242
# Deleted: sha256:fdc6cdd8925ac25b9e0ed1c8539f96ad89ba1b21793d061e2349b62dd517dadf
# Deleted: sha256:c20e4aa46615fe512a4133089a5cd66f9b7da76366c96548790d5bf865bd49c4
# Deleted: sha256:6d6460a744475a357a2b631a4098aa1862d04510f3625feb316358536fcd8641
Puoi anche usare il comando image prune
per fare pulizia di tutte le immagini sospese non taggate, come segue:
docker image prune --force
# Deleted Images:
# deleted: sha256:ba9558bdf2beda81b9acc652ce4931a85f0fc7f69dbc91b4efc4561ef7378aff
# deleted: sha256:ad9cc3ff27f0d192f8fa5fadebf813537e02e6ad472f6536847c4de183c02c81
# deleted: sha256:f1e9b82068d43c1bb04ff3e4f0085b9f8903a12b27196df7f1145aa9296c85e7
# deleted: sha256:ec16024aa036172544908ec4e5f842627d04ef99ee9b8d9aaa26b9c2a4b52baa
# Total reclaimed space: 59.19MB
L'opzione --force
o -f
salta ogni domanda di conferma. Puoi anche usare l'opzione --all
o -a
per rimuovere tutte le immagini nella cache del tuo registro locale.
Come comprendere i livelli di un'immagine Docker
Fin dall'inizio di questo libro, ho detto che le immagini sono file a più livelli. In questa sezione, mostrerò i vari livelli di un'immagine e come giocano un ruolo importante nel processo di build dell'immagine.
Per questa dimostrazione, utilizzerò l'immagine custom-nginx:packaged
della sezione precedente.
Per visualizzare i molti livelli di un'immagine, puoi usare il comando image history
. I vari livelli dell'immagine custom-nginx:packaged
possono essere visualizzati come segue:
docker image history custom-nginx:packaged
# IMAGE CREATED CREATED BY SIZE COMMENT
# 7f16387f7307 5 minutes ago /bin/sh -c #(nop) CMD ["nginx" "-g" "daemon… 0B
# 587c805fe8df 5 minutes ago /bin/sh -c apt-get update && apt-get ins… 60MB
# 6fe4e51e35c1 6 minutes ago /bin/sh -c #(nop) EXPOSE 80 0B
# d70eaf7277ea 17 hours ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B
# <missing> 17 hours ago /bin/sh -c mkdir -p /run/systemd && echo 'do… 7B
# <missing> 17 hours ago /bin/sh -c [ -z "$(apt-get indextargets)" ] 0B
# <missing> 17 hours ago /bin/sh -c set -xe && echo '#!/bin/sh' > /… 811B
# <missing> 17 hours ago /bin/sh -c #(nop) ADD file:435d9776fdd3a1834… 72.9MB
Ci sono otto livelli in questa immagine. Quello superiore è l'ultimo e man mano che scendi diventano meno recenti. Il livello superiore è quello che di solito si usa per eseguire i container.
Ora, diamo un'occhiata da vicino alle immagini partendo dall'immagine d70eaf7277ea
fino a 7f16387f7307
. Ignorerò i quattro livelli inferiori in cui IMAGE
è <missing>
(mancante), che non sono una nostra preoccupazione.
d70eaf7277ea
è stato creato da/bin/sh -c #(nop) CMD ["/bin/bash"]
che indica che la shell predefinita all'interno di Ubuntu è stata caricata con successo.6fe4e51e35c1
è stato creato da/bin/sh -c #(nop) EXPOSE 80
che era la seconda istruzione nel codice.587c805fe8df
è stato creato da/bin/sh -c apt-get update && apt-get install nginx -y && apt-get clean && rm -rf /var/lib/apt/lists/*
, la terza istruzione nel codice. Puoi anche vedere che questa immagine ha una dimensione di60MB
visti tutti i pacchetti che sono stati installati durante l'esecuzione di questa istruzione.- Infine, il livello più alto
7f16387f7307
è stato creato da/bin/sh -c #(nop) CMD ["nginx", "-g", "daemon off;"]
, che definisce il comando di default per quest'immagine.
Come puoi vedere, l'immagine comprende molti livelli di sola lettura, ognuno dei quali registra un nuovo set di cambiamenti allo stato innescati da determinate istruzioni. Quando avvii un container usando un'immagine, ottieni un nuovo livello scrivibile sopra agli altri livelli.
Questo fenomeno di stratificazione che avviene ogni volta che lavori con Docker è reso possibile da un magnifico concetto tecnico detto union file system. Union si riferisce all'unione nella teoria degli insiemi. Secondo Wikipedia:
Consente di sovrapporre in modo trasparente file e directory di file system separati, detti branch (rami), per formare un singolo file system coerente. I contenuti delle directory che hanno lo stesso percorso nei rami uniti, saranno visti come se fossero nella stessa directory del file system virtuale.
Utilizzando questo concetto, Docker può evitare la duplicazione dei dati e utilizzare i livelli precedentemente creati come cache per i build successivi. Ciò dà come risultato immagini compatte ed efficienti che possono essere usate ovunque.
Come eseguire il build di NGINX dalla sorgente
Nella sezione precedente, hai imparato le istruzioni FROM
, EXPOSE
, RUN
e CMD
. In questa sezione, parleremo molto di altre istruzioni.
Creeremo ancora un'immagine NGINX personalizzata. Ma invece di usare un gestore di pacchetti come apt-get
dell'esempio precedente, farai il build di NGINX dalla sorgente.
Per eseguire il build di NGINX dalla sorgente, hai bisogno del file sorgente di NGINX. Se hai clonato il mio repository, vedrai un file chiamato nginx-1.19.2.tar.gz
nella cartella custom-nginx
. Userai questo archivio come sorgente per fare il build di NGINX.
Prima di immergerci nel codice, pianifichiamo il processo. La creazione dell'immagine stavolta può essere fatta nei sette passaggi elencati di seguito:
- Ottenere una buona immagine di base per il build dell'applicazione, come ubuntu.
- Installare le dipendenze di build necessarie sull'immagine di base.
- Copiare il file
nginx-1.19.2.tar.gz
all'interno dell'immagine. - Estrarre i contenuti dell'archivio e liberarsene.
- Configurare il build, compilare e installare il programma usando lo strumento
make
. - Liberarsi del codice sorgente estratto.
- Lanciare l'eseguibile
nginx
.
Ora che abbiamo un piano, inizia aprendo il vecchio Dockerfile
, aggiornandone il contenuto come segue:
FROM ubuntu:latest
RUN apt-get update && \
apt-get install build-essential\
libpcre3 \
libpcre3-dev \
zlib1g \
zlib1g-dev \
libssl1.1 \
libssl-dev \
-y && \
apt-get clean && rm -rf /var/lib/apt/lists/*
COPY nginx-1.19.2.tar.gz .
RUN tar -xvf nginx-1.19.2.tar.gz && rm nginx-1.19.2.tar.gz
RUN cd nginx-1.19.2 && \
./configure \
--sbin-path=/usr/bin/nginx \
--conf-path=/etc/nginx/nginx.conf \
--error-log-path=/var/log/nginx/error.log \
--http-log-path=/var/log/nginx/access.log \
--with-pcre \
--pid-path=/var/run/nginx.pid \
--with-http_ssl_module && \
make && make install
RUN rm -rf /nginx-1.19.2
CMD ["nginx", "-g", "daemon off;"]
Come puoi vedere, il codice all'interno di Dockerfile
riflette i sette passaggi di cui ho parlato.
- L'istruzione
FROM
definisce Ubuntu come immagine di base creando un ambiente ideale per il build di qualsiasi applicazione. - L'istruzione
RUN
installa i pacchetti standard necessari per il build di NGINX dalla sorgente. - L'istruzione
COPY
è qualcosa di nuovo. Questa istruzione è responsabile di copiare il filenginx-1.19.2.tar.gz
all'interno dell'immagine. La sintassi generica dell'istruzioneCOPY
èCOPY <sorgente> <destinazione>
, dove la sorgente è nel tuo file system locale e la destinazione è all'interno dell'immagine..
come destinazione si riferisce alla cartella di lavoro all'interno dell'immagine che è di default/
, se non impostata diversamente. - La seconda istruzione
RUN
estrae il contenuto dall'archivio usandotar
e in seguito se ne sbarazza. - Il file di archivio contiene una cartella chiamata
nginx-1.19.2
con il codice sorgente. Quindi, nel prossimo passaggio, dovrai spostarti in questa directory (cd
) e svolgere il processo di build. Se vuoi imparare di più su questo argomento puoi leggere questo articolo (risorsa in inglese). - Una volta che il build e l'installazione sono completati, rimuovi la cartella
nginx-1.19.2
usando il comandorm
. - Nello step finale, avvia NGINX nella modalità a processo singolo, proprio come hai fatto prima.
Per fare il build di un'immagine usando questo codice, esegui il seguente comando:
docker image build --tag custom-nginx:built .
# Step 1/7 : FROM ubuntu:latest
# ---> d70eaf7277ea
# Step 2/7 : RUN apt-get update && apt-get install build-essential libpcre3 libpcre3-dev zlib1g zlib1g-dev libssl-dev -y && apt-get clean && rm -rf /var/lib/apt/lists/*
# ---> Running in 2d0aa912ea47
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container 2d0aa912ea47
# ---> cbe1ced3da11
# Step 3/7 : COPY nginx-1.19.2.tar.gz .
# ---> 7202902edf3f
# Step 4/7 : RUN tar -xvf nginx-1.19.2.tar.gz && rm nginx-1.19.2.tar.gz
---> Running in 4a4a95643020
### LONG EXTRACTION STUFF GOES HERE ###
# Removing intermediate container 4a4a95643020
# ---> f9dec072d6d6
# Step 5/7 : RUN cd nginx-1.19.2 && ./configure --sbin-path=/usr/bin/nginx --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --with-pcre --pid-path=/var/run/nginx.pid --with-http_ssl_module && make && make install
# ---> Running in b07ba12f921e
### LONG CONFIGURATION AND BUILD STUFF GOES HERE ###
# Removing intermediate container b07ba12f921e
# ---> 5a877edafd8b
# Step 6/7 : RUN rm -rf /nginx-1.19.2
# ---> Running in 947e1d9ba828
# Removing intermediate container 947e1d9ba828
# ---> a7702dc7abb7
# Step 7/7 : CMD ["nginx", "-g", "daemon off;"]
# ---> Running in 3110c7fdbd57
# Removing intermediate container 3110c7fdbd57
# ---> eae55f7369d3
# Successfully built eae55f7369d3
# Successfully tagged custom-nginx:built
Questo codice va bene, ma ci sono dei punti in cui possiamo fare dei miglioramenti.
- Invece di scrivere il nome del file in codifica fissa come
nginx-1.19.2.tar.gz
, puoi creare un argomento usando l'istruzioneARG
. In questo modo, sarai in grado di cambiare la versione o il nome del file cambiando l'argomento. - Invece di scaricare manualmente l'archivio, puoi far scaricare il file al demone durante il processo di build. Esiste un'altra istruzione come
COPY
, chiamataADD
, che ti consente di aggiungere dei file da internet.
Apri il file Dockerfile
e aggiorna il suo contenuto come segue:
FROM ubuntu:latest
RUN apt-get update && \
apt-get install build-essential\
libpcre3 \
libpcre3-dev \
zlib1g \
zlib1g-dev \
libssl1.1 \
libssl-dev \
-y && \
apt-get clean && rm -rf /var/lib/apt/lists/*
ARG FILENAME="nginx-1.19.2"
ARG EXTENSION="tar.gz"
ADD https://nginx.org/download/${FILENAME}.${EXTENSION} .
RUN tar -xvf ${FILENAME}.${EXTENSION} && rm ${FILENAME}.${EXTENSION}
RUN cd ${FILENAME} && \
./configure \
--sbin-path=/usr/bin/nginx \
--conf-path=/etc/nginx/nginx.conf \
--error-log-path=/var/log/nginx/error.log \
--http-log-path=/var/log/nginx/access.log \
--with-pcre \
--pid-path=/var/run/nginx.pid \
--with-http_ssl_module && \
make && make install
RUN rm -rf /${FILENAME}}
CMD ["nginx", "-g", "daemon off;"]
Il codice è quasi identico al blocco di codice precedente, eccetto per delle nuove istruzioni ARG
sulle righe 13, 14 e per l'uso dell'istruzione ADD
sulla riga 16. La spiegazione per il codice aggiornato è riportata di seguito:
- L'istruzione
ARG
ti permette di dichiarare variabili come in altri linguaggi. Queste variabili o argomenti possono essere consultate in seguito usando la sintassi${nome argomento}
. Qui ho usatonginx-1.19.2
come nome del file etar.gz
come estensione in due argomenti separati. In questo modo posso passare a nuove versioni di NGINX o al formato archivio facendo una modifica in un solo posto. Nel codice qui sopra, ho aggiunto i valori predefiniti alle variabili. I valori delle variabili possono anche essere passati come opzioni del comandoimage build
. Puoi consultare i riferimenti ufficiali per maggiori dettagli. - Nell'istruzione
ADD
, ho formato l'URL del download dinamicamente, usando gli argomenti dichiarati in precedenza. La rigahttps://nginx.org/download/${FILENAME}.${EXTENSION}
darà come risultatohttps://nginx.org/download/nginx-1.19.2.tar.gz
durante il processo di build. Puoi cambiare la versione del file e l'estensione soltanto in un punto grazie all'istruzioneARG
. - L'istruzione
ADD
non estrae i file ottenuti da internet in modo predefinito, per cui l'utilizzo ditar
nella riga 18.
Il resto del codice è praticamente invariato. Adesso dovresti essere in grado di capire da solo l'utilizzo degli argomenti. Infine, proviamo a fare il build dell'immagine da questo codice aggiornato.
docker image build --tag custom-nginx:built .
# Step 1/9 : FROM ubuntu:latest
# ---> d70eaf7277ea
# Step 2/9 : RUN apt-get update && apt-get install build-essential libpcre3 libpcre3-dev zlib1g zlib1g-dev libssl-dev -y && apt-get clean && rm -rf /var/lib/apt/lists/*
# ---> cbe1ced3da11
### LONG INSTALLATION STUFF GOES HERE ###
# Step 3/9 : ARG FILENAME="nginx-1.19.2"
# ---> Running in 33b62a0e9ffb
# Removing intermediate container 33b62a0e9ffb
# ---> fafc0aceb9c8
# Step 4/9 : ARG EXTENSION="tar.gz"
# ---> Running in 5c32eeb1bb11
# Removing intermediate container 5c32eeb1bb11
# ---> 36efdf6efacc
# Step 5/9 : ADD https://nginx.org/download/${FILENAME}.${EXTENSION} .
# Downloading [==================================================>] 1.049MB/1.049MB
# ---> dba252f8d609
# Step 6/9 : RUN tar -xvf ${FILENAME}.${EXTENSION} && rm ${FILENAME}.${EXTENSION}
# ---> Running in 2f5b091b2125
### LONG EXTRACTION STUFF GOES HERE ###
# Removing intermediate container 2f5b091b2125
# ---> 2c9a325d74f1
# Step 7/9 : RUN cd ${FILENAME} && ./configure --sbin-path=/usr/bin/nginx --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --with-pcre --pid-path=/var/run/nginx.pid --with-http_ssl_module && make && make install
# ---> Running in 11cc82dd5186
### LONG CONFIGURATION AND BUILD STUFF GOES HERE ###
# Removing intermediate container 11cc82dd5186
# ---> 6c122e485ec8
# Step 8/9 : RUN rm -rf /${FILENAME}}
# ---> Running in 04102366960b
# Removing intermediate container 04102366960b
# ---> 6bfa35420a73
# Step 9/9 : CMD ["nginx", "-g", "daemon off;"]
# ---> Running in 63ee44b571bb
# Removing intermediate container 63ee44b571bb
# ---> 4ce79556db1b
# Successfully built 4ce79556db1b
# Successfully tagged custom-nginx:built
Ora dovresti essere in grado di eseguire un container usando l'immagine custom-nginx:built
.
docker container run --rm --detach --name custom-nginx-built --publish 8080:80 custom-nginx:built
# 90ccdbc0b598dddc4199451b2f30a942249d85a8ed21da3c8d14612f17eed0aa
docker container ls
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# 90ccdbc0b598 custom-nginx:built "nginx -g 'daemon of…" 2 minutes ago Up 2 minutes 0.0.0.0:8080->80/tcp custom-nginx-built
Un container che usa l'immagine custom-nginx:built-v2
è stato eseguito con successo. Il container dovrebbe essere accessibile all'indirizzo http://127.0.0.1:8080
.

Ed ecco la pagina di risposta predefinita di NGINX. Puoi visitare il sito di riferimento ufficiale per imparare altro sulle istruzioni disponibili.
Come ottimizzare le immagini Docker
L'immagine di cui abbiamo fatto il build nell'ultima sezione è funzionale ma non ottimizzata. Per provarlo, diamo un'occhiata alla dimensione dell'immagine usando il comando image ls
:
docker image ls
# REPOSITORY TAG IMAGE ID CREATED SIZE
# custom-nginx built 1f3aaf40bb54 16 minutes ago 343MB
Per un'immagine contenente solo NGINX è troppo. Se scarichi l'immagine ufficiale e verifichi la sua dimensione, vedrai quanto è ridotta:
docker image pull nginx:stable
# stable: Pulling from library/nginx
# a076a628af6f: Pull complete
# 45d7b5d3927d: Pull complete
# 5e326fece82e: Pull complete
# 30c386181b68: Pull complete
# b15158e9ebbe: Pull complete
# Digest: sha256:ebd0fd56eb30543a9195280eb81af2a9a8e6143496accd6a217c14b06acd1419
# Status: Downloaded newer image for nginx:stable
# docker.io/library/nginx:stable
docker image ls
# REPOSITORY TAG IMAGE ID CREATED SIZE
# custom-nginx built 1f3aaf40bb54 25 minutes ago 343MB
# nginx stable b9e1dc12387a 11 days ago 133MB
Per capirne la ragione, iniziamo dando uno sguardo a Dockerfile
:
FROM ubuntu:latest
RUN apt-get update && \
apt-get install build-essential\
libpcre3 \
libpcre3-dev \
zlib1g \
zlib1g-dev \
libssl1.1 \
libssl-dev \
-y && \
apt-get clean && rm -rf /var/lib/apt/lists/*
ARG FILENAME="nginx-1.19.2"
ARG EXTENSION="tar.gz"
ADD https://nginx.org/download/${FILENAME}.${EXTENSION} .
RUN tar -xvf ${FILENAME}.${EXTENSION} && rm ${FILENAME}.${EXTENSION}
RUN cd ${FILENAME} && \
./configure \
--sbin-path=/usr/bin/nginx \
--conf-path=/etc/nginx/nginx.conf \
--error-log-path=/var/log/nginx/error.log \
--http-log-path=/var/log/nginx/access.log \
--with-pcre \
--pid-path=/var/run/nginx.pid \
--with-http_ssl_module && \
make && make install
RUN rm -rf /${FILENAME}}
CMD ["nginx", "-g", "daemon off;"]
Come puoi vedere nella riga 3, l'istruzione RUN
installa un sacco di cose. Sebbene questi pacchetti siano necessari per fare il build di NGINX dal sorgente, non sono necessari per la sua esecuzione.
Di 6 pacchetti che abbiamo installato, solo 2 sono necessari per eseguire NGINX. Si tratta di libpcre3
e zlib1g
. Quindi sarebbe una buona idea disinstallare gli altri pacchetti una volta che il processo di build è stato completato.
Per farlo, aggiorniamo Dockerfile
come segue:
FROM ubuntu:latest
EXPOSE 80
ARG FILENAME="nginx-1.19.2"
ARG EXTENSION="tar.gz"
ADD https://nginx.org/download/${FILENAME}.${EXTENSION} .
RUN apt-get update && \
apt-get install build-essential \
libpcre3 \
libpcre3-dev \
zlib1g \
zlib1g-dev \
libssl1.1 \
libssl-dev \
-y && \
tar -xvf ${FILENAME}.${EXTENSION} && rm ${FILENAME}.${EXTENSION} && \
cd ${FILENAME} && \
./configure \
--sbin-path=/usr/bin/nginx \
--conf-path=/etc/nginx/nginx.conf \
--error-log-path=/var/log/nginx/error.log \
--http-log-path=/var/log/nginx/access.log \
--with-pcre \
--pid-path=/var/run/nginx.pid \
--with-http_ssl_module && \
make && make install && \
cd / && rm -rfv /${FILENAME} && \
apt-get remove build-essential \
libpcre3-dev \
zlib1g-dev \
libssl-dev \
-y && \
apt-get autoremove -y && \
apt-get clean && rm -rf /var/lib/apt/lists/*
CMD ["nginx", "-g", "daemon off;"]
Come puoi vedere, nella riga 10 una singola istruzione RUN
sta facendo tutto il lavoro pesante necessario. L'esatta catena di eventi è la seguente:
- Dalla riga 10 alla 17, vengono installati tutti i pacchetti necessari.
- Nella riga 18, il codice viene estratto e l'archivio scaricato viene rimosso.
- Dalla riga 19 alla 28, NGINX viene configurato, viene eseguito il build e poi viene installato sul sistema.
- Nella riga 29, i file estratti dall'archivio scaricato vengono rimossi.
- Dalla riga 30 alla 36, tutti i pacchetti non necessari vengono disinstallati e la cache ripulita. I pacchetti
libpcre3
ezlib1g
sono necessari per eseguire NGINX quindi vengono mantenuti.
Potresti chiederti perché sto facendo tutto questo lavoro in una singola istruzione RUN
invece di separare il tutto in diverse istruzioni come abbiamo fatto in precedenza. Beh, dividerle sarebbe un errore.
Se installi i pacchetti e poi li rimuovi in istruzioni RUN
separate, saranno in livelli diversi dell'immagine. Sebbene l'immagine finale non avrà i pacchetti rimossi, la loro dimensione sarà comunque aggiunta all'immagine finale dato che esistono in uno dei livelli che costituiscono l'immagine. Quindi assicurati di apportare questi cambiamenti in un solo livello.
Facciamo il build dell'immagine usando questo Dockerfile
e guarda le differenze.
docker image build --tag custom-nginx:built .
# Sending build context to Docker daemon 1.057MB
# Step 1/7 : FROM ubuntu:latest
# ---> f63181f19b2f
# Step 2/7 : EXPOSE 80
# ---> Running in 006f39b75964
# Removing intermediate container 006f39b75964
# ---> 6943f7ef9376
# Step 3/7 : ARG FILENAME="nginx-1.19.2"
# ---> Running in ffaf89078594
# Removing intermediate container ffaf89078594
# ---> 91b5cdb6dabe
# Step 4/7 : ARG EXTENSION="tar.gz"
# ---> Running in d0f5188444b6
# Removing intermediate container d0f5188444b6
# ---> 9626f941ccb2
# Step 5/7 : ADD https://nginx.org/download/${FILENAME}.${EXTENSION} .
# Downloading [==================================================>] 1.049MB/1.049MB
# ---> a8e8dcca1be8
# Step 6/7 : RUN apt-get update && apt-get install build-essential libpcre3 libpcre3-dev zlib1g zlib1g-dev libssl-dev -y && tar -xvf ${FILENAME}.${EXTENSION} && rm ${FILENAME}.${EXTENSION} && cd ${FILENAME} && ./configure --sbin-path=/usr/bin/nginx --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --with-pcre --pid-path=/var/run/nginx.pid --with-http_ssl_module && make && make install && cd / && rm -rfv /${FILENAME} && apt-get remove build-essential libpcre3-dev zlib1g-dev libssl-dev -y && apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/*
# ---> Running in e5675cad1260
### LONG INSTALLATION AND BUILD STUFF GOES HERE ###
# Removing intermediate container e5675cad1260
# ---> dc7e4161f975
# Step 7/7 : CMD ["nginx", "-g", "daemon off;"]
# ---> Running in b579e4600247
# Removing intermediate container b579e4600247
# ---> 512aa6a95a93
# Successfully built 512aa6a95a93
# Successfully tagged custom-nginx:built
docker image ls
# REPOSITORY TAG IMAGE ID CREATED SIZE
# custom-nginx built 512aa6a95a93 About a minute ago 81.6MB
# nginx stable b9e1dc12387a 11 days ago 133MB
Come puoi vedere, la dimensione dell'immagine è passata da 343MB a 81.6MB. L'immagine ufficiale è 133MB. Questo è un build piuttosto ottimizzato, ma possiamo spingerci ancora oltre nella prossima sezione.
Alpine Linux
Se stai smanettando con i container da un po', potresti aver sentito parlare di Alpine Linux. Si tratta di una distribuzione Linux completa come Ubuntu, Debian o Fedora.
Ma la cosa buona di Alpine è che è costruito attorno a musl
libc
e busybox
ed è leggero. Mentre l'ultima immagine di ubuntu pesa circa 28MB, alpine pesa 2.8MB.
Oltre alla caratteristica leggerezza, Alpine è anche sicuro ed è molto più adatto per creare container rispetto ad altre distribuzioni.
Nonostante non sia user friendly come altre distribuzioni commerciali, la transizione ad Alpine è ancora molto semplice. In questa sezione, imparerai come ricreare l'immagine custom-nginx
usando l'immagine di Alpine come base.
Apri Dockerfile
e aggiorna il suo contenuto come segue:
FROM alpine:latest
EXPOSE 80
ARG FILENAME="nginx-1.19.2"
ARG EXTENSION="tar.gz"
ADD https://nginx.org/download/${FILENAME}.${EXTENSION} .
RUN apk add --no-cache pcre zlib && \
apk add --no-cache \
--virtual .build-deps \
build-base \
pcre-dev \
zlib-dev \
openssl-dev && \
tar -xvf ${FILENAME}.${EXTENSION} && rm ${FILENAME}.${EXTENSION} && \
cd ${FILENAME} && \
./configure \
--sbin-path=/usr/bin/nginx \
--conf-path=/etc/nginx/nginx.conf \
--error-log-path=/var/log/nginx/error.log \
--http-log-path=/var/log/nginx/access.log \
--with-pcre \
--pid-path=/var/run/nginx.pid \
--with-http_ssl_module && \
make && make install && \
cd / && rm -rfv /${FILENAME} && \
apk del .build-deps
CMD ["nginx", "-g", "daemon off;"]
Il codice è quasi identico, eccetto che per alcuni cambiamenti. Elencherò le modifiche e le spiegherò passo passo:
- Invece di usare
apt-get install
per installare i pacchetti, usiamoapk add
. L'opzione--no-cache
vuol dire che i pacchetti scaricati non saranno salvati nella cache. Allo stesso modo, useremoapk del
invece diapt-get remove
per disinstallare i pacchetti. - L'opzione
--virtual
per il comandoapk add
viene usata per impacchettare un gruppo di pacchetti in un solo pacchetto virtuale per una gestione più semplice. I pacchetti che sono necessari solo per il build del programma sono etichettati come.build-deps
e vengono rimossi nella riga 29 eseguendo il comandoapk del .build-deps
. Per saperne di più puoi consultare la documentazione ufficiale. - I nomi dei pacchetti sono un po' diversi qui. Di solito, ogni distribuzione Linux ha un suo repository dove cercare pacchetti disponibile per chiunque. Se conosci i pacchetti richiesti per una certa attività, puoi andare dritto all'apposito repository per una distribuzione e cercarlo. Puoi cercare i pacchetti per Alpine Linux qui.
Ora fai il build di una nuova immagine usando questo Dockerfile
e vedi la differenza nella dimensione del file:
docker image build --tag custom-nginx:built .
# Sending build context to Docker daemon 1.055MB
# Step 1/7 : FROM alpine:latest
# ---> 7731472c3f2a
# Step 2/7 : EXPOSE 80
# ---> Running in 8336cfaaa48d
# Removing intermediate container 8336cfaaa48d
# ---> d448a9049d01
# Step 3/7 : ARG FILENAME="nginx-1.19.2"
# ---> Running in bb8b2eae9d74
# Removing intermediate container bb8b2eae9d74
# ---> 87ca74f32fbe
# Step 4/7 : ARG EXTENSION="tar.gz"
# ---> Running in aa09627fe48c
# Removing intermediate container aa09627fe48c
# ---> 70cb557adb10
# Step 5/7 : ADD https://nginx.org/download/${FILENAME}.${EXTENSION} .
# Downloading [==================================================>] 1.049MB/1.049MB
# ---> b9790ce0c4d6
# Step 6/7 : RUN apk add --no-cache pcre zlib && apk add --no-cache --virtual .build-deps build-base pcre-dev zlib-dev openssl-dev && tar -xvf ${FILENAME}.${EXTENSION} && rm ${FILENAME}.${EXTENSION} && cd ${FILENAME} && ./configure --sbin-path=/usr/bin/nginx --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --with-pcre --pid-path=/var/run/nginx.pid --with-http_ssl_module && make && make install && cd / && rm -rfv /${FILENAME} && apk del .build-deps
# ---> Running in 0b301f64ffc1
### LONG INSTALLATION AND BUILD STUFF GOES HERE ###
# Removing intermediate container 0b301f64ffc1
# ---> dc7e4161f975
# Step 7/7 : CMD ["nginx", "-g", "daemon off;"]
# ---> Running in b579e4600247
# Removing intermediate container b579e4600247
# ---> 3e186a3c6830
# Successfully built 3e186a3c6830
# Successfully tagged custom-nginx:built
docker image ls
# REPOSITORY TAG IMAGE ID CREATED SIZE
# custom-nginx built 3e186a3c6830 8 seconds ago 12.8MB
La versione ubuntu era 81.6MB, mentre la versione alpine è scesa fino a 12.8MB, un guadagno enorme. Oltre al gestore di pacchetti apk
, ci sono alcune cose che differenziano Alpine da Ubuntu ma non sono così importanti. Puoi sempre fare una ricerca su internet se resti bloccato.
Come creare immagini Docker eseguibili
Nella sezione precedente hai lavorato con l'immagine fhsinchy/rmbyext. In questa sezione imparerai come realizzare un'immagine eseguibile come quella.
Per iniziare, apri la cartella in cui hai clonato il repository in dotazione con questo libro. Il codice per l'applicazione rmbyext
si trova all'interno della sotto-cartella con lo stesso nome.
Prima di iniziare a lavorare sul Dockerfile
, prendi un momento per pianificare quale dovrebbe essere l'output finale. Secondo me, dovrebbe essere qualcosa del genere:
- L'immagine dovrebbe avere Python preinstallato.
- Dovrebbe contenere una copia del mio script
rmbyext
. - Dovrebbe essere definita una cartella di lavoro in cui verrà eseguito lo script.
- Lo script
rmbyext
dovrebbe essere impostato come entry-point in modo che l'immagine possa prendere i nomi delle estensioni come argomenti.
Per fare il build dell'immagine sopra menzionata, segui i seguenti passaggi:
- Ottieni una buona immagine base per eseguire gli script Python, come python.
- Definisci la cartella di lavoro come una cartella di facile accesso.
- Installa Git in modo che lo script possa essere installato dal mio repository GitHub.
- Installa lo script usando Git e pip.
- Sbarazzati dei pacchetti di build non necessari.
- Imposta
rmbyext
come entry-point dell'immagine.
Ora crea un nuovo Dockerfile
nella cartella rmbyext
e inserisci il seguente codice al suo interno:
FROM python:3-alpine
WORKDIR /zone
RUN apk add --no-cache git && \
pip install git+https://github.com/fhsinchy/rmbyext.git#egg=rmbyext && \
apk del git
ENTRYPOINT [ "rmbyext" ]
La spiegazione delle istruzioni in questo file è la seguente:
- L'istruzione
FROM
definisce python come immagine base, creando un ambiente ideale per eseguire gli script Python. Il tag3-alpine
indica che vuoi la variante Alpine di Python 3. - L'istruzione
WORKDIR
imposta la cartella di lavoro predefinita su/zone
. Il nome della cartella di lavoro è completamente casuale qui. Ho usato zone, ma tu puoi usare il nome che preferisci. - Dato che lo script
rmbyext
è installato da GitHub,git
è una dipendenza di installazione. L'istruzioneRUN
nella riga 5, installagit
, poi installa lo scriptrmbyext
usando Git e pip. In seguito, si libera anche digit
. - Infine, nella riga 9, l'istruzione
ENTRYPOINT
imposta lo scriptrmbyext
come entry-point dell'immagine.
In questo file, nella riga 9 c'è la magia che trasforma questa immagine apparentemente normale in un'immagine eseguibile. Adesso per fare il build dell'immagine puoi eseguire il seguente comando:
docker image build --tag rmbyext .
# Sending build context to Docker daemon 2.048kB
# Step 1/4 : FROM python:3-alpine
# 3-alpine: Pulling from library/python
# 801bfaa63ef2: Already exists
# 8723b2b92bec: Already exists
# 4e07029ccd64: Already exists
# 594990504179: Already exists
# 140d7fec7322: Already exists
# Digest: sha256:7492c1f615e3651629bd6c61777e9660caa3819cf3561a47d1d526dfeee02cf6
# Status: Downloaded newer image for python:3-alpine
# ---> d4d4f50f871a
# Step 2/4 : WORKDIR /zone
# ---> Running in 454374612a91
# Removing intermediate container 454374612a91
# ---> 7f7e49bc98d2
# Step 3/4 : RUN apk add --no-cache git && pip install git+https://github.com/fhsinchy/rmbyext.git#egg=rmbyext && apk del git
# ---> Running in 27e2e96dc95a
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container 27e2e96dc95a
# ---> 3c7389432e36
# Step 4/4 : ENTRYPOINT [ "rmbyext" ]
# ---> Running in f239bbea1ca6
# Removing intermediate container f239bbea1ca6
# ---> 1746b0cedbc7
# Successfully built 1746b0cedbc7
# Successfully tagged rmbyext:latest
docker image ls
# REPOSITORY TAG IMAGE ID CREATED SIZE
# rmbyext latest 1746b0cedbc7 4 minutes ago 50.9MB
Qui non ho fornito nessun tag dopo il nome dell'immagine, quindi l'immagine è stata taggata di default come latest
. Dovresti essere in grado di eseguire l'immagine come hai visto nella sezione precedente. Ricorda di fare riferimento al nome che hai impostato per l'immagine invece di fhsinchy/rmbyext
.
Come condividere le tue immagini Docker online
Ora che sai come creare delle immagini, è tempo di condividerle con il mondo. Condividere immagini online è semplice. Tutto ciò di cui hai bisogno è un account in un qualsiasi registro online. Io userò Docker Hub.
Vai sulla pagina di registrazione e crea un account gratuito, che ti permetterà di ospitare repository pubblici senza limiti e un repository privato.
Una volta creato l'account, dovrai accedere usando la CLI docker. Quindi apri il terminale ed esegui il seguente comando per farlo:
docker login
# Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
# Username: fhsinchy
# Password:
# WARNING! Your password will be stored unencrypted in /home/fhsinchy/.docker/config.json.
# Configure a credential helper to remove this warning. See
# https://docs.docker.com/engine/reference/commandline/login/#credentials-store
#
# Login Succeeded
Ti verranno richiesti nome utente e password. Inserendoli correttamente, accederai al tuo account.
Per condividere un'immagine online, occorre che questa sia taggata. Hai imparato come taggare un'immagine in una sezione precedente. Tanto per rinfrescarti la memoria, la sintassi generica per l'opzione --tag
o -t
è la seguente:
--tag <repository immagine>:<tag immagine>
Come esempio, condividiamo online l'immagine custom-nginx
. Per farlo, apri un nuovo terminale all'interno della directory del progetto custom-nginx
.
Per condividere un'immagine online, devi taggarla seguendo la sintassi <username docker hub>/<nome immagine>:<tag immagine>
. Il mio username è fhsinchy
, quindi il comando sarà:
docker image build --tag fhsinchy/custom-nginx:latest --file Dockerfile.built .
# Step 1/9 : FROM ubuntu:latest
# ---> d70eaf7277ea
# Step 2/9 : RUN apt-get update && apt-get install build-essential libpcre3 libpcre3-dev zlib1g zlib1g-dev libssl-dev -y && apt-get clean && rm -rf /var/lib/apt/lists/*
# ---> cbe1ced3da11
### LONG INSTALLATION STUFF GOES HERE ###
# Step 3/9 : ARG FILENAME="nginx-1.19.2"
# ---> Running in 33b62a0e9ffb
# Removing intermediate container 33b62a0e9ffb
# ---> fafc0aceb9c8
# Step 4/9 : ARG EXTENSION="tar.gz"
# ---> Running in 5c32eeb1bb11
# Removing intermediate container 5c32eeb1bb11
# ---> 36efdf6efacc
# Step 5/9 : ADD https://nginx.org/download/${FILENAME}.${EXTENSION} .
# Downloading [==================================================>] 1.049MB/1.049MB
# ---> dba252f8d609
# Step 6/9 : RUN tar -xvf ${FILENAME}.${EXTENSION} && rm ${FILENAME}.${EXTENSION}
# ---> Running in 2f5b091b2125
### LONG EXTRACTION STUFF GOES HERE ###
# Removing intermediate container 2f5b091b2125
# ---> 2c9a325d74f1
# Step 7/9 : RUN cd ${FILENAME} && ./configure --sbin-path=/usr/bin/nginx --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --with-pcre --pid-path=/var/run/nginx.pid --with-http_ssl_module && make && make install
# ---> Running in 11cc82dd5186
### LONG CONFIGURATION AND BUILD STUFF GOES HERE ###
# Removing intermediate container 11cc82dd5186
# ---> 6c122e485ec8
# Step 8/9 : RUN rm -rf /${FILENAME}}
# ---> Running in 04102366960b
# Removing intermediate container 04102366960b
# ---> 6bfa35420a73
# Step 9/9 : CMD ["nginx", "-g", "daemon off;"]
# ---> Running in 63ee44b571bb
# Removing intermediate container 63ee44b571bb
# ---> 4ce79556db1b
# Successfully built 4ce79556db1b
# Successfully tagged fhsinchy/custom-nginx:latest
In questo comando, fhsinchy/custom-nginx
è il repository dell'immagine e latest
è il tag. Il nome dell'immagine può essere qualsiasi cosa desideri e non può essere cambiato una volta che carichi l'immagine. Il tag può essere cambiato ogni volta che vuoi e solitamente riflette la versione del software o diversi tipi di build.
Prendi l'immagine node
come esempio. L'immagine node:lts
fa riferimento alla versione con supporto a lungo termine di Node.js, mentre la versione node:lts-alpine
fa riferimento alla versione di Node.js per Alpine Linux, che è molto più leggera di quella normale.
Se non dai nessun tag all'immagine, sarà automaticamente taggata come latest
. Ma ciò non vuol dire che il tag latest
farà sempre riferimento all'ultima versione. Se per qualche ragione, usi esplicitamente il tag latest
per una vecchia versione dell'immagine, Docker non farà nessuno sforzo extra per verificare che sia corretto.
Una volta che il build è stato effettuato, puoi caricare l'immagine eseguendo il seguente comando:
docker image push <repository immagine>:<tag immagine>
Quindi, in questo caso il comando sarà:
docker image push fhsinchy/custom-nginx:latest
# The push refers to repository [docker.io/fhsinchy/custom-nginx]
# 4352b1b1d9f5: Pushed
# a4518dd720bd: Pushed
# 1d756dc4e694: Pushed
# d7a7e2b6321a: Pushed
# f6253634dc78: Mounted from library/ubuntu
# 9069f84dbbe9: Mounted from library/ubuntu
# bacd3af13903: Mounted from library/ubuntu
# latest: digest: sha256:ffe93440256c9edb2ed67bf3bba3c204fec3a46a36ac53358899ce1a9eee497a size: 1788
A seconda della dimensione dell'immagine, il caricamento potrebbe richiedere del tempo. Una volta terminato, dovresti essere in grado di trovare l'immagine nella pagina del tuo profilo sull'hub.
Come containerizzare una app JavaScript
Ora che ti sei fatto un'idea di come creare immagini, è tempo di lavorare con qualcosa di più rilevante.
In questa sezione, lavorerai con il codice sorgente dell'immagine fhsinchy/hello-dock usata nella sezione precedente. Nel processo di containerizzazione di questa semplice applicazione, parleremo dei volumi e dei build multi-stadio, due dei concetti più importanti in Docker.
Come scrivere il Dockerfile di sviluppo
Per iniziare, apri la cartella in cui hai clonato il repository in dotazione con questo libro. Il codice per l'applicazione hello-dock
si trova nella sotto-cartella con lo stesso nome.
Questo è un progetto JavaScript molto semplice realizzato dal progetto vitejs/vite. Non preoccuparti, non hai bisogno di conoscere JavaScript o vite per affrontare questa sezione. Una comprensione base di Node.js e npm è sufficiente.
Proprio come ogni altro progetto delle sezioni precedenti, inizierai facendo un piano di come vuoi eseguire l'applicazione. Secondo me, il piano dovrebbe essere il seguente:
- Ottenere una buona immagine base per eseguire applicazioni JavaScript, come node.
- Definire la cartella di lavoro nell'immagine.
- Copiare il file
package.json
nell'immagine. - Installare le dipendenze necessarie.
- Copiare il resto dei file del progetto.
- Avviare il server di sviluppo di
vite
eseguendo il comandonpm run dev
.
Questo piano dovrebbe arrivare sempre dallo sviluppatore dell'applicazione che stai containerizzando. Se sei tu lo sviluppatore, allora dovresti avere una comprensione appropriata di come dovrebbe essere eseguita l'applicazione.
Se metti il piano sopra menzionato all'interno di Dockerfile.dev
, il file dovrebbe avere questo aspetto:
FROM node:lts-alpine
EXPOSE 3000
USER node
RUN mkdir -p /home/node/app
WORKDIR /home/node/app
COPY ./package.json .
RUN npm install
COPY . .
CMD [ "npm", "run", "dev" ]
La spiegazione di questo codice è la seguente:
- L'istruzione
FROM
imposta l'immagine ufficiale di Node.js come base, rendendo disponibili tutte le qualità di Node.js necessarie per eseguire qualsiasi applicazione JavaScript. Il taglts-alpine
indica che vuoi usare la variante Alpine, versione con supporto a lungo termine dell'immagine. I tag disponibili e la documentazione necessaria può essere trovata sulla pagina node. - L'istruzione
USER
imposta l'utente predefinito per l'immagine comenode
. Di default Docker esegue i container come utente root. Ma secondo le buone pratiche di Docker e Node.js questo può causare problemi di sicurezza. Quindi è meglio non farlo quando possibile. L'immagine di node ha un utente non-root chiamatonode
, che puoi impostare come utente di default usando l'istruzioneUSER
. - L'istruzione
RUN mkdir -p /home/node/app
crea una cartella chiamataapp
all'interno della cartella home dell'utentenode
. La cartella home per qualsiasi utente non-root in Linux è solitamente/home/<nome utente>
di default. - Poi l'istruzione
WORKDIR
imposta la cartella di lavoro predefinita con la nuova cartella/home/node/app
appena creata. Di default, la cartella di lavoro di ogni immagini è la radice. Dato che non vuoi file non necessari sparsi in giro nella tua directory root, è bene cambiare la cartella di lavoro predefinita in qualcosa di più ragionevole come/home/node/app
o quello che preferisci. Questa cartella di lavoro sarà applicabile a ogni istruzioneCOPY
,ADD
,RUN
eCMD
successiva. - L'istruzione
COPY
copia il filepackage.json
che contiene informazioni riguardo a tutte le dipendenze necessarie per l'applicazione. L'istruzioneRUN
esegue il comandonpm install
, che è il comando predefinito per installare le dipendenze usando un filepackage.json
nei progetti Node.js. Il punto.
alla fine rappresenta la cartella di lavoro. - La seconda istruzione
COPY
copia il resto del contenuto della cartella corrente (.
) del file system host nella cartella di lavoro (.
) all'interno dell'immagine. - Infine, l'istruzione
CMD
imposta il comando predefinito per l'immagine, che ènpm run dev
scritto in formaexec
. - Il server di sviluppo
vite
gira di default sulla porta3000
, e aggiungere un comandoEXPOSE
sembrava una buona idea.
Per fare il build dell'immagine da Dockerfile.dev
puoi eseguire il seguente comando:
docker image build --file Dockerfile.dev --tag hello-dock:dev .
# Step 1/7 : FROM node:lts
# ---> b90fa0d7cbd1
# Step 2/7 : EXPOSE 3000
# ---> Running in 722d639badc7
# Removing intermediate container 722d639badc7
# ---> e2a8aa88790e
# Step 3/7 : WORKDIR /app
# ---> Running in 998e254b4d22
# Removing intermediate container 998e254b4d22
# ---> 6bd4c42892a4
# Step 4/7 : COPY ./package.json .
# ---> 24fc5164a1dc
# Step 5/7 : RUN npm install
# ---> Running in 23b4de3f930b
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container 23b4de3f930b
# ---> c17ecb19a210
# Step 6/7 : COPY . .
# ---> afb6d9a1bc76
# Step 7/7 : CMD [ "npm", "run", "dev" ]
# ---> Running in a7ff529c28fe
# Removing intermediate container a7ff529c28fe
# ---> 1792250adb79
# Successfully built 1792250adb79
# Successfully tagged hello-dock:dev
Dato che il nome del file non è Dockerfile
, devi passare esplicitamente il nome del file usando l'opzione --file
. Un container può essere eseguito usando questa immagine con il seguente comando:
docker container run \
--rm \
--detach \
--publish 3000:3000 \
--name hello-dock-dev \
hello-dock:dev
# 21b9b1499d195d85e81f0e8bce08f43a64b63d589c5f15cbbd0b9c0cb07ae268
Se visiti http://127.0.0.1:3000
vedrai l'applicazione hello-dock
in azione.

Congratulazioni per aver eseguito la tua prima vera applicazione all'interno di un container. Il codice che hai appena scritto va bene, ma c'è un grande problema e qualche cosuccia che può essere migliorata. Iniziamo dal problema principale.
Come lavorare con i bind mount in Docker
Se prima d'ora hai lavorato con qualsiasi framework front-end JavaScript, dovresti sapere che i server di sviluppo in questi framework hanno solitamente una funzionalità di ricaricamento rapido. Cioè, se fai un cambiamento al tuo codice, il server sarà ricaricato automaticamente riflettendo tutti i cambiamenti che hai fatto immediatamente.
Ma se fai delle modifiche al codice proprio adesso, non vedrai accadere nulla all'applicazione in esecuzione nel browser. Questo perché stai apportando dei cambiamenti al codice che hai nel tuo file system locale, ma l'applicazione che vedi nel browser risiede nel file system del container.
Per risolvere questo problema, puoi fare di nuovo uso di un bind mount. Usando i bind mount, puoi montare facilmente una delle cartelle nel file system locale all'interno di un container. Invece di fare una copia del file system locale, il bind mount può fare riferimento al file system locale direttamente dall'interno del container.
In questo modo, ogni cambiamento che fai alla tua sorgente locale verrà riflesso immediatamente all'interno del container, innescando la funzionalità di ricaricamento rapido del server di sviluppo vite
. I cambiamenti apportati al file system nel container verranno riportati allo stesso modo nel file system locale.
Come hai già imparato nella sezione Come lavorare con immagini eseguibili, i bind mount possono essere creati usando l'opzione --volume
o -v
per i comandi container run
o container start
. Come promemoria, ecco la sintassi generica:
--volume <percorso assoluto cartella file system locale>:<percorso assoluto cartella file system container>:<accesso read write>
Blocca il container hello-dock-dev
precedentemente avviato e avviane uno nuovo eseguendo:
docker container run \
--rm \
--publish 3000:3000 \
--name hello-dock-dev \
--volume $(pwd):/home/node/app \
hello-dock:dev
# sh: 1: vite: not found
# npm ERR! code ELIFECYCLE
# npm ERR! syscall spawn
# npm ERR! file sh
# npm ERR! errno ENOENT
# npm ERR! hello-dock@0.0.0 dev: `vite`
# npm ERR! spawn ENOENT
# npm ERR!
# npm ERR! Failed at the hello-dock@0.0.0 dev script.
# npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
# npm WARN Local package.json exists, but node_modules missing, did you mean to install?
Ho omesso l'opzione --detach
per dimostrare un punto molto importante. Come puoi vedere, l'applicazione non viene eseguita adesso.
Questo perché l'uso di un volume risolve il problema del ricaricamento rapido, ma ne introduce un altro. Se hai dell'esperienza pregressa con Node.js, potresti sapere che le dipendenze di un progetto Node.js risiedono nella cartella node_modules
nella radice del progetto.
Adesso che stai montando la radice del progetto sul tuo file system locale come un volume all'interno del container, il contenuto del container viene sostituito insieme alla cartella node_modules
, in cui ci sono tutte le dipendenze. Ciò significa che il pacchetto vite
è sparito.
Come lavorare con volumi anonimi in Docker
Questo problema può essere risolto usando un volume anonimo, che è identico a un bind mount, tranne per il fatto che non è necessario specificare la directory sorgente. La sintassi generica per creare un volume anonimo è la seguente:
--volume <percorso assoluto cartella file system container>:<accesso read write>
Quindi il comando finale per avviare il container hello-dock
con entrambi i volumi è il seguente:
docker container run \
--rm \
--detach \
--publish 3000:3000 \
--name hello-dock-dev \
--volume $(pwd):/home/node/app \
--volume /home/node/app/node_modules \
hello-dock:dev
# 53d1cfdb3ef148eb6370e338749836160f75f076d0fbec3c2a9b059a8992de8b
Docker prenderà l'intera cartella node_modules
dall'interno del container, la metterà da parte in un'altra cartella gestita dal demone di Docker sul file system host e la monterà come node_modules
all'interno del container.
Come eseguire dei build multi-stadio in Docker
Finora in questa sezione, hai fatto il build di un'immagine per eseguire un'applicazione JavaScript in modalità di sviluppo. Quando vuoi fare il build dell'immagine in modalità di produzione, vengono fuori nuove sfide.
In modalità di sviluppo, il comando npm run serve
avvia un server di sviluppo che serve l'applicazione all'utente. Non serve soltanto i file all'utente, ma fornisce anche la funzionalità di ricaricamento rapido.
In modalità di produzione, il comando npm run build
compila tutto il codice JavaScript in file HTML statici, CSS e JavaScript. Per eseguire questi file, non c'è bisogno di node o di altre dipendenze di runtime. Tutto ciò di cui hai bisogno è un server come nginx
, ad esempio.
Per creare un'immagine in cui l'applicazione viene eseguita in modalità di produzione, potresti seguire i seguenti passaggi:
- Usa
node
come immagine base e fai il build dell'applicazione. - Installa
nginx
all'interno dell'immagine di node e usala per servire i file statici.
Questo approccio è assolutamente valido, ma il problema è che l'immagine di node
è grande e la maggior parte di ciò che contiene non è necessaria per servire i file statici. Un approccio migliore per questo caso è il seguente:
- Usa
node
come immagine base e fai il build dell'applicazione. - Copia i file creati usando l'immagine di
node
in un'immagine dinginx
. - Crea l'immagine finale sulla base di
nginx
e scarta tutta la roba correlata anode
.
In questo modo, l'immagine contiene solo i file che sono necessari e diventa molto gestibile.
Questo approccio è un build multi-stadio. Per eseguirlo, crea un nuovo Dockerfile
all'interno della directory del progetto hello-dock
e inserisci il seguente contenuto:
FROM node:lts-alpine as builder
WORKDIR /app
COPY ./package.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:stable-alpine
EXPOSE 80
COPY --from=builder /app/dist /usr/share/nginx/html
Come puoi vedere, Dockerfile
somiglia molto ai precedenti con qualche particolarità. La spiegazione per questo file è la seguente:
- Nella riga 1 inizia il primo stadio di build usando
node:lts-alpine
come immagine base. La sintassias builder
assegna a questo stadio un nome con cui farvi riferimento in seguito. - Dalla riga 3 alla 9, cose standard che hai già visto molte volte. Il comando
RUN npm run build
compila l'intera applicazione e la mette da parte nella cartella/app/dist
dove/app
è la cartella di lavoro e/dist
è la cartella di output predefinita per le applicazionivite
. - Nella riga 11 inizia il secondo stadio del build usando
nginx:stable-alpine
come immagine base. - Il server NGINX gira sulla porta 80 di default, di qui la riga
EXPOSE 80
. - L'ultima riga è un'istruzione
COPY
. La parte--from=builder
indica che vuoi copiare dei file dallo stadiobuilder
. Oltre questo, si tratta di un'istruzione copy standard dove/app/dist
è la sorgente e/usr/share/nginx/html
è la destinazione. La destinazione usata qui è il percorso predefinito per NGINX, in modo che ogni file statico che metti al suo interno sarà servito automaticamente.
Come puoi vedere, l'immagine risultante è una base nginx
contenente solo i file necessari per eseguire l'applicazione. Per svolgere il build di quest'immagine esegui il seguente comando:
docker image build --tag hello-dock:prod .
# Step 1/9 : FROM node:lts-alpine as builder
# ---> 72aaced1868f
# Step 2/9 : WORKDIR /app
# ---> Running in e361c5c866dd
# Removing intermediate container e361c5c866dd
# ---> 241b4b97b34c
# Step 3/9 : COPY ./package.json ./
# ---> 6c594c5d2300
# Step 4/9 : RUN npm install
# ---> Running in 6dfabf0ee9f8
# npm WARN deprecated fsevents@2.1.3: Please update to v 2.2.x
#
# > esbuild@0.8.29 postinstall /app/node_modules/esbuild
# > node install.js
#
# npm notice created a lockfile as package-lock.json. You should commit this file.
# npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@~2.1.2 (node_modules/chokidar/node_modules/fsevents):
# npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.1.3: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
# npm WARN hello-dock@0.0.0 No description
# npm WARN hello-dock@0.0.0 No repository field.
# npm WARN hello-dock@0.0.0 No license field.
#
# added 327 packages from 301 contributors and audited 329 packages in 35.971s
#
# 26 packages are looking for funding
# run `npm fund` for details
#
# found 0 vulnerabilities
#
# Removing intermediate container 6dfabf0ee9f8
# ---> 21fd1b065314
# Step 5/9 : COPY . .
# ---> 43243f95bff7
# Step 6/9 : RUN npm run build
# ---> Running in 4d918cf18584
#
# > hello-dock@0.0.0 build /app
# > vite build
#
# - Building production bundle...
#
# [write] dist/index.html 0.39kb, brotli: 0.15kb
# [write] dist/_assets/docker-handbook-github.3adb4865.webp 12.32kb
# [write] dist/_assets/index.eabcae90.js 42.56kb, brotli: 15.40kb
# [write] dist/_assets/style.0637ccc5.css 0.16kb, brotli: 0.10kb
# - Building production bundle...
#
# Build completed in 1.71s.
#
# Removing intermediate container 4d918cf18584
# ---> 187fb3e82d0d
# Step 7/9 : EXPOSE 80
# ---> Running in b3aab5cf5975
# Removing intermediate container b3aab5cf5975
# ---> d6fcc058cfda
# Step 8/9 : FROM nginx:stable-alpine
# stable: Pulling from library/nginx
# 6ec7b7d162b2: Already exists
# 43876acb2da3: Pull complete
# 7a79edd1e27b: Pull complete
# eea03077c87e: Pull complete
# eba7631b45c5: Pull complete
# Digest: sha256:2eea9f5d6fff078ad6cc6c961ab11b8314efd91fb8480b5d054c7057a619e0c3
# Status: Downloaded newer image for nginx:stable
# ---> 05f64a802c26
# Step 9/9 : COPY --from=builder /app/dist /usr/share/nginx/html
# ---> 8c6dfc34a10d
# Successfully built 8c6dfc34a10d
# Successfully tagged hello-dock:prod
Una volta completato il build, puoi eseguire un nuovo container con il comando:
docker container run \
--rm \
--detach \
--name hello-dock-prod \
--publish 8080:80 \
hello-dock:prod
# 224aaba432bb09aca518fdd0365875895c2f5121eb668b2e7b2d5a99c019b953
L'applicazione in esecuzione dovrebbe essere disponibile su http://127.0.0.1:8080
:

Qui puoi vedere la mia applicazione hello-dock
in tutta la sua gloria. I build multi-stadio possono essere molto utili per applicazioni estese con molte dipendenze. Se configurate correttamente, le immagini con build multi-stadio possono essere davvero ottimizzate e compatte.
Come ignorare i file non necessari
Se hai lavorato a dei progetti con git
, saprai dei file .gitignore
. Contengono una lista di file e cartelle da escludere dal repository.
Beh, Docker possiede un concetto simile. Il file .dockerignore
contiene una lista di file e cartelle da escludere dal build di un'immagine. Puoi trovare un file .dockerignore
già creato nella cartella hello-dock
.
.git
*Dockerfile*
*docker-compose*
node_modules
Il file .dockerignore
deve essere nel contesto di build. I file e le cartelle menzionate qui saranno ignorati dall'istruzione COPY
. Ma se usi un bind mount, il file .dockerignore
non avrà effetto. Ho aggiunto dei file .dockerignore
dove necessario nel repository del progetto.
Manipolazione base delle reti in Docker
Finora in questo manuale, hai lavorato su progetti con container singoli. Ma in realtà, la maggior parte dei progetti con cui avrai a che fare nella vita reale avrà più di un container. E onestamente, lavorare con un gruppo di container può essere un po' difficile se non afferri le sfumature dell'isolamento dei container.
In questa sezione, acquisirai familiarità con il networking di base con Docker e lavorerai direttamente su un piccolo progetto multi-container.
Hai già imparato nella sezione precedente che i container sono ambienti isolati. Ora considera una situazione in cui hai una applicazione notes-api
alimentata da Express.js e un database PostgreSQL in esecuzione in due container separati.
Questi due container sono completamente isolati tra di loro e ognuno ignora l'esistenza dell'altro. Come puoi connetterli? Sembra una bella sfida.
Puoi pensare a due possibili soluzioni a questo problema:
- Accedere al server del database usando una porta esposta.
- Accedere al server del database usando il suo indirizzo IP e la porta predefinita.
La prima implica di esporre una porta dal container postgres
e notes-api
si connetterà attraverso di essa. Supponi che la porta esposta dal container postgres
sia 5432. Se provi a connetterti a 127.0.0.1:5432
dal container notes-api
, scoprirai che notes-api
non è in grado di trovare il server del database.
La ragione è che quando dici 127.0.0.1
all'interno del container notes-api
, fai riferimento al localhost
soltanto di quel container. Il server postgres
non esiste lì. Come risultato, l'applicazione notes-api
fallisce la connessione.
La seconda soluzione che puoi escogitare è trovare l'esatto indirizzo IP del container postgres
usando il comando container inspect
e usarlo con la porta. Assumendo che il nome del container postgres
sia notes-api-db-server
, puoi ottenere facilmente l'indirizzo IP eseguendo il seguente comando:
docker container inspect --format='{{range .NetworkSettings.Networks}} {{.IPAddress}} {{end}}' notes-api-db-server
# 172.17.0.2
Posto che la porta di default per postgres
è 5432
, puoi accedere al server del database molto facilmente connettendoti a 172.17.0.2:5432
dal container notes-api
.
Anche in questo approccio ci sono dei problemi. Usare l'indirizzo IP per fare riferimento a un container non è raccomandato. Inoltre, se il container viene distrutto e ricreato, l'indirizzo IP può cambiare. Tenere traccia dei cambiamenti dell'indirizzo IP può essere un po' caotico.
Ora che ho liquidato le possibili risposte sbagliate alla domanda originale, la risposta corretta è che li connetti mettendoli sotto una rete bridge definita dall'utente.
Basi delle reti Docker
Una rete (network) Docker è un altro oggetto logico come una container o un'immagine. Proprio come per gli altri due, esiste una pletora di comandi nel gruppo docker network
per manipolare i network.
Per elencare le reti nel tuo sistema, esegui il seguente comando:
docker network ls
# NETWORK ID NAME DRIVER SCOPE
# c2e59f2b96bd bridge bridge local
# 124dccee067f host host local
# 506e3822bf1f none null local
Dovresti vedere 3 reti nel tuo sistema. Ora guarda la colonna DRIVER
della tabella. Questi driver possono essere interpretati come il tipo di rete.
Di default, Docker ha cinque driver di rete:
bridge
- Il driver di rete predefinito in Docker. Può essere usato quando più container sono in esecuzione in modalità standard e devono comunicare tra di loro.host
- Rimuove completamente l'isolamento della rete. Ogni container in esecuzione sotto una retehost
è praticamente collegato alla rete del sistema host.none
- Questo driver disabilita del tutto la rete per i container. Non ho ancora trovato dei casi in cui utilizzarlo.overlay
- Usato per connettere più demoni Docker tra computer; fuori dallo scopo di questo libro.macvlan
- Permette di assegnare indirizzi MAC ai container, facendoli funzionare come dispositivi fisici in una rete.
Esistono anche plugin di terze parti che consentono di integrare Docker con stack di rete specializzati. Tra i cinque menzionati qui sopra, in questo manuale lavorerai soltanto con il driver di rete bridge
.
Come creare un bridge in Docker
Prima di iniziare a creare il tuo bridge, vorrei spendere un po' di tempo per discutere della rete bridge predefinita. Iniziamo elencando tutte le reti del sistema:
docker network ls
# NETWORK ID NAME DRIVER SCOPE
# c2e59f2b96bd bridge bridge local
# 124dccee067f host host local
# 506e3822bf1f none null local
Come puoi vedere, Docker possiede una rete bridge di default, denominata bridge
. Ogni container che esegui si collegherà automaticamente a questa rete bridge:
docker container run --rm --detach --name hello-dock --publish 8080:80 fhsinchy/hello-dock
# a37f723dad3ae793ce40f97eb6bb236761baa92d72a2c27c24fc7fda0756657d
docker network inspect --format='{{range .Containers}}{{.Name}}{{end}}' bridge
# hello-dock
I container collegati alla rete bridge predefinita possono comunicare tra loro usando gli indirizzi IP, di cui ho sconsigliato l'uso nella sezione precedente.
Tuttavia, un bridge definito da un utente ha delle funzionalità extra rispetto a quello di default. Secondo la documentazione ufficiale su questo argomento, alcune funzionalità aggiuntive importanti sono le seguenti:
- I bridge definiti da un utente offrono la risoluzione DNS automatica tra container: ciò vuol dire che i container collegati alla stessa rete possono comunicare tra loro usando il nome del container. Dunque, se hai due container chiamati
notes-api
enotes-db
il container dell'API sarà in grado di connettersi al container del database usando il nomenotes-db
. - I bridge definiti da un utente offrono un migliore isolamento: tutti i container sono collegati alla rete bridge di default, il che può causare dei conflitti tra di loro. Collegare i container a un bridge definito da un utente può garantire un isolamento migliore.
- I container possono essere collegati e scollegati al volo da reti definiti da un utente: durante la vita di un container, puoi connetterlo e disconnetterlo da reti definite da un utente molto semplicemente. Per rimuovere il container dalla rete bridge predefinita, devi fermare il container e ricrearlo con diverse opzioni di rete.
Ora che hai imparato abbastanza sulle reti definite da un utente, è tempo di crearne una. Una rete può essere creata usando il comando network create
, con la seguente sintassi generica:
docker network create <nome rete>
Per creare una rete con il nome skynet
esegui il seguente comando:
docker network create skynet
# 7bd5f351aa892ac6ec15fed8619fc3bbb95a7dcdd58980c28304627c8f7eb070
docker network ls
# NETWORK ID NAME DRIVER SCOPE
# be0cab667c4b bridge bridge local
# 124dccee067f host host local
# 506e3822bf1f none null local
# 7bd5f351aa89 skynet bridge local
Come puoi vedere, è stata creata una nuova rete con il nome dato. Nessun container è attualmente collegato a questa rete. Nella prossima sezione, imparerai come collegare i container a una rete.
Come collegare un container a una rete in Docker
Esistono principalmente due modi per collegare un container a una rete. Il primo modo è usare il comando network connect
, la cui sintassi generica è:
docker network connect <identificatore rete> <identificatore container>
Per connettere il container hello-dock
alla rete skynet
, puoi eseguire il seguente comando:
docker network connect skynet hello-dock
docker network inspect --format='{{range .Containers}} {{.Name}} {{end}}' skynet
# hello-dock
docker network inspect --format='{{range .Containers}} {{.Name}} {{end}}' bridge
# hello-dock
Come puoi vedere dagli output dei due comandi network inspect
, il container hello-dock
adesso è collegato sia alla rete skynet
che alla rete bridge
predefinita.
Il secondo modo di collegare un container a una rete è usare l'opzione --network
per i comandi container run
o container create
. La sintassi generica dell'opzione è la seguente:
--network <identificatore rete>
Per eseguire un altro container hello-dock
collegato alla stessa rete, puoi eseguire il seguente comando:
docker container run --network skynet --rm --name alpine-box -it alpine sh
# lands you into alpine linux shell
/ # ping hello-dock
# PING hello-dock (172.18.0.2): 56 data bytes
# 64 bytes from 172.18.0.2: seq=0 ttl=64 time=0.191 ms
# 64 bytes from 172.18.0.2: seq=1 ttl=64 time=0.103 ms
# 64 bytes from 172.18.0.2: seq=2 ttl=64 time=0.139 ms
# 64 bytes from 172.18.0.2: seq=3 ttl=64 time=0.142 ms
# 64 bytes from 172.18.0.2: seq=4 ttl=64 time=0.146 ms
# 64 bytes from 172.18.0.2: seq=5 ttl=64 time=0.095 ms
# 64 bytes from 172.18.0.2: seq=6 ttl=64 time=0.181 ms
# 64 bytes from 172.18.0.2: seq=7 ttl=64 time=0.138 ms
# 64 bytes from 172.18.0.2: seq=8 ttl=64 time=0.158 ms
# 64 bytes from 172.18.0.2: seq=9 ttl=64 time=0.137 ms
# 64 bytes from 172.18.0.2: seq=10 ttl=64 time=0.145 ms
# 64 bytes from 172.18.0.2: seq=11 ttl=64 time=0.138 ms
# 64 bytes from 172.18.0.2: seq=12 ttl=64 time=0.085 ms
--- hello-dock ping statistics ---
13 packets transmitted, 13 packets received, 0% packet loss
round-trip min/avg/max = 0.085/0.138/0.191 ms
Come puoi vedere, eseguire ping hello-dock
dall'interno del container alpine-box
funziona perché entrambi i contenitori sono sotto la stessa rete bridge definita da un utente e la risoluzione DNS automatica sta funzionando.
Però tieni a mente che affinché la risoluzione DNS automatica funzioni, devi assegnare nomi personalizzati ai contenitori. Usando i nomi casuali generati automaticamente non funzionerà.
Come scollegare dei container da una rete in Docker
Nella sezione precedente hai imparato come collegare i container a una rete. In questa sezione imparerai come scollegarli.
Per compiere questa azione puoi usare il comando network disconnect
. La sintassi generica è la seguente:
docker network disconnect <identificatore rete> <identificatore container>
Per scollegare il container hello-dock
dalla rete skynet
, puoi eseguire il seguente comando:
docker network disconnect skynet hello-dock
Proprio come il comando network connect
, anche il comando network disconnect
non dà output.
Come rimuovere una rete in Docker
Proprio come gli altri oggetti logici in Docker, le reti possono essere rimosse usando il comando network rm
. La sintassi generica è:
docker network rm <identificatore rete>
Per rimuovere la rete skynet
dal tuo sistema, puoi eseguire il seguente comando:
docker network rm skynet
Puoi anche usare il comando network prune
per rimuovere ogni rete inutilizzata dal tuo sistema. Il comando ha anche le opzioni -f
o --force
e -a
o --all
.
Come containerizzare un'applicazione JavaScript multi-container
Ora che hai imparato abbastanza sulle reti in Docker, in questa sezione imparerai a containerizzare un progetto multi-container a vero e proprio. Il progetto su cui lavorerai è una semplice notes-api
alimentata da Express.js e PostgreSQL.
In questo progetto, ci sono due contenitori che dovrai connettere usando una rete. Oltre questo, imparerai anche concetti come le variabili di ambiente e i volumi. Senza ulteriori indugi, andiamo subito al dunque.
Come eseguire il server del database
Il server del database in questo progetto è un semplice server PostgreSQL e utilizza l'immagine ufficiale di postgres.
Secondo la documentazione ufficiale, per eseguire un container con questa immagine, devi fornire la variabile di ambiente POSTGRES_PASSWORD
. In aggiunta, darò anche un nome per il database di default usando la variabile di ambiente POSTGRES_DB
. PostgreSQL di default è in ascolto sulla porta 5432
, quindi devi pubblicare anche quella.
Per eseguire il server del database puoi eseguire il seguente comando:
docker container run \
--detach \
--name=notes-db \
--env POSTGRES_DB=notesdb \
--env POSTGRES_PASSWORD=secret \
--network=notes-api-network \
postgres:12
# a7b287d34d96c8e81a63949c57b83d7c1d71b5660c87f5172f074bd1606196dc
docker container ls
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# a7b287d34d96 postgres:12 "docker-entrypoint.s…" About a minute ago Up About a minute 5432/tcp notes-db
L'opzione --env
per i comandi container run
e container create
può essere usata per fornire le variabili di ambiente a un container. Come puoi vedere, il container del database è stato creato con successo ed è in esecuzione.
Sebbene il container sia in esecuzione, c'è un piccolo problema. Database come PostgreSQL, MongoDB e MySQL mantengono i propri dati in una cartella. PostgreSQL usa la cartella /var/lib/postgresql/data
all'interno del container per mantenere i dati.
E se il container venisse distrutto per qualche ragione? Perderesti tutti i dati. Per risolvere questo problema può essere usato un volume con un nome.
Come lavorare con i volumi in Docker
Precedentemente hai usato bind mount e volumi anonimi. Un volume con un nome è molto simile a un volume anonimo, eccetto che puoi farvi riferimento usando il suo nome.
I volumi sono anche oggetti logici in Docker e possono essere manipolati usando la riga di comando. Il comando volume create
può essere usato per creare un volume con un nome.
La sintassi generica del comando è la seguente:
docker volume create <nome volume>
Per creare un volume chiamato notes-db-data
puoi eseguire il seguente comando:
docker volume create notes-db-data
# notes-db-data
docker volume ls
# DRIVER VOLUME NAME
# local notes-db-data
Questo volume può essere montato su /var/lib/postgresql/data
all'interno del container notes-db
. Per farlo, blocca e rimuovi il container notes-db
:
docker container stop notes-db
# notes-db
docker container rm notes-db
# notes-db
Adesso esegui un nuovo container e assegna il volume usando l'opzione --volume
o -v
.
docker container run \
--detach \
--volume notes-db-data:/var/lib/postgresql/data \
--name=notes-db \
--env POSTGRES_DB=notesdb \
--env POSTGRES_PASSWORD=secret \
--network=notes-api-network \
postgres:12
# 37755e86d62794ed3e67c19d0cd1eba431e26ab56099b92a3456908c1d346791
Adesso analizza il container notes-db
per assicurarti che il montaggio sia avvenuto con successo:
docker container inspect --format='{{range .Mounts}} {{ .Name }} {{end}}' notes-db
# notes-db-data
Ora i dati saranno conservati al sicuro all'interno del volume notes-db-data
e potranno essere riusati in futuro. È possibile usare anche un bind mount ma in questi casi preferisco un volume.
Come accedere ai log da un container in Docker
Per vedere i log da un container, puoi usare il comando container logs
. La sintassi generica del comando è la seguente:
docker container logs <identificatore container>
Per accedere ai log dal container notes-db
, puoi eseguire il seguente comando:
docker container logs notes-db
# The files belonging to this database system will be owned by user "postgres".
# This user must also own the server process.
# The database cluster will be initialized with locale "en_US.utf8".
# The default database encoding has accordingly been set to "UTF8".
# The default text search configuration will be set to "english".
#
# Data page checksums are disabled.
#
# fixing permissions on existing directory /var/lib/postgresql/data ... ok
# creating subdirectories ... ok
# selecting dynamic shared memory implementation ... posix
# selecting default max_connections ... 100
# selecting default shared_buffers ... 128MB
# selecting default time zone ... Etc/UTC
# creating configuration files ... ok
# running bootstrap script ... ok
# performing post-bootstrap initialization ... ok
# syncing data to disk ... ok
#
#
# Success. You can now start the database server using:
#
# pg_ctl -D /var/lib/postgresql/data -l logfile start
#
# initdb: warning: enabling "trust" authentication for local connections
# You can change this by editing pg_hba.conf or using the option -A, or
# --auth-local and --auth-host, the next time you run initdb.
# waiting for server to start....2021-01-25 13:39:21.613 UTC [47] LOG: starting PostgreSQL 12.5 (Debian 12.5-1.pgdg100+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0-6) 8.3.0, 64-bit
# 2021-01-25 13:39:21.621 UTC [47] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
# 2021-01-25 13:39:21.675 UTC [48] LOG: database system was shut down at 2021-01-25 13:39:21 UTC
# 2021-01-25 13:39:21.685 UTC [47] LOG: database system is ready to accept connections
# done
# server started
# CREATE DATABASE
#
#
# /usr/local/bin/docker-entrypoint.sh: ignoring /docker-entrypoint-initdb.d/*
#
# 2021-01-25 13:39:22.008 UTC [47] LOG: received fast shutdown request
# waiting for server to shut down....2021-01-25 13:39:22.015 UTC [47] LOG: aborting any active transactions
# 2021-01-25 13:39:22.017 UTC [47] LOG: background worker "logical replication launcher" (PID 54) exited with exit code 1
# 2021-01-25 13:39:22.017 UTC [49] LOG: shutting down
# 2021-01-25 13:39:22.056 UTC [47] LOG: database system is shut down
# done
# server stopped
#
# PostgreSQL init process complete; ready for start up.
#
# 2021-01-25 13:39:22.135 UTC [1] LOG: starting PostgreSQL 12.5 (Debian 12.5-1.pgdg100+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0-6) 8.3.0, 64-bit
# 2021-01-25 13:39:22.136 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432
# 2021-01-25 13:39:22.136 UTC [1] LOG: listening on IPv6 address "::", port 5432
# 2021-01-25 13:39:22.147 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
# 2021-01-25 13:39:22.177 UTC [75] LOG: database system was shut down at 2021-01-25 13:39:22 UTC
# 2021-01-25 13:39:22.190 UTC [1] LOG: database system is ready to accept connections
Come si evince dal testo della riga 57, il database è configurato e pronto per accettare connessioni esterne. Esiste anche l'opzione --follow
o -f
per questo comando, che ti permette di collegare la console all'output dei log e ottenere un flusso di testo continuo.
Come creare una rete e collegare il server del database in Docker
Come hai già imparato nella sezione precedente, i container devono essere collegati a una rete bridge definita da un utente per poter comunicare tra loro usando i nomi dei container. A questo scopo, nel tuo sistema crea una rete chiamata notes-api-network
:
docker network create notes-api-network
Adesso collega il container notes-db
a questa rete, eseguendo il seguente comando:
docker network connect notes-api-network notes-db
Come scrivere il Dockerfile
Vai nella cartella in cui hai clonato il codice del progetto. Al suo interno, vai nella cartella notes-api/api
e crea un nuovo Dockerfile
. Inserisci nel file il seguente codice:
# stage one
FROM node:lts-alpine as builder
# install dependencies for node-gyp
RUN apk add --no-cache python make g++
WORKDIR /app
COPY ./package.json .
RUN npm install --only=prod
# stage two
FROM node:lts-alpine
EXPOSE 3000
ENV NODE_ENV=production
USER node
RUN mkdir -p /home/node/app
WORKDIR /home/node/app
COPY . .
COPY --from=builder /app/node_modules /home/node/app/node_modules
CMD [ "node", "bin/www" ]
Questo è un build multi-stadio. Il primo stadio viene usato per il build e l'installazione delle dipendenze usando node-gyp
e il secondo stadio è per eseguire l'applicazione. Analizziamo brevemente i passaggi:
- Lo stadio 1 utilizza
node:lts-alpine
come base ebuilder
come nome dello stadio. - Nella riga 5, installiamo
python
,make
eg++
. Lo strumentonode-gyp
richiede questi tre pacchetti per essere eseguito. - Nella riga 7, impostiamo la cartella
/app
comeWORKDIR
. - Nelle righe 9 e 10, copiamo il file
package.json
inWORKDIR
e installiamo tutte le dipendenze. - Anche lo stadio 2 fa uso di
node-lts:alpine
come base. - Nella riga 16, impostiamo la variabile di ambiente
NODE_ENV
suproduction
. È importante affinché l'API sia eseguita in modo appropriato. - Dalla riga 18 alla 20, impostiamo utente predefinito su
node
, creiamo la cartella/home/node/app
e la impostiamo comeWORKDIR
. - Nella riga 22, copiamo tutti i file del progetto e nella riga 23 copiamo la cartella
node_modules
dallo stadiobuilder
. Questa cartella contiene tutte le dipendenze necessarie per eseguire l'applicazione. - Nella riga 25, definiamo il comando di default.
Per eseguire il build di un'immagine da questo Dockerfile
, puoi usare il seguente comando:
docker image build --tag notes-api .
# Sending build context to Docker daemon 37.38kB
# Step 1/14 : FROM node:lts-alpine as builder
# ---> 471e8b4eb0b2
# Step 2/14 : RUN apk add --no-cache python make g++
# ---> Running in 5f20a0ecc04b
# fetch http://dl-cdn.alpinelinux.org/alpine/v3.11/main/x86_64/APKINDEX.tar.gz
# fetch http://dl-cdn.alpinelinux.org/alpine/v3.11/community/x86_64/APKINDEX.tar.gz
# (1/21) Installing binutils (2.33.1-r0)
# (2/21) Installing gmp (6.1.2-r1)
# (3/21) Installing isl (0.18-r0)
# (4/21) Installing libgomp (9.3.0-r0)
# (5/21) Installing libatomic (9.3.0-r0)
# (6/21) Installing mpfr4 (4.0.2-r1)
# (7/21) Installing mpc1 (1.1.0-r1)
# (8/21) Installing gcc (9.3.0-r0)
# (9/21) Installing musl-dev (1.1.24-r3)
# (10/21) Installing libc-dev (0.7.2-r0)
# (11/21) Installing g++ (9.3.0-r0)
# (12/21) Installing make (4.2.1-r2)
# (13/21) Installing libbz2 (1.0.8-r1)
# (14/21) Installing expat (2.2.9-r1)
# (15/21) Installing libffi (3.2.1-r6)
# (16/21) Installing gdbm (1.13-r1)
# (17/21) Installing ncurses-terminfo-base (6.1_p20200118-r4)
# (18/21) Installing ncurses-libs (6.1_p20200118-r4)
# (19/21) Installing readline (8.0.1-r0)
# (20/21) Installing sqlite-libs (3.30.1-r2)
# (21/21) Installing python2 (2.7.18-r0)
# Executing busybox-1.31.1-r9.trigger
# OK: 212 MiB in 37 packages
# Removing intermediate container 5f20a0ecc04b
# ---> 637ca797d709
# Step 3/14 : WORKDIR /app
# ---> Running in 846361b57599
# Removing intermediate container 846361b57599
# ---> 3d58a482896e
# Step 4/14 : COPY ./package.json .
# ---> 11b387794039
# Step 5/14 : RUN npm install --only=prod
# ---> Running in 2e27e33f935d
# added 269 packages from 220 contributors and audited 1137 packages in 140.322s
#
# 4 packages are looking for funding
# run `npm fund` for details
#
# found 0 vulnerabilities
#
# Removing intermediate container 2e27e33f935d
# ---> eb7cb2cb0b20
# Step 6/14 : FROM node:lts-alpine
# ---> 471e8b4eb0b2
# Step 7/14 : EXPOSE 3000
# ---> Running in 4ea24f871747
# Removing intermediate container 4ea24f871747
# ---> 1f0206f2f050
# Step 8/14 : ENV NODE_ENV=production
# ---> Running in 5d40d6ac3b7e
# Removing intermediate container 5d40d6ac3b7e
# ---> 31f62da17929
# Step 9/14 : USER node
# ---> Running in 0963e1fb19a0
# Removing intermediate container 0963e1fb19a0
# ---> 0f4045152b1c
# Step 10/14 : RUN mkdir -p /home/node/app
# ---> Running in 0ac591b3adbd
# Removing intermediate container 0ac591b3adbd
# ---> 5908373dfc75
# Step 11/14 : WORKDIR /home/node/app
# ---> Running in 55253b62ff57
# Removing intermediate container 55253b62ff57
# ---> 2883cdb7c77a
# Step 12/14 : COPY . .
# ---> 8e60893a7142
# Step 13/14 : COPY --from=builder /app/node_modules /home/node/app/node_modules
# ---> 27a85faa4342
# Step 14/14 : CMD [ "node", "bin/www" ]
# ---> Running in 349c8ca6dd3e
# Removing intermediate container 349c8ca6dd3e
# ---> 9ea100571585
# Successfully built 9ea100571585
# Successfully tagged notes-api:latest
Prima di eseguire un container usando questa immagine, assicurati che il container del database sia in esecuzione e collegato a notes-api-network
.
docker container inspect notes-db
# [
# {
# ...
# "State": {
# "Status": "running",
# "Running": true,
# "Paused": false,
# "Restarting": false,
# "OOMKilled": false,
# "Dead": false,
# "Pid": 11521,
# "ExitCode": 0,
# "Error": "",
# "StartedAt": "2021-01-26T06:55:44.928510218Z",
# "FinishedAt": "2021-01-25T14:19:31.316854657Z"
# },
# ...
# "Mounts": [
# {
# "Type": "volume",
# "Name": "notes-db-data",
# "Source": "/var/lib/docker/volumes/notes-db-data/_data",
# "Destination": "/var/lib/postgresql/data",
# "Driver": "local",
# "Mode": "z",
# "RW": true,
# "Propagation": ""
# }
# ],
# ...
# "NetworkSettings": {
# ...
# "Networks": {
# "bridge": {
# "IPAMConfig": null,
# "Links": null,
# "Aliases": null,
# "NetworkID": "e4c7ce50a5a2a49672155ff498597db336ecc2e3bbb6ee8baeebcf9fcfa0e1ab",
# "EndpointID": "2a2587f8285fa020878dd38bdc630cdfca0d769f76fc143d1b554237ce907371",
# "Gateway": "172.17.0.1",
# "IPAddress": "172.17.0.2",
# "IPPrefixLen": 16,
# "IPv6Gateway": "",
# "GlobalIPv6Address": "",
# "GlobalIPv6PrefixLen": 0,
# "MacAddress": "02:42:ac:11:00:02",
# "DriverOpts": null
# },
# "notes-api-network": {
# "IPAMConfig": {},
# "Links": null,
# "Aliases": [
# "37755e86d627"
# ],
# "NetworkID": "06579ad9f93d59fc3866ac628ed258dfac2ed7bc1a9cd6fe6e67220b15d203ea",
# "EndpointID": "5b8f8718ec9a5ec53e7a13cce3cb540fdf3556fb34242362a8da4cc08d37223c",
# "Gateway": "172.18.0.1",
# "IPAddress": "172.18.0.2",
# "IPPrefixLen": 16,
# "IPv6Gateway": "",
# "GlobalIPv6Address": "",
# "GlobalIPv6PrefixLen": 0,
# "MacAddress": "02:42:ac:12:00:02",
# "DriverOpts": {}
# }
# }
# }
# }
# ]
Ho accorciato l'output per facilitare la visualizzazione. Sul mio sistema, il container notes-db
è in esecuzione, utilizza il volume notes-db-data
ed è collegato alla rete bridge notes-api-network
.
Una volta che ti sei assicurato che è tutto in regola, puoi eseguire un nuovo container con il seguente comando:
docker container run \
--detach \
--name=notes-api \
--env DB_HOST=notes-db \
--env DB_DATABASE=notesdb \
--env DB_PASSWORD=secret \
--publish=3000:3000 \
--network=notes-api-network \
notes-api
# f9ece420872de99a060b954e3c236cbb1e23d468feffa7fed1e06985d99fb919
Dovresti essere in grado di capire da solo questo lungo comando, quindi esaminerò brevemente le variabili di ambiente.
L'applicazione notes-api
richiede di impostare tre variabili di ambiente:
DB_HOST
- È l'host del server del database. Dato che sia il database del server che l'API sono collegati alla stessa rete bridge definita da un utente, il server del database può essere indicato usando il nome del suo container, che in questo caso ènotes-db
.DB_DATABASE
- Il database che userà questa API. Eseguendo il server del database, abbiamo impostato il nome predefinito sunotesdb
usando la variabile di ambientePOSTGRES_DB
. La useremo qui.DB_PASSWORD
- La password per connettere il database. Anche questa è stata impostata nella sezione Eseguire il server del database, usando la variabile di ambientePOSTGRES_PASSWORD
.
Per vedere se il container è correttamente in esecuzione o no, puoi usare il comando container ls
:
docker container ls
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# f9ece420872d notes-api "docker-entrypoint.s…" 12 minutes ago Up 12 minutes 0.0.0.0:3000->3000/tcp notes-api
# 37755e86d627 postgres:12 "docker-entrypoint.s…" 17 hours ago Up 14 minutes 5432/tcp notes-db
Il container adesso è in esecuzione. Puoi andare su http://127.0.0.1:3000/
per vedere l'API in azione.

L'API ha 5 rotte in totale, che puoi vedere nel file /notes-api/api/api/routes/notes.js
.
Sebbene il container sia in esecuzione, c'è un'ultima cosa che devi fare prima di poterlo usare. Dovrai eseguire la migrazione del database necessaria per configurare le tabelle del database, e puoi farlo eseguendo il comando npm run db:migrate
all'interno del container.
Come eseguire i comandi in un container in esecuzione
Hai già imparato come eseguire i comandi in un container stoppato. Un'altra situazione è eseguire un comando in un container in esecuzione.
Per questo dovrai usare il comando exec
per eseguire un comando personalizzato all'interno del container in esecuzione.
La sintassi generica per il comando exec
è la seguente:
docker container exec <identificatore container> <comando>
Per eseguire npm run db:migrate
all'interno del container notes-api
, puoi usare il seguente comando:
docker container exec notes-api npm run db:migrate
# > notes-api@ db:migrate /home/node/app
# > knex migrate:latest
#
# Using environment: production
# Batch 1 run: 1 migrations
In casi in cui vuoi eseguire un comando interattivo all'interno di un container in esecuzione, dovrai usare l'opzione -it
. Come esempio, se vuoi accedere alla shell in esecuzione all'interno del container notes-api
, puoi usare il comando:
docker container exec -it notes-api sh
# / # uname -a
# Linux b5b1367d6b31 5.10.9-201.fc33.x86_64 #1 SMP Wed Jan 20 16:56:23 UTC 2021 x86_64 Linux
Come scrivere degli script di gestione in Docker
Gestire un progetto multi-container insieme a rete, volumi e compagnia, significa scrivere un sacco di comandi. Per semplificare il processo, di solito mi servo di semplici script di shell e di un Makefile.
Troverai i seguenti quattro script di shell nella cartella notes-api
:
boot.sh
- Usato per avviare i container, se esistono già.build.sh
- Crea ed esegue i container. All'occorrenza crea anche immagini, volumi e reti.destroy.sh
- Rimuove tutti i container, volumi e reti associati a un progetto.stop.sh
- Ferma tutti i container in esecuzione.
C'è anche un Makefile
che contiene quattro target chiamati start
, stop
, build
e destroy
, ognuno dei quali invoca i precedenti script di shell.
Eseguire make stop
dovrebbe fermare tutti i container in esecuzione sul tuo sistema. Eseguire make destroy
dovrebbe fermare tutti i container e rimuovere tutto. Assicurati di lanciare gli script all'interno della cartella notes-api
:
make destroy
# ./shutdown.sh
# stopping api container --->
# notes-api
# api container stopped --->
# stopping db container --->
# notes-db
# db container stopped --->
# shutdown script finished
# ./destroy.sh
# removing api container --->
# notes-api
# api container removed --->
# removing db container --->
# notes-db
# db container removed --->
# removing db data volume --->
# notes-db-data
# db data volume removed --->
# removing network --->
# notes-api-network
# network removed --->
# destroy script finished
Se stai ottenendo un errore di autorizzazione negata (permission denied), allora esegui chmod +x
sugli script:
chmod +x boot.sh build.sh destroy.sh shutdown.sh
Non spiegherò questi script perché sono semplici istruzioni if-else
insieme ad alcuni comandi Docker che hai già visto molte volte. Se hai un po' di conoscenza della shell di Linux, dovresti essere in grado di capire anche questi script.
Come comporre i progetti usando Docker-Compose
Nella sezione precedente, hai imparato come gestire un progetto multi-container e le difficoltà che implicate. Invece di scrivere così tanti comandi, esiste un modo più facile per gestire dei progetti multi-container, grazie a uno strumento chiamato Docker Compose.
Secondo la documentazione di Docker:
Compose è uno strumento per definire ed eseguire applicazioni multi-container in Docker. Con compose si utilizza un file YAML per configurare i servizi di un'applicazione. Poi, con un singolo comando, si creano e avviano tutti i servizi dalla configurazione.
Sebbene Compose funzioni in tutti gli ambienti, è più incentrato sullo sviluppo e il collaudo. Usare Compose in un ambiente di produzione non è assolutamente consigliato.
Le basi di Docker Compose
Vai nella cartella in cui hai clonato il repository in dotazione con questo manuale. Vai nella cartella notes-api/api
e crea un file Dockerfile.dev
. Inserisci al suo interno il seguente codice:
# stage one
FROM node:lts-alpine as builder
# install dependencies for node-gyp
RUN apk add --no-cache python make g++
WORKDIR /app
COPY ./package.json .
RUN npm install
# stage two
FROM node:lts-alpine
ENV NODE_ENV=development
USER node
RUN mkdir -p /home/node/app
WORKDIR /home/node/app
COPY . .
COPY --from=builder /app/node_modules /home/node/app/node_modules
CMD [ "./node_modules/.bin/nodemon", "--config", "nodemon.json", "bin/www" ]
Il codice è quasi identico al Dockerfile
con cui hai lavorato nella sezione precedente, eccetto per le seguenti tre differenze:
- Nella riga 10, eseguiamo
npm install
invece dinpm run install --only=prod
perché vogliamo anche le dipendenze di sviluppo. - Nella riga 15, impostiamo la variabile di ambiente
NODE_ENV
sudevelopment
invece diproduction
. - Nella riga 24, usiamo uno strumento chiamato nodemon per ottenere la funzionalità di ricaricamento rapido per l'API.
Sai già che questo progetto ha due container:
notes-db
- Un server del database alimentato da PostgreSQL.notes-api
- Una API REST alimentata da Express.js
Nel mondo di Compose, ogni container che costituisce l'applicazione è detto servizio. Il primo passo nel comporre un progetto multi-container è definire questi servizi.
Proprio come il demone Docker utilizza un Dockerfile
per il build delle immagini, Docker Compose usa un file docker-compose.yaml
da cui legge le definizioni dei servizi.
Vai nella cartella notes-api
e crea un nuovo file docker-compose.yaml
. Metti il seguente codice all'interno del file appena creato:
version: "3.8"
services:
db:
image: postgres:12
container_name: notes-db-dev
volumes:
- notes-db-dev-data:/var/lib/postgresql/data
environment:
POSTGRES_DB: notesdb
POSTGRES_PASSWORD: secret
api:
build:
context: ./api
dockerfile: Dockerfile.dev
image: notes-api:dev
container_name: notes-api-dev
environment:
DB_HOST: db ## same as the database service name
DB_DATABASE: notesdb
DB_PASSWORD: secret
volumes:
- /home/node/app/node_modules
- ./api:/home/node/app
ports:
- 3000:3000
volumes:
notes-db-dev-data:
name: notes-db-dev-data
Un file docker-compose.yaml
valido inizia con la definizione della versione del file. Al momento della stesura di questo manuale, l'ultima versione era 3.8
. Puoi controllare qui l'ultima versione.
I blocchi in un file YAML sono definiti dall'indentazione. Esaminerò ogni blocco e spiegherò cosa fa.
- Il blocco
services
contiene le definizioni per ognuno dei servizi o container nell'applicazione.db
eapi
sono i due servizi che costituiscono questo progetto. - Il blocco
db
definisce un nuovo servizio nell'applicazione e contiene le informazioni necessarie per avviare il container. Ogni servizio richiede un'immagine predefinita o unDockerfile
per l'esecuzione di un container. Per il serviziodb
stiamo usando l'immagine ufficiale di PostgreSQL. - A differenza del servizio
db
, non esiste un'immagine di cui è già stato fatto il build per il servizioapi
. Quindi useremo il fileDockerfile.dev
. - Il blocco
volumes
definisce ogni volume con nome necessario per ogni servizio. Al momento elenca solo il volumenotes-db-dev-data
usato dal serviziodb
.
Ora che hai avuto una panoramica ad alto livello del file docker-compose.yaml
, diamo un'occhiata più da vicino ai singoli servizi.
Il codice della definizione per il servizio db
è il seguente:
db:
image: postgres:12
container_name: notes-db-dev
volumes:
- db-data:/var/lib/postgresql/data
environment:
POSTGRES_DB: notesdb
POSTGRES_PASSWORD: secret
- La chiave
image
contiene il repository e il tag dell'immagine usata per questo contenitore. Stiamo eseguendo l'immaginepostgres:12
per eseguire il container del database. container_name
indica il nome del container. Di default i container sono chiamati seguendo la sintassi<nome directory progetto>_<nome servizio>
. Puoi sovrascriverlo usandocontainer_name
.- L'array
volumes
contiene la mappatura dei volumi per il servizio e supporta volumi con nome, volumi anonimi e bind mount. La sintassi<sorgente>:<destinazione>
è identica a quella che hai già visto prima. - La mappa
environment
contiene i valori delle diverse variabili di ambiente necessarie per il servizio.
Il codice di definizione per il servizio api
è il seguente:
api:
build:
context: ./api
dockerfile: Dockerfile.dev
image: notes-api:dev
container_name: notes-api-dev
environment:
DB_HOST: db ## same as the database service name
DB_DATABASE: notesdb
DB_PASSWORD: secret
volumes:
- /home/node/app/node_modules
- ./api:/home/node/app
ports:
- 3000:3000
- Il servizio
api
non ha un'immagine di cui è già stato fatto il build, ma possiede una configurazione di build. Sotto il bloccobuild
definiamo il contesto e il nome del Dockerfile per il build di un'immagine. Ormai dovresti avere una buona comprensione del contesto e del Dockerfile, quindi non mi ci soffermerò oltre. - La chiave
image
contiene il nome dell'immagine di cui fare il build. Se non assegnato, all'immagine verrà dato un nome seguendo la sintassi<nome directory progetto>_<nome servizio>
. - All'interno della mappa
environment
, la variabileDB_HOST
dà prova di una funzionalità di Compose, ovvero che puoi fare riferimento a un altro servizio nella stessa applicazione usando il suo nome. Quindi,db
sarà sostituito dall'indirizzo IP del container del servizioapi
. Le variabiliDB_DATABASE
eDB_PASSWORD
devono corrispondere rispettivamente conPOSTGRES_DB
ePOSTGRES_PASSWORD
dalla definizione del serviziodb
. - Nella mappa
volumes
, puoi vedere descritti un volume anonimo e un bind mount. La sintassi è identica a quella che hai visto nelle sezionii precedenti. - La mappa
ports
definisce ogni port mapping. La sintassi,<porta host>:<porta container>
è identica all'opzione--publish
usata precedentemente.
Infine, il codice per volumes
è il seguente:
volumes:
db-data:
name: notes-db-dev-data
Ogni volume con un nome usato in qualsiasi dei servizi deve essere definito qui. Se non definisci un nome, il volume sarà chiamato seguendo <nome directory progetto>_<chiave volume>
dove, in questo caso, la chiave è db-data
.
Puoi imparare le diverse opzioni per la configurazione dei volumi nella documentazione ufficiale.
Come avviare i servizi in Docker Compose
Esistono diversi modi per avviare dei servizi definiti in un file YAML. Il primo comando che imparerai a conoscere è up
. Il comando up
svolge il build di ogni immagine mancante, crea container e li avvia in un colpo solo.
Prima di eseguire il comando, assicurati di aver aperto il terminale nella stessa directory in cui si trova il file docker-compose.yaml
. Questo è molto importante per tutti i comandi docker-compose
che esegui.
docker-compose --file docker-compose.yaml up --detach
# Creating network "notes-api_default" with the default driver
# Creating volume "notes-db-dev-data" with default driver
# Building api
# Sending build context to Docker daemon 37.38kB
#
# Step 1/13 : FROM node:lts-alpine as builder
# ---> 471e8b4eb0b2
# Step 2/13 : RUN apk add --no-cache python make g++
# ---> Running in 197056ec1964
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container 197056ec1964
# ---> 6609935fe50b
# Step 3/13 : WORKDIR /app
# ---> Running in 17010f65c5e7
# Removing intermediate container 17010f65c5e7
# ---> b10d12e676ad
# Step 4/13 : COPY ./package.json .
# ---> 600d31d9362e
# Step 5/13 : RUN npm install
# ---> Running in a14afc8c0743
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container a14afc8c0743
# ---> 952d5d86e361
# Step 6/13 : FROM node:lts-alpine
# ---> 471e8b4eb0b2
# Step 7/13 : ENV NODE_ENV=development
# ---> Running in 0d5376a9e78a
# Removing intermediate container 0d5376a9e78a
# ---> 910c081ce5f5
# Step 8/13 : USER node
# ---> Running in cfaefceb1eff
# Removing intermediate container cfaefceb1eff
# ---> 1480176a1058
# Step 9/13 : RUN mkdir -p /home/node/app
# ---> Running in 3ae30e6fb8b8
# Removing intermediate container 3ae30e6fb8b8
# ---> c391cee4b92c
# Step 10/13 : WORKDIR /home/node/app
# ---> Running in 6aa27f6b50c1
# Removing intermediate container 6aa27f6b50c1
# ---> 761a7435dbca
# Step 11/13 : COPY . .
# ---> b5d5c5bdf3a6
# Step 12/13 : COPY --from=builder /app/node_modules /home/node/app/node_modules
# ---> 9e1a19960420
# Step 13/13 : CMD [ "./node_modules/.bin/nodemon", "--config", "nodemon.json", "bin/www" ]
# ---> Running in 5bdd62236994
# Removing intermediate container 5bdd62236994
# ---> 548e178f1386
# Successfully built 548e178f1386
# Successfully tagged notes-api:dev
# Creating notes-api-dev ... done
# Creating notes-db-dev ... done
L'opzione --detach
o -d
funziona nello stesso modo che hai visto precedentemente. L'opzione --file
o -f
è necessaria solo se il file YAML non è chiamato docker-compose.yaml
(ma l'ho usata qui a scopo di dimostrazione).
Oltre al comando up
, c'è il comando start
. La differenza principale tra i due è che il comando start
non crea container mancanti, ma avvia solo container già esistenti. In pratica è l'equivalente del comando container start
.
L'opzione --build
per il comando up
forza un rebuild delle immagini. Ci sono alcune altre opzioni per il comando up
che puoi vedere nella documentazione ufficiale.
Come elencare i servizi in Docker Compose
Sebbene i container dei servizi avviati da Compose possano essere elencati usando il comando container ls
, esiste il comando ps
per elencare i container definiti soltanto in YAML.
docker-compose ps
# Name Command State Ports
# -------------------------------------------------------------------------------
# notes-api-dev docker-entrypoint.sh ./nod ... Up 0.0.0.0:3000->3000/tcp
# notes-db-dev docker-entrypoint.sh postgres Up 5432/tcp
Non dà tutte le informazioni che fornisce l'output di container ls
, ma è utile quando ci sono un sacco di container in esecuzione simultanea.
Come eseguire dei comandi all'interno di un servizio in esecuzione in Docker Compose
Spero che ricordi dalla sezione precedente che per creare le tabelle del database per questa API devi eseguire alcuni script di migrazione.
Proprio come per il comando container exec
, c'è un comando exec
per docker-compose
. La sintassi generica del comando è la seguente:
docker-compose exec <nome servizio> <comando>
Per eseguire il comando npm run db:migrate
all'interno del servizio api
, puoi eseguire il seguente comando:
docker-compose exec api npm run db:migrate
# > notes-api@ db:migrate /home/node/app
# > knex migrate:latest
#
# Using environment: development
# Batch 1 run: 1 migrations
A differenza del comando container exec
, non devi passare il flag -it
per le sessioni interattive. docker-compose
lo fa automaticamente.
Come accedere ai log da un servizio in esecuzione in Docker Compose
Puoi anche usare il comando logs
per recuperare i log da un servizio in esecuzione. La sintassi generica del comando è:
docker-compose logs <nome servizio>
Per accedere ai log dal servizio api
, esegui il seguente comando:
docker-compose logs api
# Attaching to notes-api-dev
# notes-api-dev | [nodemon] 2.0.7
# notes-api-dev | [nodemon] reading config ./nodemon.json
# notes-api-dev | [nodemon] to restart at any time, enter `rs`
# notes-api-dev | [nodemon] or send SIGHUP to 1 to restart
# notes-api-dev | [nodemon] ignoring: *.test.js
# notes-api-dev | [nodemon] watching path(s): *.*
# notes-api-dev | [nodemon] watching extensions: js,mjs,json
# notes-api-dev | [nodemon] starting `node bin/www`
# notes-api-dev | [nodemon] forking
# notes-api-dev | [nodemon] child pid: 19
# notes-api-dev | [nodemon] watching 18 files
# notes-api-dev | app running -> http://127.0.0.1:3000
Questa è solo una porzione dell'output. Puoi agganciarti al flusso di output del servizio e ottenere i log in tempo reale usando l'opzione -f
o --follow
. Ogni log successivo verrà mostrato istantaneamente nel terminale finché non esci premendo ctrl + c
o chiudendo la finestra. L'esecuzione del container continuerà anche se esci dalla finestra dei log.
Come fermare dei servizi in Docker Compose
Per stoppare dei servizi, puoi intraprendere due approcci. Il primo è il comando down
, che blocca tutti i container in esecuzione e li rimuove dal sistema. Rimuove anche tutte le reti:
docker-compose down --volumes
# Stopping notes-api-dev ... done
# Stopping notes-db-dev ... done
# Removing notes-api-dev ... done
# Removing notes-db-dev ... done
# Removing network notes-api_default
# Removing volume notes-db-dev-data
L'opzione --volumes
indica che vuoi rimuovere ogni volume con nome definito nel blocco volumes
. Puoi imparare quali sono le opzioni aggiuntive per il comando down
nella documentazione ufficiale.
Un altro modo per stoppare i servizi è il comando stop
, che funziona allo stesso modo del comando container stop
. Ferma tutti i container di un'applicazione e li mantiene. Questi container possono essere riavviati in seguito con il comando start
o up
.
Come comporre un'applicazione full-stack in Docker Compose
In questa sezione, aggiungeremo un front-end alla nostra API e la trasformeremo in un'applicazione full-stack completa. Non spiegherò nulla riguardo ai file Dockerfile.dev
(eccetto quello per il servizio nginx
) dato che sono identici a quelli esaminati nelle sezioni precedenti.
Se hai clonato il repository con il codice del progetto, vai nella cartella fullstack-notes-application
. Ogni cartella all'interno della radice del progetto contiene il codice per ogni servizio e il corrispondente Dockerfile
.
Prima di partire con il file docker-compose.yaml
, diamo un'occhiata al diagramma che illustra come funzionerà l'applicazione:
Invece di accettare richieste direttamente, come prima, in questa applicazione tutte le richieste saranno ricevute da un servizio NGINX (che chiameremo router).
Il router poi valuterà se l'end-point richiesto ha /api
al suo interno. Se è così, instraderà la richiesta al back-end o, in caso contrario, al front-end.
Questo perché un'applicazione front-end non viene eseguita in un container, ma nel browser, servito da un container. Come risultato, il networking di Compose non funziona come atteso e l'applicazione front-end fallisce nel trovare il servizio api
.
NGINX, d'altro canto, viene eseguito in un container e può comunicare con i diversi servizi nell'intera applicazione.
Non mi addentrerò nella configurazione di NGINX. Si tratta di un argomento fuori dallo scopo di questo manuale, ma se vuoi dare un'occhiata, guarda pure i file /notes-api/nginx/development.conf
e /notes-api/nginx/production.conf
. Il codice per /notes-api/nginx/Dockerfile.dev
è il seguente:
FROM nginx:stable-alpine
COPY ./development.conf /etc/nginx/conf.d/default.conf
Tutto ciò che fa è copiare la configurazione del file in /etc/nginx/conf.d/default.conf
all'interno del container.
Iniziamo scrivendo il file docker-compose.yaml
. Oltre ai servizi api
e db
, ci saranno i servizi client
e nginx
. Ci saranno anche delle definizioni di rete di cui parlerò a breve.
version: "3.8"
services:
db:
image: postgres:12
container_name: notes-db-dev
volumes:
- db-data:/var/lib/postgresql/data
environment:
POSTGRES_DB: notesdb
POSTGRES_PASSWORD: secret
networks:
- backend
api:
build:
context: ./api
dockerfile: Dockerfile.dev
image: notes-api:dev
container_name: notes-api-dev
volumes:
- /home/node/app/node_modules
- ./api:/home/node/app
environment:
DB_HOST: db ## same as the database service name
DB_PORT: 5432
DB_USER: postgres
DB_DATABASE: notesdb
DB_PASSWORD: secret
networks:
- backend
client:
build:
context: ./client
dockerfile: Dockerfile.dev
image: notes-client:dev
container_name: notes-client-dev
volumes:
- /home/node/app/node_modules
- ./client:/home/node/app
networks:
- frontend
nginx:
build:
context: ./nginx
dockerfile: Dockerfile.dev
image: notes-router:dev
container_name: notes-router-dev
restart: unless-stopped
ports:
- 8080:80
networks:
- backend
- frontend
volumes:
db-data:
name: notes-db-dev-data
networks:
frontend:
name: fullstack-notes-application-network-frontend
driver: bridge
backend:
name: fullstack-notes-application-network-backend
driver: bridge
Il file è quasi identico al precedente su cui hai lavorato. L'unica cosa che richiede delle spiegazioni è la configurazione della rete. Il codice per il blocco networks
è il seguente:
networks:
frontend:
name: fullstack-notes-application-network-frontend
driver: bridge
backend:
name: fullstack-notes-application-network-backend
driver: bridge
Ho definito due reti bridge. Di default, Compose crea una rete bridge a cui collega tutti i container. In questo progetto, tuttavia, volevo una rete appropriatamente isolata. Quindi ho definito due reti, una per i servizi front-end e una per i servizi back-end.
Ho anche aggiunto un blocco networks
in ogni definizione dei servizi. In questo modo i servizi api
e db
saranno collegati a una rete e il servizio client
sarà collegato a una rete separata. Ma il servizio nginx
sarà collegato a entrambe le reti in modo da agire come router tra i servizi front-end e back-end.
Avvia tutti i servizi eseguendo il seguente comando:
docker-compose --file docker-compose.yaml up --detach
# Creating network "fullstack-notes-application-network-backend" with driver "bridge"
# Creating network "fullstack-notes-application-network-frontend" with driver "bridge"
# Creating volume "notes-db-dev-data" with default driver
# Building api
# Sending build context to Docker daemon 37.38kB
#
# Step 1/13 : FROM node:lts-alpine as builder
# ---> 471e8b4eb0b2
# Step 2/13 : RUN apk add --no-cache python make g++
# ---> Running in 8a4485388fd3
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container 8a4485388fd3
# ---> 47fb1ab07cc0
# Step 3/13 : WORKDIR /app
# ---> Running in bc76cc41f1da
# Removing intermediate container bc76cc41f1da
# ---> 8c03fdb920f9
# Step 4/13 : COPY ./package.json .
# ---> a1d5715db999
# Step 5/13 : RUN npm install
# ---> Running in fabd33cc0986
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container fabd33cc0986
# ---> e09913debbd1
# Step 6/13 : FROM node:lts-alpine
# ---> 471e8b4eb0b2
# Step 7/13 : ENV NODE_ENV=development
# ---> Using cache
# ---> b7c12361b3e5
# Step 8/13 : USER node
# ---> Using cache
# ---> f5ac66ca07a4
# Step 9/13 : RUN mkdir -p /home/node/app
# ---> Using cache
# ---> 60094b9a6183
# Step 10/13 : WORKDIR /home/node/app
# ---> Using cache
# ---> 316a252e6e3e
# Step 11/13 : COPY . .
# ---> Using cache
# ---> 3a083622b753
# Step 12/13 : COPY --from=builder /app/node_modules /home/node/app/node_modules
# ---> Using cache
# ---> 707979b3371c
# Step 13/13 : CMD [ "./node_modules/.bin/nodemon", "--config", "nodemon.json", "bin/www" ]
# ---> Using cache
# ---> f2da08a5f59b
# Successfully built f2da08a5f59b
# Successfully tagged notes-api:dev
# Building client
# Sending build context to Docker daemon 43.01kB
#
# Step 1/7 : FROM node:lts-alpine
# ---> 471e8b4eb0b2
# Step 2/7 : USER node
# ---> Using cache
# ---> 4be5fb31f862
# Step 3/7 : RUN mkdir -p /home/node/app
# ---> Using cache
# ---> 1fefc7412723
# Step 4/7 : WORKDIR /home/node/app
# ---> Using cache
# ---> d1470d878aa7
# Step 5/7 : COPY ./package.json .
# ---> Using cache
# ---> bbcc49475077
# Step 6/7 : RUN npm install
# ---> Using cache
# ---> 860a4a2af447
# Step 7/7 : CMD [ "npm", "run", "serve" ]
# ---> Using cache
# ---> 11db51d5bee7
# Successfully built 11db51d5bee7
# Successfully tagged notes-client:dev
# Building nginx
# Sending build context to Docker daemon 5.12kB
#
# Step 1/2 : FROM nginx:stable-alpine
# ---> f2343e2e2507
# Step 2/2 : COPY ./development.conf /etc/nginx/conf.d/default.conf
# ---> Using cache
# ---> 02a55d005a98
# Successfully built 02a55d005a98
# Successfully tagged notes-router:dev
# Creating notes-client-dev ... done
# Creating notes-api-dev ... done
# Creating notes-router-dev ... done
# Creating notes-db-dev ... done
Adesso vai su http://localhost:8080
e voilà!

Prova ad aggiungere e cancellare delle note per vedere se l'applicazione funziona adeguatamente. Il progetto comprende anche degli script da shell e un Makefile
. Sperimenta per vedere come puoi eseguire questo progetto senza l'aiuto di docker-compose
come hai fatto nella sezione precedente.
Conclusione
Ti ringrazio dal profondo del mio cuore per il tempo che hai speso leggendo questo manuale. Spero che ti sia piaciuto e che tu abbia imparato tutto l'essenziale di Docker.
Oltre questo, ho scritto manuali completi su altri argomenti complicati che sono disponibili gratuitamente su freeCodeCamp.
Questi manuali sono parte della mia missione di semplificare per tutti l'apprendimento di tecnologie difficili da comprendere. La stesura di ognuno di questi manuali richiede molto tempo e impegno.
Se ti è piaciuto leggere questo manuale e vuoi motivarmi, considera di lasciarmi delle stelle su GitHub e sostienimi per le mie competenze su LinkedIn. Accetto anche il supporto tramite buymeacoffee.
Sono sempre aperto a suggerimenti e discussioni su Twitter o LinkedIn. Contattami con un messaggio privato.
Infine, considera di condividere queste risorse con altri:
Condividere il sapere è l'atto di amicizia più importante, perché è un modo in cui puoi dare qualcosa senza perdere nulla. — Richard Stallman
Buono studio, ci vediamo alla prossima.