Articolo originale: https://www.freecodecamp.org/news/the-docker-handbook/

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

Il codice dei progetti

Il codice dei progetti di esempio è disponibile nel seguente repository:

GitHub - fhsinchy/docker-handbook-projects: Project codes used in “The Docker Handbook”
Project codes used in “The Docker Handbook”. Contribute to fhsinchy/docker-handbook-projects development by creating an account on GitHub.
d76adb00-c391-11ea-8ecb-718db51373b2
dammi una ⭐ per mantenermi motivato

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:

GitHub - fhsinchy/the-docker-handbook: An open-source full-length book on Docker
An open-source full-length book on Docker. Contribute to fhsinchy/the-docker-handbook development by creating an account on GitHub.
the-docker-handbook
dammi una ⭐ per mantenermi motivato

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:

The Docker Handbook - The Docker Handbook
assets%2F-MD_PXBw_anEnk5G-Lck%2F-MD_PhskKcWWHdAgEPMM%2F-MD_QKqWhQ7Ymti30dvJ%2Fdocker-handbook-preview
non dimenticare di lasciare una ⭐ sul repository

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

The Docker Handbook – Learn Docker for Beginners
The concept of containerization itself is pretty old. But the emergence of the Docker Engine [https://docs.docker.com/get-started/overview/#docker-engine] in2013 has made it much easier to containerize your applications. According to the Stack Overflow Developer Survey - 2020[https://insights.stackoverflow.com/survey/2020#overview…
docker-1280x612-2021
condividi questo manuale

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.

drag-docker-in-applications-directory

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.

docker-icon-in-menubar

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:

  1. Vai su questo sito e segui le istruzioni per installare WSL2 su Windows 10.
  2. Vai sulla pagina ufficiale di download e clicca sul pulsante Download for Windows (stable).
  3. 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.

docker-icon-in-taskbar

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.

docker-and-compose-version-on-windows

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:

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.

docker-and-compose-version-on-linux

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.

virtual-machines

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.

containers

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.

docker-hub

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

my-images-on-docker-hub

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:

  1. 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.
  2. Docker Client: Il client (docker) è un'interfaccia da riga di comando principalmente responsabile del trasporto dei comandi lanciati dagli utenti.
  3. 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:

docker-run-hello-world

Questa immagine è una versione leggermente modificata di quella presente nella documentazione ufficiale. Gli eventi che si verificano quando esegui il comando sono i seguenti:

  1. Esegui il comando docker run hello-world dove hello-world è il nome dell'immagine.
  2. Il client di Docker comunica con il demone, dicendogli di prendere l'immagine hello-world ed eseguire un container per questa immagine.
  3. 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.
  4. Il demone poi comunica con il registro pubblico, Docker Hub, e scarica l'ultima copia dell'immagine hello-world, indicata dalla riga latest: Pulling from library/hello-world nel terminale.
  5. Docker daemon poi crea un nuovo container dall'immagine appena scaricata.
  6. 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 oggetto container, image, network o volume.
  • comando indica l'azione che deve essere svolta dal demone, cioè il comando run.
  • 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.

hello-dock

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.

hello-dock

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:

GitHub - fhsinchy/rmbyext: Recursively removes all files with given extension(s)
Recursively removes all files with given extension(s) - GitHub - fhsinchy/rmbyext: Recursively removes all files with given extension(s)
rmbyext
dammi una ⭐ per tenermi motivato

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.

nginx-default

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'istruzione FROM. Questa istruzione imposta la base dell'immagine risultante. Impostando ubuntu:latest come immagine base, ottieni tutte le qualità di Ubuntu già disponibili nella tua immagine personalizzata, così da poter usare cose come il comando apt-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'istruzione EXPOSE 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 un Dockerfile esegue un comando all'interno della shell. Il comando apt-get update && apt-get install nginx -y controlla le versioni aggiornate del pacchetto e installa NGINX. Il comando apt-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 istruzioni RUN qui sono scritte in forma shell. Possono anche essere scritte in forma exec. 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 forma exec, costituita da tre parti separate. nginx si riferisce all'eseguibile NGINX. -g e daemon 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'istruzione CMD Può ancora essere scritta in forma shell. 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 chiamato Dockerfile 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.

nginx-default

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 di 60MB 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 file nginx-1.19.2.tar.gz all'interno dell'immagine. La sintassi generica dell'istruzione COPY è 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 usando tar 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 comando rm.
  • 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'istruzione ARG. 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, chiamata ADD, 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 usato nginx-1.19.2 come nome del file e tar.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 comando image 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 riga https://nginx.org/download/${FILENAME}.${EXTENSION} darà come risultato https://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'istruzione ARG.
  • L'istruzione ADD non estrae i file ottenuti da internet in modo predefinito, per cui l'utilizzo di tar 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.

nginx-default

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 e zlib1g 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, usiamo apk add. L'opzione --no-cache vuol dire che i pacchetti scaricati non saranno salvati nella cache. Allo stesso modo, useremo apk del invece di apt-get remove per disinstallare i pacchetti.
  • L'opzione --virtual per il comando apk 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 comando apk 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 tag 3-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'istruzione RUN nella riga 5, installa git, poi installa lo script rmbyext usando Git e pip. In seguito, si libera anche di git.
  • Infine, nella riga 9, l'istruzione ENTRYPOINT imposta lo script rmbyext 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 comando npm 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 tag lts-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 come node. 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 chiamato node, che puoi impostare come utente di default usando l'istruzione USER.
  • L'istruzione RUN mkdir -p /home/node/app crea una cartella chiamata app all'interno della cartella home dell'utente node. 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 istruzione COPY, ADD, RUN e CMD successiva.
  • L'istruzione COPY copia il file package.json che contiene informazioni riguardo a tutte le dipendenze necessarie per l'applicazione. L'istruzione RUN esegue il comando npm install, che è il comando predefinito per installare le dipendenze usando un file package.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 forma exec.
  • Il server di sviluppo vite gira di default sulla porta 3000, e aggiungere un comando EXPOSE 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.

hello-dock-dev

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.

local-vs-container-file-system

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.

bind-mounts

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 di nginx.
  • Crea l'immagine finale sulla base di nginx e scarta tutta la roba correlata a node.

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 sintassi as 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 applicazioni vite.
  • 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 stadio builder. 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:

hello-dock

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 rete host è 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 e notes-db il container dell'API sarà in grado di connettersi al container del database usando il nome notes-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 e builder come nome dello stadio.
  • Nella riga 5, installiamo python, make e g++. Lo strumento node-gyp richiede questi tre pacchetti per essere eseguito.
  • Nella riga 7, impostiamo la cartella /app come WORKDIR .
  • Nelle righe 9 e 10, copiamo il file package.json in WORKDIR 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 su production. È 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 come WORKDIR.
  • Nella riga 22, copiamo tutti i file del progetto e nella riga 23 copiamo la cartella node_modules dallo stadio builder. 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 su notesdb usando la variabile di ambiente POSTGRES_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 ambiente POSTGRES_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.

bonjour-mon-ami

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 di npm run install --only=prod perché vogliamo anche le dipendenze di sviluppo.
  • Nella riga 15, impostiamo la variabile di ambiente NODE_ENV su development invece di production.
  • 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 e api 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 un Dockerfile per l'esecuzione di un container. Per il servizio db 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 servizio api. Quindi useremo il file Dockerfile.dev.
  • Il blocco volumes definisce ogni volume con nome necessario per ogni servizio. Al momento elenca solo il volume notes-db-dev-data usato dal servizio db.

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'immagine postgres: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 usando container_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 blocco build 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 variabile DB_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 servizio api. Le variabili DB_DATABASE e DB_PASSWORD devono corrispondere rispettivamente con POSTGRES_DB e POSTGRES_PASSWORD dalla definizione del servizio db.
  • 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:

fullstack-application-design

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à!

notes-application

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.