Build di un immagine NGINX con Dockerfile

Introduzione

Docker ha rivoluzionato il modo di creare, gestire immagini e deployare container. Ma ti sei mai chiesto come creare un’immagine custom? O hai mai sentito parlare di Dockerfile? In questo articolo vedremo come è possibile creare e distribuire un immagine custom attraverso i Dockerfiles. Sarà fatto un esempio pratico della creazione di un’immagine custom NGINX basata su ubuntu 22.04

Cosa sono i Dockerfiles?

Un Dockerfile è semplicemente un file di testo contenente un set di istruzioni che dicono a Docker (o BuildCTL) come creare un’immagine per una specifica applicazione o servizio.
Queste istruzioni vengono eseguite nell’ordine in cui sono scritte, per questo il Dockerfile è uno strumento così potente, infatti questa caratteristica rende il processo di creazione facile e manutenibile.

Cosa sono i Deckerville?

Un Dockerfile è semplicemente un file di testo contenente un set di istruzioni che dicono a Docker (o BuildCTL) come creare un’immagine per una specifica applicazione o servizio.
Queste istruzioni vengono eseguite nell’ordine in cui sono scritte, per questo il Dockerfile è uno strumento così potente, infatti questa caratteristica rende il processo di creazione facile e manutenibile.

Anatomia di un Dockerfile

Partiamo subito col dire che un Dockerfile non ha bisogno di estensioni ma, per essere riconosciuto come tale, deve proprio chiamarsi Dockerfile.
Dentro il Dockerfile troviamo, come detto, le istruzioni per la creazione delle immagini. I vari comandi che inseriamo al suo interno non sono case-sensitive, tuttavia per convenzione tutte le istruzioni sono scritte in MAIUSCOLO in modo tale da distinguere i comandi dai loro argomenti.

Istruzione FROM

La prima istruzione ESSENZIALE di ogni Dockerfile è l’istruzione FROM, questa istruzione specifica l’immagine di base del container su cui lavorare per la creazione della nostra immagine. Questa immagine può essere un’immagine ufficiale del Docker Hub o una nostra immagine creata da noi stessi in precedenza. Se volessimo creare una nostra immagine del popolare web server NGINX, che si poggia su ubuntu:22.04 allora la prima riga del nostro Dockerfile sarà:

Il FROM come abbiamo detto è la prima istruzione di qualsiasi Dockerfile, tuttavia ci sono delle eccezioni, infatti può essere preceduta da:

  • Parser Directives
  • Commenti
  • Valori ARGs globali

Va comunque tenuto bene a mente che l’immagine di base deve includere tutte le dipendenze necessarie e le librerie per l’esecuzione dell’applicazione.

FROM ubuntu : 22.04

Istruzione RUN

Seconda istruzione che generalmente incontriamo è RUN, istruzione che ci consente di eseguire comandi all’interno del container ad esempio per l’installazione di pacchetti o per l’aggiornamento. Gli argomenti passati all’istruzione RUN sono eseguiti in fase di build e il risultato è memorizzato nell’immagine finale. Nel nostro caso siamo interessati all’installazione di NGINX quindi avremo:

RUN apt update && apt upgrade -y && apt install -y nginx

Istruzione COPY

Altra istruzione molto importante è l’istruzione COPY che ci permette di copiare file e directory dal file system dell’host all’interno del container. Questa è una istruzione particolarmente utile per inserire file di configurazione, nel nostro particolare caso del server NGINX potremmo essere interessati ai file di configurazione e le eventuali pagine html nginx.conf, default.conf e index.html. Quindi, a patto di avere questi file salvati all’interno della stessa directory in cui è presente il nostro Dockerfile aggiungeremo le seguenti righe al suo interno:

COPY nginx.conf /etc/nginx/nginx.conf
COPY default.conf /etc/nginx/conf.d/default.conf
COPY index.html /usr/share/nginx/html/index.html

Quindi ricapitolando quanto fatto finora:

  • Abbiamo usato come immagine base ubuntu:22.04
  • Abbiamo detto di fare eventuali aggiornamenti e di installare NGINX
  • Abbiamo copiato i vari file necessari all’interno del container

Ora però sorgono spontanee due domande. Come diciamo al nostro container di essere in ascolto sulla porta 80? E come avviamo effettivamente NGINX?

Istruzione EXPOSE

L’istruzione EXPOSE viene utilizzata per esplicitare su che porta il nostro container sarà in ascolto ed eventualmente che protocollo usare se TCP o UDP, di default sarà automaticamente utilizzato il protocollo TCP se non specifichiamo nulla.
Nel nostro caso vogliamo che NGINX sia in ascolto sulla porta 80 e che utilizzi il protocollo TCP, quindi scriveremo:

EXPOSE 80/tcp

Istruzione CMD

A questo punto non ci resta che far avviare NGINX in modo che il container una volta che eseguito resti attivo e che funzioni effettivamente. Per far ciò usiamo l’istruzione CMD, questo comando specificherà che comando lanciare appena il container parte.
Di istruzione CMD deve essercene una sola, qualora ne specifichiamo più di una, solo l’ultima verrà effettivamente vista come comando effettivo da lanciare. Abbiamo più modi di scrivere l’istruzione CMD:

CMD [“executable”, “param1”, “param2”] (Raccomandata)
CMD [“param1”, “param2”]
CMD command param1 param2

A questo punto inseriamo l’ultima riga del nostro file.

CMD [“nginx”, “-g”, “daemon off;”]

Va ricordato che è il CMD che specifica l’entry point dell’applicazione.

Altro?

Ovviamente abbiamo molte più istruzioni per il Dockerfile. Rimandiamo comunque alla documentazione ufficiale di Docker per avere una più approfondita conoscenza delle varie istruzioni utilizzabili.

Il Dockerfile completo

Possiamo ricapitolare quanto abbiamo visto finora presentando l’intero Dockerfile (N.B. i caratteri # indicano un commento all’interno del Dockerfile, questi possono essere molto utili per specificare cosa stiamo effettivamente facendo):

# Uso come base l’immagine di ubuntu 22.04
FROM ubuntu:22.04
# Faccio gli update e installo NGINX
RUN apt update && apt upgrade -y && apt install nginx -y
# Copio gli eventuali file utili all’interno
COPY nginx.conf /etc/nginx/nginx.conf
COPY default.conf /etc/nginx.conf.d/default.conf
COPY index.html /usr/share/nginx/html/index.html
# Dico di usare la porta 80 di default verrà usato il protocollo TCP
EXPOSE 80
# Avvio nginx
CMD [“nginx”, “-g”, “daemon off;”]

Questione di Layers

Come abbiamo visto è possibile dare più istruzioni e comandi all’interno del nostro Dockerfile. Potrebbe sembrare una buona idea scrivere molte istruzioni per rendere minimo il lavoro di aggiornamento delle immagini e aumentare il livello di customizzazione. Tuttavia non è così.
In Docker ogni istruzione all’interno del Dockerfile crea quello che si chiama layer. Un layer è una parte dell’immagine che rappresenta una modifica rispetto al layer precedente. Ad ogni istruzione nel Dockerfile corrisponde un nuovo layer.

All’interno del Dockerfile è consigliato avere pochi layers, infatti abbiamo vari vantaggi tra cui:

  • Immagini più piccole: avremo immagini più facili da trasferire e gestire
  • Migliori performance: ogni layer viene caricato in memoria, averne pochi migliora le performance
  • Più facile da capire e mantenere: facilita l’identificazione di bug.

Ci sono vantaggi e casi in cui avere più layer diventa vantaggioso, ad esempio quando vogliamo isolare parti dell’applicazione per renderla più modulare e dunque far sì che queste parti risultino facilmente aggiornabili o sostituibili.