Block Image

Abbiamo due modalità per creare immagini custom:

  1. creando un'immagine a partire da un container col comando docker commit
  2. creando un'immagine da un Dockerfile eseguendo il comando docker build

Creare un'immagine a partire da un container: il comando docker commit

È possibile creare un'immagine a partire da un container, magari modificato dopo che è stato avviato, in modo tale che il container creato da questa nuova immagine, abbia già le modifiche effettuate precedentemente.

Ad esempio creiamo un container da un'immagine NGINX dal Docker Hub:

Block Image

Ispezioniamo il container per conoscere il suo indirizzo IP (in un successivo articolo, parleremo nel network di Docker):

Block Image

In questo caso l'indirizzo IP è 172.17.0.2. Inoltre nella sezione Ports, possiamo conoscere la porta in ascolto:

"Ports": {
                "80/tcp": null
            },

Digitiamo da browser l'indirizzo IP di NGINX: avremo la pagine di benvenuto di default di NGINX:

Block Image

Modifichiamo ora la pagina di default di NGINX, entrando nel container:

docker exec -it nginx-container /bin/bash

ora digitiamo i comandi per installare un editor di testo, visto che di default il container non ha nè VIM nè NANO:

apt update && apt install vim

modifichiamo a nostro piacimento il file HTML che contiene la pagina di benvenuto di NGINX, che si trova al path:
/usr/share/nginx/html/index.html.

Una volta modificata la pagina, possiamo aggiornare il nostro browser e vedere che la pagina è stata cambiata.

Ora creiamo un'immagine a partire da questo container, in modo tale da avere subito di default la pagina custom e il VIM installato.
Per fare ciò, digitiamo il comando:

docker commit nginx-container nginx-custom

Avremo ora una nuova immagine custom di nginx chiamata nginx-custom!

Block Image

Se provassimo ora a creare un container a partire dall'immagine custom di NGINX, avremo la nostra pagina modificata e il VIM installato.

Il Dockerfile

È sempre una scelta migliore creare un'immagine a partire da un Dockerfile piuttosto che da un container, per un discorso di documentazione e manutenibilità.
Creando l'immagine tramite un file, appunto il Dockerfile, è possibile versionare quest'ultimo come qualsiasi altro file e quindi avere i benefici di un sistema di versioning.

Il Dockerfile è un file di testo che contiene delle istruzioni necessarie per la creazione di un'immagine.
Deve necessariamente iniziare con l'istruzione FROM.

Un esempio di Dockerfile: creare un'immagine di una app Java

Per introdurre i comandi e il funzionamento del Dockerfile, proveremo a containerizzare una app Java, in particolare useremo una semplice app Spring Boot utilizzata in questo articolo: Spring Boot Data Rest

Il Dockerfile utilizzato sarà il seguente:

FROM openjdk:11-jdk
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application/app.jar
RUN groupadd -g 1000 java_group
RUN useradd -u 1000 -ms /bin/bash -g java_group java_user
USER java_user
EXPOSE 8080
WORKDIR application
ENTRYPOINT ["java", "-jar", "app.jar"]
CMD ["arg1", "arg2"]

Vediamo adesso uno per uno le istruzioni nel dettaglio

L'istruzione FROM

L'istruzione FROM serve a specificare l'immagine parent. Si parte sempre da un immagine parent per creare un'immagine custom.
Come abbiamo detto nel precedente articolo (Primi passi con Docker), un'immagine può essere formata da più layers (più immagini).
L'istruzione FROM crea il primo layer. Ogni altra istruzione che modifica il filesystem (come le istruzioni RUN e COPY), creerà un ulteriore layer.

In questo caso, la nostra immagine di partenza è openjdk:11-jdk, che a sua volta è formata da più immagini.
Infatti si basa su un'immagine di Debian, a cui poi viene aggiunta l'installazione di Java.

L'istruzione ARG

L'istruzione ARG serve a dichiarare una variabile che sarà disponibile solo durante la creazione del container.

In questo caso, assumendo che abbiamo buildato il progetto e creato quindi il suo corrispondete file JAR, stiamo assegnano alla variabile JAR_FILE il path del JAR.

L'istruzione COPY

L'istruzione COPY copia file o cartelle dal path host sorgente (primo argomento) sul filesystem del container (secondo argomento).
Il path sorgente può contenere wildcards (ad esempio noi abbiamo aggiunto l'asterisco in target/*.jar).
Si possono specificare più file sorgenti. Se la cartella di destinazione non esiste, verrà creata automaticamente da Docker (come in questo caso, con la cartella application). L'esecuzione di questa istruzione, visto che modifica il filesystem dell'immagine, creerà un nuovo layer.

COPY vs ADD

Esiste un'altra istruzione simile a COPY, ovvero ADD. Anch'esso copia file e cartelle sul filesystem del container.
A differenza di COPY però, è possibile specificare come sorgente anche una URL remota. Se non ricadiamo in questo ultimo caso, è consigliabile utilizzare l'istruzione COPY.

L'istruzione RUN

L'istruzione RUN permette l'esecuzione di comandi su un nuovo livello, sopra l'immagine corrente.
L'immagine risultante verrà utilizzata dal passo successivo nel Dockerfile.

Creare più layers è un concetto fondamentale di Docker: vengono committate singole istruzioni cosicché si possa creare il container da qualsiasi punto (layer) dell'immagine.

Nell'esempio abbiamo utilizzato due volte l'istruzione per creare un gruppo chiamato java_group e un utente chiamato java_user.

L'istruzione RUN può avere queste due forme:

  1. RUN <command> (forma shell; il comando viene lanciato in una shell, che di default è /bin/sh -c per Linux e cmd /S /C per Windows).
  2. RUN ["executable", "param1", "param2" (forma exec).

Nel Dockerfile di esempio viene utilizzata la prima forma. Un esempio di seconda forma:

RUN ["npm","install"]
L'istruzione USER

Questa istruzione permette d'impostare l'utente per l'esecuzione dell'immagine e di qualunque istruzione RUN, CMD ed ENTRYPOINT.
Di default, se non specifichiamo un utente, il container verrà avviato con permessi di ROOT.

Sebbene non siano attive alcune CAPABILITIES, è sempre buona norma, per motivi di sicurezza, avviare i container senza privilegi di ROOT.

Se vuoi sapere di più sulle capabilities Linux:
https://man7.org/linux/man-pages/man7/capabilities.7.html
L'istruzione EXPOSE

Questa istruzione informa Docker che il container è in ascolto su una o più porte. Puoi specificare se le porte sono in ascolto su TCP o UDP (il default è TCP).
L'istruzione EXPOSE non pubblica effettivamente la porta. Ha la funziona di documentare quali porte vengono utilizzate dal container.
Per pubblicare una porta quando il container è avviato, può essere usata la flag -p quando si esegue docker run (parleremo più nel dettaglio di questo in un articolo sul network di Docker).

L'istruzione WORKDIR

Questa istruzione imposta la cartella di lavoro per le istruzioni RUN, CMD, ENTRYPOINT, COPY e ADD.
Se la WORKDIR non esiste, verrà creata da Docker.

Le istruzione ENTRYPOINT e CMD

Queste istruzioni permettono di eseguire il container con un certo comando.

CMD permette di eseguire un comando di default che verrà eseguito solamente se non si specifica un comando durante la creazione del container, col comando docker run.
ENTRYPOINT invece, verrà eseguito sempre (a meno che non venga esplicitamente sovrascritto col la flag --entrypoint col comando docker run).

CMD ha tre forme:

  1. CMD ["executable","param1","param2"] (forma exec, che è la forma da preferire)
  2. CMD ["param1","param2"] (che permette di dare dei parametri di default al comando specificato nell'istruzione ENTRYPOINT)
  3. CMD <command param1 param2> (forma shell)

ENTRYPOINT ha due forme:

  1. ENTRYPOINT ["executable", "param1", "param2"] (forma exec, da preferire)
  2. ENTRYPOINT <command param1 param2> (forma shell)

Nel Dockerfile di esempio viene utilizzata la forma 2 di CMD e la forma 1 di ENTRYPOINT.
CMD quindi in questo caso aggiunge dei parametri di default al comando specificato da ENTRYPOINT; questi parametri possono essere sovrascritti durante l'esecuzione del comando docker run.

La forma shell di ENTRYPOINT impedisce l'uso di argomenti tramite CMD o tramite il comando docker run, ma ha lo svantaggio di creare un sottocomando di /bin/sh -c che non passa segnali. Questo significa che il vero comando eseguibile del container non avrà PID 1, e l'eseguibile non riceverà un segnale di SIGTERM dal comando docker stop <container>.

Se nel Dockerfile vi sono più istruzioni ENTRYPOINT, verrà eseguita solo l'ultima. Lo stesso vale per CMD.

Creiamo ora l'immagine dal Dockerfile: il comando docker build

Block Image
Struttura del progetto Spring Boot Rest

Questo comando permette di creare un'immagine a partire da un Dockerfile.
Posizioniamoci nella cartella contenente il Dockerfile, in questo caso la root del progetto spring-boot-rest ed eseguiamo il comando:

docker build -t spring-boot-rest .
  1. Con -t diamo un nome all'immagine, specificando eventualmente un tag.
  2. Con "." includiamo le cartelle e file all'interno del path corrente. Alternativamente, è possibile specificare un path.
docker build -t spring-boot-rest .
Sending build context to Docker daemon  40.97MB
Step 1/10 : FROM openjdk:11-jdk
11-jdk: Pulling from library/openjdk
bb7d5a84853b: Pull complete 
f02b617c6a8c: Pull complete 
d32e17419b7e: Pull complete 
c9d2d81226a4: Pull complete 
fab4960f9cd2: Pull complete 
da1c1e7baf6d: Pull complete 
Digest: sha256:5d235a84e8e0817f8e35327c5f6ab6a81cad2fa5e9a3d9998aa8f2eb891c6c8e
Status: Downloaded newer image for openjdk:11-jdk
 ---> 40eccaa4f420
Step 2/10 : ARG JAR_FILE=target/*.jar
 ---> Running in b0df0cc5ffe4
Removing intermediate container b0df0cc5ffe4
 ---> d114ca11b755
Step 3/10 : COPY ${JAR_FILE} application/app.jar
 ---> dabd1db4c49e
Step 4/10 : RUN groupadd -g 1000 java_group
 ---> Running in e991fd3061a1
...
Step 9/10 : ENTRYPOINT ["java", "-jar", "app.jar"]
 ---> Running in 36b44717a478
Removing intermediate container 36b44717a478
 ---> 70b960a6ece8
Step 10/10 : CMD ["arg1", "arg2"]
 ---> Running in b28ac3f6421a
Removing intermediate container b28ac3f6421a
 ---> cdc8072078d8
Successfully built cdc8072078d8
Successfully tagged spring-boot-rest:latest

Da notare che ogni istruzione è contrassegnato da uno STEP, e ogni step crea un container intermedio, che viene rimosso.

Creiamo ora il container dall'immagine Docker:

docker run --name=spring-boot -d spring-boot-rest

Vediamo la lista di container attivi:

docker ps
CONTAINER ID   IMAGE              COMMAND                  CREATED              STATUS              PORTS      NAMES
0e51fbcdaf58   spring-boot-rest   "java -jar app.jar a…"   About a minute ago   Up About a minute   8080/tcp   spring-boot

Il nostro container è up & running!

Vediamo anche i log col comando docker logs:

Block Image

Da notare che vengono stampati anche gli argomenti di default arg1 e arg2, contenenti nell'istruzione CMD.

Possiamo ulteriormente verificare che tutto sia ok, provando prima a capire quel è l'indirizzo IP del container, con docker inspect (come fatto nell'esempio precedente) e poi fare una cURL. Nel mio caso:

curl 172.17.0.2:8080
{
  "_links" : {
    "users" : {
      "href" : "http://172.17.0.2:8080/users{?page,size,sort}",
      "templated" : true
    },
    "profile" : {
      "href" : "http://172.17.0.2:8080/profile"
    }
  }
}

Il Web Service ha risposto correttamente.

Ora creiamo un altro container dalla stessa immagine, quest volta passando degli argomenti:

docker run --name=spring-boot-args -d spring-boot-rest change args

Venendo i log, possiamo notare che effettivamente i parametri dell'istruzione CMD sono stati correttamente sovrascritti:

docker logs spring-boot-args
...
change
args

Export ed import di un'immagine Docker

Spesso si ha la necessità di esportare un'immagine Docker e poi importarla su un altro host (se non si utilizzano i registry).

Proviamo a esportare l'immagine spring-boot-rest appena creata.
Utilizziamo il comando:

docker save spring-boot-rest > spring-boot-rest.tar

Questo comando crea un archivio tar contenente l'immagine.
Per importare poi l'immagine dall'archivio, basta eseguire il comando:

docker load < spring-boot-rest.tar

Conclusioni

In questo articolo abbiamo visto come creare un'immagine custom sia a partire da un container, sia a partire da un Dockerfile. Abbiamo visto varie istruzioni del Dockerfile per containerizzare una app Java Spring Boot.
Infine, abbiamo visto come esportare e importare un'immagine Docker.

Per altre istruzioni del Dockerfile: https://docs.docker.com/engine/reference/builder/
Link GitHub del progetto Spring Boot Rest: Spring Boot Rest
Articoli su Docker: Docker

Libri consigliati su Docker e Kubernetes: