Problema con l’uso della libreria curl con PHP

curl-logo
curl-logo

Da tempo sono perseguitato da un errore che si ripete (per quanto sospetto) ad ogni aggiornamento di php e che riguarda la libreria curl.

Sia l’invocazione da Apache che da client mi presentano un errore con questo tono:

$ php -r "curl_init();"
PHP Warning:  PHP Startup: Unable to load dynamic library 'curl.so' (tried: 
    /usr/lib/php/20210902/curl.so    (/usr/lib/php/20210902/curl.so: undefined symbol: curl_mime_addpart, version CURL_OPENSSL_4), 
    /usr/lib/php/20210902/curl.so.so (/usr/lib/php/20210902/curl.so.so: undefined symbol: curl_mime_addpart, version CURL_OPENSSL_4)
) in Unknown on line 0
PHP Fatal error:  Uncaught Error: Call to undefined function curl_init() in Command line code:1
Stack trace:
\#0 {main}
  thrown in Command line code on line 1

Un paio di volte ho pensato di aver risolvto il problema (vedi link in fondo al post) ma il problema è risaltato fuori al primo aggiornamento proposto da Canonical.

È stato stressante non trovare una soluzione in tutti questi anni perché non ne avevo il tempo. Solo che bisogna ad un certo punto venire ad un compromesso tra le cose da fare e lo stress che ci procura il nostro lavoro. Passato un certo segno, lo stress non è più tollerabile per cui le cose da fare passano assolutamente in seconda categoria di importanza.

Fondamentalmente l’articolo che mi trovavo sempre a leggere quando googlavo su questo argomento è quello riportato con Risorsa web [1] con la risposta illuminante dello sviluppatore finlandese:

And there’s your problem.

Toni Viemerö

Il riferimento che fa todeveni è al fatto che un aggiornamento manuale della libreria curl ha rotto quella di sistema.

Intanto mi assicuro che il file oggetto esista

$ stat /usr/lib/php/20210902/curl.so
  File: /usr/lib/php/20210902/curl.so
  Dim.: 117000      Blocchi: 232        Blocco di IO: 4096   file regolare
Device: 801h/2049d  Inode: 12327696    Coll.: 1
Accesso: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
Accesso  : 2022-09-09 10:21:33.681311330 +0200
Modifica : 2022-08-15 11:40:11.000000000 +0200
Cambio   : 2022-09-02 15:54:45.221958306 +0200
Creazione: 2022-09-02 15:54:45.181957917 +0200

Il file c’è. Poi cerco se è stato agganciato per PHP. Per vedere questo devo vedere la directory dei mods-available del PHP (non quella di Apache!):

$ cd /etc/php/8.1/mods-available/
$ ll | grep curl
-rw-r--r-- 1 root root   68 dic 31  2021 curl.ini
$ $ cat curl.ini 
; configuration for php curl module
; priority=20
extension=curl.so

Sempre nell’articolo in cui todeveni spiega cos’è successo trovo questo comando che mi conferma che c’è un problema nella libreria:

$ ldd /usr/lib/php/20210902/curl.so 
/usr/lib/php/20210902/curl.so: 
   /usr/local/lib/libcurl.so.4: 
      no version information available (required by /usr/lib/php/20210902/curl.so)
   linux-vdso.so.1 (0x00007fffe63f1000)
...

Il comando ldd (List Dynamic Dependencies) stampa gli oggetti condivisi (librerie condivise) richiesti da ciascun programma o oggetto condiviso specificato nella riga di comando. Un esempio del suo utilizzo e output è il seguente:

marcob@jsbach:/usr/local/lib$ ldd /bin/ls
	linux-vdso.so.1 (0x00007ffe77541000)
	libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007fda76ff6000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fda76dce000)
	libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007fda76d32000)
	/lib64/ld-linux-x86-64.so.2 (0x00007fda77069000)

Tornando al file /usr/lib/php/20210902/curl.so, se cerco le sue dipendenze trovo

marcob@jsbach:/etc/php$ ldd /usr/lib/php/20210902/curl.so 
/usr/lib/php/20210902/curl.so: /usr/local/lib/libcurl.so.4: no version information available (required by /usr/lib/php/20210902/curl.so)
	linux-vdso.so.1 (0x00007fffe63f1000)
	libcurl.so.4 => /usr/local/lib/libcurl.so.4 (0x00007f9a73200000)
...

Attenzione: trovo che il file nella libreria di PHP (/usr/lib/php/20210902/) dipende da un file posizionato nalla cartella /usr/local/lib/ e però è quest’ultimo ad avere qualche problema.

Provo a localizzare il file libcurl.so.4; ce n’è una marea:

$ locate libcurl.so.4
/home/marcob/.snap/auxdata/netcdf_natives/8.0.0/amd64/libcurl.so.4
/home/marcob/.snap/auxdata/netcdf_natives/8.0.5/amd64/libcurl.so.4
/home/marcob/.snap/auxdata/netcdf_natives/8.0.8/amd64/libcurl.so.4
/home/marcob/.snap/auxdata/netcdf_natives/8.0.9/amd64/libcurl.so.4
....
/usr/local/lib/libcurl.so.4 <-- questo è il file che vedo con ldd /usr/lib/php/20210902/curl.so 
/usr/local/lib/libcurl.so.4.4.0
/var/lib/flatpak/runtime/org.freedesktop.Platform/x86_64/18.08/1f7a5575c84c1df838ff07540cbc155e1eb78fee764860cbcbfab6d766328588/files/lib/x86_64-linux-gnu/libcurl.so.4
...

Però effettivamente il php punta a quello nella cartella /usr/local/lib/.

Attenzione: se cerco quali sono le librerie da da cui dipende il file in questa cartella non vedo nessun errore:

$ ldd /usr/local/lib/libcurl.so.4 | grep curl
$

ma non trovo neanche una dipendenza.

Ora però vediamo chi è davvero questo file so:

$ ls -l /usr/local/lib/libcurl.so.4
lrwxrwxrwx 1 root root 16 set  7 11:23 /usr/local/lib/libcurl.so.4 -> libcurl.so.4.4.0

Questo file non fa riferimento ad una libreria di sistema ma al file libcurl.so.4.4.0 che è stato buildato il 23/05/2017 e che ormai non è più allineato con quello ufficiale rilasciato da Canonical.

La soluzione arriva dalla Risorsa Web [4].

Dunque siamo alla soluzione, che comprende due operazioni

Rimuovo il file .so dalla /usr/lib/local

marcob@jsbach:/etc/php$ cd /usr/local/lib
marcob@jsbach:/usr/local/lib$ ll
totale 14368
drwxr-xr-x 10 root root     4096 set  7 11:23 ./
drwxr-xr-x 19 root root     4096 set 24  2021 ../
-rw-r--r--  1 root root     1957 set  2 17:07 curl.txt
-rw-r--r--  1 root root  1006444 mag 23  2017 libcurl.a
-rwxr-xr-x  1 root root     1042 mag 23  2017 libcurl.la*
lrwxrwxrwx  1 root root       16 set  7 11:23 libcurl.so.4 -> libcurl.so.4.4.0*
-rwxr-xr-x  1 root root   542272 mag 23  2017 libcurl.so.4.4.0*
-rw-r--r--  1 root root  4129514 gen 30  2018 libfilezilla.a
...
$ 
marcob@jsbach:/usr/local/lib$ sudo unlink libcurl.so.4
marcob@jsbach:/usr/local/lib$

E lo faccio puntare ad una libreria di sistema

$ sudo ln -s /usr/lib/x86_64-linux-gnu/libcurl.so.4 
$ ll libcurl.so.4 
lrwxrwxrwx  1 root root       38 set  9 11:35 libcurl.so.4 -> /usr/lib/x86_64-linux-gnu/libcurl.so.4

Praticamente sono passato da

libcurl.so.4 -> libcurl.so.4.4.0

a

libcurl.so.4 -> /usr/lib/x86_64-linux-gnu/libcurl.so.4

Ora, se ripeto un comando che dava l’errore:

marcob@jsbach:/usr/local/lib$ php -r "curl_init();"
marcob@jsbach:/usr/local/lib$

Ma qualsiasi comando con php dovrebbe darlo e non lo da più. In particolare quello che mi conferma che ora il modulo curl è caricato per il client:

marcob@jsbach:/usr/local/lib$ php -m | grep curl
curl

Faccio ripartire anche Apache:

marcob@jsbach:/usr/local/lib$ sudo apache2ctl restart
marcob@jsbach:/usr/local/lib$

E finalmente il comando phpinfo(); mi mostra il modulo caricato anche da Apache:

php curl, problema risolto
php curl, problema risolto

Risorse web

  1. Primo tentativo di fixare il problema
  2. Secondo tentativo
  3. Il file a cui punta il modulo PHP non è corretto (Todeveni)
  4. Una soluzione definitiva (spero, grazie a Chenjian e Scott Skiles)
  5. Pagina Linux man di ldd

Network: quando l’interfaccia di rete non comunica con l’esterno.

network: non raggiungo nessun host per nome
network: non raggiungo nessun host per nome

Mi è capitato un paio di volte di rimanere in braghe di tela col network: il browser mi dice che non sono connesso a internet, un ping generico all’host che voglio raggiungere mi dice questo:

$ ping www.google.com
ping: www.google.com: Nome o servizio sconosciuto

Sintomo 2: posso fare ping direttamente ad un nameserver pubblico come quello di Google:

marcob@jsbach:~$ ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=115 time=15.9 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=115 time=15.8 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=115 time=15.7 ms
^C

Sì, ok, non è detto che la porta ICMP di qualunque server sia aperta perché PING sia un comando che sempre ci può dire se un host è vivo oppure no. Però nel caso di Google va bene.

Sembra quindi che la mia interfaccia di rete sia attiva ma non sia in grado di raggiungere i DNS, perché l’interfaccia ha effettivamente un indirizzo IP:

$ ifconfig 
...
enp1s0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 10.1.23.54  netmask 255.255.255.0  broadcast 10.1.23.255
        inet6 fe80::f152:defa:9f22:1355  prefixlen 64  scopeid 0x20<link>
        ether dc:4a:3e:db:c2:e9  txqueuelen 1000  (Ethernet)
        RX packets 394867  bytes 307996101 (307.9 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 280505  bytes 82589311 (82.5 MB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
...

Spegnere e riaccendere l’interfaccia non da il risultato sperato

$ sudo if down/up enp1s0

Nemmeno il riavvio del servizio di networking:

$ sudo service networking restart

L’unico comando che funziona (che permette di resuscitare il nome a dominio) è aprire la configurazione di rete (Impostazioni > Rete) e impostare il Metodo ipv4 su “Automatico (DHCP”) (era impostato su “Manuale”…) :

GUI gestione network
GUI gestione network

Non so la causa di questo malfunzionamento, può essere che l’attivazione di una delle VPN che uso sconfiguri l’interfaccia cancellando i record DNS.

In effetti in questa schermata gli IP address dei server DNS primario e secondario sono scritti a mano (per un altro malfunzionamento di una VPN che non mi configura in automatico questi dati). Forse l’origine del problema è proprio qui.

Risorse web

Cercare info su un pacchetto Ubuntu 22.04

Ubuntu logo 2022
Official Ubuntu logo since 2022 March; used in the 22.04 release for the first time.

Ci sono vari modi in Ubuntu per cercare informazioni su un pacchetto, in generale sono mutuamente esclusivi (un pacchetto si gestisce con un gestore o con l’altro, mai con tutti quanti).

Esempi

$ dpkg --print-avail vim-common
Package: vim-common
Priority: important
Section: editors
Installed-Size: 323
Origin: Ubuntu
Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
Bugs: https://bugs.launchpad.net/ubuntu/+filebug
Architecture: amd64
Source: vim
Version: 2:7.4.712-2ubuntu4
Replaces: vim-gui-common (<< 2:7.4.488-4~)
Depends: libc6 (>= 2.3.4)
Recommends: vim | vim-gnome | vim-gtk | vim-athena | vim-nox | vim-tiny
Breaks: vim-gui-common (<< 2:7.4.488-4~)
Filename: pool/main/v/vim/vim-common_7.4.712-2ubuntu4_amd64.deb
Size: 102918
MD5sum: 53a26189ee129613ef153cc8d43dbb4c
Description: Vi IMproved - Common files
Original-Maintainer: Debian Vim Maintainers <pkg-vim-maintainers@lists.alioth.debian.org>
SHA1: 30391762a053f76f766b263f5adb9665108b12fd
SHA256: 87b12bd7bba0df3e5ffdd1fcfc38103a6cc2aeb4958717f5f7dd635051c5a2f6
Homepage: http://www.vim.org/
Description-md5: dc8579ec9ee0dc36b43d271645170c36
Supported: 9m
Task: minimal

oppure

$ apt-cache show conky
Package: conky
Architecture: all
Version: 1.12.2-1
Priority: optional
Section: universe/utils
Origin: Ubuntu
Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
Original-Maintainer: Vincent Cheng <vcheng@debian.org>
Bugs: https://bugs.launchpad.net/ubuntu/+filebug
Installed-Size: 28
Depends: conky-std | conky-cli | conky-all
Filename: pool/universe/c/conky/conky_1.12.2-1_all.deb
Size: 3244
MD5sum: 471cf8fb8bcd0d625eb765dcd769a6e4
SHA1: 858a207e872da62504526dad7121734e4dcbed7c
SHA256: 64ea4228325b775b1f714cc8a1b26ca2d871de6142b8de70a017b4addf9b4b2b
SHA512: 2540b39ce9a36185646b70feba7fe42f2d62633e4aa7f8c1f14bbc8aa9a36c2b1708c2bf7f4d245835e790ff4f59ebe04ee9bc7be85c09c44bc0eb67ec33d312
Homepage: https://github.com/brndnmtthws/conky
Description-it: monitor di sistema altamente configurabile (pacchetto di transizione)
 Conky è un monitor di sistema che può visualizzare quasi tutto, sulla
 finestra root del desktop o in una propria finestra. Conky ha molti
 oggetti incorporati, oltre alla capacità di eseguire programmi o script
 esterni (sia esternamente sia attraverso l'uso interno di Lua).
 .
 Questo è un pacchetto fittizio per facilitare la transizione al nuovo
 schema di pacchettizzazione. Può essere rimosso senza problemi dopo
 l'aggiornamento/l'installazione.
Description-md5: 6ae2b0f9855afb8d0a149407cf95bd93

oppure

$ aptitude show conky
Pacchetto: conky                                    
Versione: 1.12.2-1
Stato: non installato
Priorità: opzionale
Sezione: universe/utils
Responsabile: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
Architettura: all
Dimensione pacchetto installato: 28,7 k
Dipende: conky-std | conky-cli | conky-all
Descrizione: monitor di sistema altamente configurabile (pacchetto di transizione)
 Conky è un monitor di sistema che può visualizzare quasi tutto, sulla finestra root del desktop o in una propria finestra. Conky ha molti oggetti incorporati, oltre alla capacità di eseguire programmi o
 script esterni (sia esternamente sia attraverso l'uso interno di Lua). 
 
 Questo è un pacchetto fittizio per facilitare la transizione al nuovo schema di pacchettizzazione. Può essere rimosso senza problemi dopo l'aggiornamento/l'installazione.
Homepage: https://github.com/brndnmtthws/conky

oppure

$ apt show conky
Package: conky
Version: 1.12.2-1
Priority: optional
Section: universe/utils
Origin: Ubuntu
Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
Original-Maintainer: Vincent Cheng <vcheng@debian.org>
Bugs: https://bugs.launchpad.net/ubuntu/+filebug
Installed-Size: 28,7 kB
Depends: conky-std | conky-cli | conky-all
Homepage: https://github.com/brndnmtthws/conky
Download-Size: 3.244 B
APT-Sources: http://it.archive.ubuntu.com/ubuntu jammy/universe amd64 Packages
Description: monitor di sistema altamente configurabile (pacchetto di transizione)
 Conky è un monitor di sistema che può visualizzare quasi tutto, sulla
 finestra root del desktop o in una propria finestra. Conky ha molti
 oggetti incorporati, oltre alla capacità di eseguire programmi o script
 esterni (sia esternamente sia attraverso l'uso interno di Lua).
 .
 Questo è un pacchetto fittizio per facilitare la transizione al nuovo
 schema di pacchettizzazione. Può essere rimosso senza problemi dopo
 l'aggiornamento/l'installazione.

oppure ancora

$ snap info sweethome3d-homedesign 
name:      sweethome3d-homedesign
summary:   An interior design application to draw house plans & arrange furniture
publisher: Jean-Baptiste Lallement (jibel)
store-url: https://snapcraft.io/sweethome3d-homedesign
contact:   https://launchpad.net/~jibel
license:   GPL-2.0
description: |
  Sweet Home 3D is a free interior design application that helps you place
  your furniture on a house 2D plan, with a 3D preview.
  
  Available at http://www.sweethome3d.com/, this program is aimed at people
  who want to design their interior quickly, whether they are moving or they
  just want to redesign their existing home. Numerous visual guides help you
  draw the plan of your home and layout furniture. You may draw the walls of
  your rooms upon the image of an existing plan, and then, drag and drop
  furniture onto the plan from a catalog organized by categories. Each change
  in the 2D plan is simultaneously updated in the 3D view, to show you a
  realistic rendering of your layout.
snap-id: EPpzG3puHCgp1HI89uqVPTmAuKgbLhV1
channels:
  latest/stable:    6.6 2021-08-25 (15) 140MB -
  latest/candidate: 6.6 2021-08-25 (15) 140MB -
  latest/beta:      6.6 2021-08-25 (15) 140MB -
  latest/edge:      6.6 2021-08-25 (15) 140MB -

Che differenza c’è?

Con apt si accede al parco applicazioni ufficialmente distribuite da Canonical.

Con snap so accede ad altre applicazioni non facenti parte della distribuzione Ubuntu che in genere si scaricano con tutte le dipendenze necessarie. A volte queste applicazioni potrebbero non integrarsi alla perfezione con Ubuntu.

dpgk è il gestore di pacchetti di Debian di più basso livello, e apt/aptitude sono client che utilizzano dpkg per installare, aggiornare o rimuovere pacchetti. Nel man di dpkg infatti si legge:

dpkg is a medium-level tool to install, build, remove and manage Debian packages.  
The primary and more user-friendly front-end for dpkg 
as a CLI (command-line interface) is apt(8) and 
as a TUI (terminal user interface) is aptitude(8).  
dpkg itself is controlled entirely via command line parameters, which consist of exactly one action and zero or more options. The action-parameter tells dpkg what to do and options control the behavior of the action in some way.

Dunque

  • apt è la command-line interface (il comando come espresso dall’esempio sopra) di dpgk e
  • aptitude è la terminal user interface di dpkg, una interfaccia molto oldish che mi ricorda Norton Commander
ubuntu aptitude
ubuntu aptitude

I cari vecchi anni ’80…

Riferimenti

Accedere alla shell di un container Docker

Docker logo white
Docker logo white

Una volta avviato un container, come faccio ad accedere alla sua shell?

Prima di tutto occorre elencare i container:

$ sudo docker container ls -a
CONTAINER ID   IMAGE             COMMAND                   CREATED          STATUS                      PORTS                                       NAMES
42d1ab376b0d   sent_crunch       "/bin/sh -c \"/bin/ba…"   12 minutes ago   Exited (0) 12 minutes ago                                               mybeautifulproject-app-1
e18da64098a2   postgis/postgis   "docker-entrypoint.s…"    23 hours ago     Up 10 minutes (healthy)     0.0.0.0:5433->5432/tcp, :::5433->5432/tcp   mybeautifulproject-postgres_db-1

Qui nel mio caso l’unico container running (al quale ha senso quindi collegarsi) è il e18da64098a2. Quindi

sudo docker exec -it e18da64098a2 /bin/bash
postgres@e18da64098a2:/$ 

Fatto.

I file docker-compose

Docker logo white
Docker logo white

Lo sviluppo di applicazioni con l’utilizzo di Docker può diventare difficile quando si creano più container per erogare più servizi. Impariamo Docker Compose, lo strumento che ci aiuterà a eseguire ambienti applicativi multi-container.

In questo tutorial impareremo tutto su Docker Compose, i vantaggi dell’utilizzo di questo strumento, i suoi casi d’uso e le sue funzionalità.

Cos’è Docker Compose?

Un’applicazione può essere costituita da più contenitori che eseguono servizi diversi. Può essere complicato avviare e gestire i contenitori manualmente, quindi Docker ha creato uno strumento utile che aiuta ad accelerare il processo: Docker Compose.

Docker Compose è un software utilizzato per definire ed eseguire applicazioni Docker multi-container. Può gestire più container contemporaneamente nell’ambiente di produzione, staging, sviluppo, test e CI [Continuous Integration]. Pertanto, utilizzare Docker Compose per gestire l’intero ciclo di vita dello sviluppo del software [SDLC, software development life cycle].

Docker Compose funziona applicando le regole definite in un file docker-compose.yaml. Il file YAML configura i servizi dell’applicazione e include regole che specificano come si desidera che vengano eseguiti. Con questo file, puoi avviare, interrompere o ricostruire tutti i servizi utilizzando un unico comando. Inoltre, puoi controllare lo stato di un servizio, visualizzare gli output del registro ed eseguire comandi una tantum.

Docker Compose viene fornito in due “gusti”:

  • standalone: occorre installare un nuovo programma che è docker-compose, per questo di veda [2].
  • full package: se si è installato docker, sarà anche disponibile l’opzione compose per cui si usa il comando docker compose senza dover installare altro.

Casi d’uso di Docker Compose

I casi in cui risulta utile usare Docker Compose sono i seguenti:

  • Ambienti di test automatizzati. Compose supporta il test automatizzato, che è una parte essenziale di CI/CD in quanto può creare e distruggere facilmente l’ambiente di test richiesto. Gli sviluppatori possono definire e configurare l’ambiente necessario per l’esecuzione di test end-to-end automatizzati utilizzando il file Docker Compose appropriato.
  • Installazioni su singolo host. In Docker Compose, i container sono progettati per essere eseguiti su un singolo host poiché sono stati tradizionalmente focalizzati sullo sviluppo e sui flussi di lavoro di test.
  • Ambienti di sviluppo. Compose è un modo semplice e veloce per avviare progetti in quanto può creare rapidamente nuovi ambienti di sviluppo isolati. Il software documenta e configura tutte le dipendenze del servizio dell’applicazione (inclusi database, cache, API del servizio Web, ecc.). Consente di creare e avviare uno o più contenitori per ciascuna dipendenza utilizzando un unico comando.

I vantaggi di Docker Compose

Ecco alcuni dei principali vantaggi dell’utilizzo di Docker Compose:

  • Configurazione semplice e veloce. Grazie agli script YAML e alle variabili di ambiente, puoi configurare o modificare facilmente i servizi applicativi.
  • Comunicazione interna sicura. Compose crea una rete per tutti i servizi da condividere. Ciò aggiunge un ulteriore livello di sicurezza per l’app poiché non è possibile accedere ai servizi dall’esterno.
  • Portabilità e supporto CI/CD. Poiché tutti i servizi sono definiti all’interno del file docker-compose, gli sviluppatori possono facilmente accedere e condividere l’intera configurazione. Estraendo il file YAML e il codice sorgente, possono avviare l’ambiente in pochi minuti. Ciò contribuisce a creare e abilitare una pipeline CI/CD efficiente.
  • Uso efficiente delle risorse. Docker Compose consente di ospitare più ambienti isolati su un solo host. L’esecuzione di tutto su un singolo componente hardware ti consente di risparmiare molte risorse. Le sue caratteristiche che gli consentono di memorizzare una configurazione nella cache e di riutilizzare i contenitori esistenti, contribuiscono all’uso efficiente delle risorse.

Caratteristiche di Docker Compose

Esistono diverse importanti funzionalità di Docker Compose che offrono i vantaggi sopra menzionati.

Hosting di più ambienti isolati su un singolo host

Per impostazione predefinita, il nome del progetto è il nome di base della directory del progetto, mentre la directory del progetto è la directory di base del file docker-compose.yml. È possibile modificare i valori predefiniti:

  • Impostando il nome del progetto utilizzando l’opzione della riga di comando -p o la variabile di ambiente COMPOSE_PROJECT_NAME.
  • Impostando la directory del progetto usando la variabile di ambiente il parametro --project-directory da linea di comando.

È possibile utilizzare questa funzionalità su un host di sviluppo per eseguire copie stabili di ciascun ramo di funzionalità del progetto creando più copie dell’ambiente con nomi diversi.

Supporto delle variabili di ambiente

È possibile personalizzare i contenitori per ambienti o utenti diversi aggiungendo variabili di ambiente nel file docker-compose. Ciò offre maggiore flessibilità durante la configurazione dei contenitori con Compose, poiché i valori delle variabili non sono codificati nella configurazione.

I valori delle variabili possono essere impostati nell’ambiente della shell (da cui si esegue docker-compose) o in un file .env (memorizzato nella directory del progetto). Per impostazione predefinita, Docker Compose applica i valori specificati nel file .env. Tuttavia, i valori impostati nell’ambiente shell hanno la precedenza su quelli del file .env.

Ad esempio posso definire così il nome del progetto, in base a quanto visto poco fa: nella directory del progetto (quella in cui c’è il file docker-compose-yml) creo un file .env che contiene la linea:

COMPOSE_PROJECT_NAME=My Beautiful Project

Compose cerca il file automaticamente nella directory del progetto, oppure nelle variabili di ambiente impostati da shell.

Attenzione! I valori impostati nell’ambiente shell hanno la precedenza su quelli impostati nel file .env.

Se lancio la composizione del progetto, tra i log leggo:

$ sudo docker compose up
[+] Running 2/2
 ⠿ Container mybeautifulproject-postgres_db-1  Recreated                                                                                                                                                 0.2s
 ⠿ Container mybeautifulproject-app-1          Created    
.....      

Conservazione dei dati sul volume

Un’altra grande caratteristica di Docker Compose è che salva i dati utilizzati dai servizi. Pertanto, non devi preoccuparti di perdere i dati creati nei contenitori. Se sono presenti contenitori di esecuzioni precedenti, Compose li troverà e copierà i relativi volumi nella nuova esecuzione.

Riutilizzo dei contenitori esistenti

Compose ricrea solo i contenitori che sono stati modificati dall’ultima esecuzione. Se non ci sono modifiche, riutilizza il contenitore esistente.

Questa funzione si basa sulla capacità del software di memorizzare nella cache le configurazioni dei container, consentendoti di configurare i tuoi servizi più velocemente.

I comandi di base di Docker Compose

L’uso di Compose è fondamentalmente un processo in tre fasi:

  1. Definire l’ambiente della app con un Dockerfile in modo che possa essere riprodotto ovunque.
  2. Definire i servizi che compongono la app nel file docker-compose.yml in modo che possano essere eseguiti insieme in un ambiente isolato.
  3. Esegui docker compose up (docker <spazio> compose) e il comando Docker Compose si avvia ed esegue l’intera app.
    In alternativa, puoi eseguire docker-compose up (docker <trattino> compose) utilizzando Composer standalone (docker-compose eseguibile binario, attenzione al trattino!).

Un esenpio di file docker-compose.yml è il seguente:

version: "3.9"  # optional since v1.27.0
services:
  web:
    build: .
    ports:
      - "8000:5000"
    volumes:
      - .:/code
      - logvolume01:/var/log
    depends_on:
      - redis
  redis:
    image: redis
volumes:
  logvolume01: {}

Per ulteriori informazioni sulla sintassi del file docker-compose.yml, vedere il Compose file reference.

Compose dispone di comandi per la gestione dell’intero ciclo di vita della tua applicazione:

  • Avvio, arresto e ricostruzione dei servizi
  • Visualizzazione dello stato dei servizi in esecuzione
  • Logging dell’output del registro dei servizi in esecuzione
  • Esecuzione di comandi una tantum su un servizio

Una breve sintassi di base per eseguire i comandi Docker Compose è compendiata nel seguente elenco:

ComandoDescrizione
docker compose helpmostra la guida, le istruzioni per l’uso e gli argomenti per il comando docker-compose
docker compose buildcerca tutti i servizi che contengono l’istruzione build: nel file docker-compose.yml ed esegui un docker build per ognuno
docker compose run esegue un comando una tantum su un servizio
docker compose up compila, (ri)crea, avvia e collega ai contenitori per un servizio
docker compose -f [comando] specifica la posizione di un file di configurazione docker-compose aggiungendo il flag -f
docker compose startavvia un contenitore esistente perché svolga un serivizio
docker compose stoparresta i contenitori (senza rimuoverli)
docker compose pausemetti in pausa un servizio erogato da un contenitore
docker compose unpauseriavvia dalla pausa un servizio erogato da un contenitore
docker compose downarresta i contenitori (e rimuovi contenitori, reti, volumi e immagini)
docker compose pselenca i contenitori all’interno del file di configurazione di docker-compose
docker compose imageselenca le immagini su cui sono basati i contenitori
docker compose lselenca i progetti Compose in esecuzione
Tabella – elenco dei principali comandi Docker Compse

Altri articoli betaingegneria che parlano di Docker

Riferimenti esterni

  1. What is Docker Compose su Phoenixnap.com (by Sofija Simic)
  2. Installazione di docker-compose su Ubuntu 20.04 su Phoenixnap.com (by Sofija Simic)
  3. Documentazione ufficiale Docker

Docker task#1: montare un volume

Docker types of mounts: volume
Docker types of mounts: volume

Nell’applicazione che sto progettando e che farà utilizzo di Docker, ci sono anlcune directory host che voglio vengano viste dal container dell’applicaizone. Per fare questo in Docker si usa il concetto di volume.

C’è una directory che sarà destinata ad ospitare file prodotti dall’applicazione Python che andrà a costituire il mio primo volume. Per ora mi va bene che sia una directory isolata del mio host che monto nell’immagine che conterrà la mia applicazione dockerizzata. Più avanti mi ripropongo di montare la partizione di un NAS.

Il secondo volume che mi serve è quello che contiene il codice sorgente: voglio lavorare nell’host normalmente e fare in modo che il container Docker possa accedere al codice sorgente Python up to date. Voglio evitare la situazione in cui sviluppo nell’host e poi copio, ad ogni piccola modifica, i file dentro alla directory del contenitore docker.

Voglio in definitiva che il container veda il codice live.

Montaggio di una directory host come volume di dati Docker

Possiamo quindi montare una directory esistente dall’host a un container. Questi tipi di volumi sono chiamati Volumi Host.

È possibile montare i volumi host utilizzando il flag -v e specificando il nome della directory host.

Tutto ciò che si trova all’interno della directory host è quindi disponibile nel contenitore. Inoltre, tutti i dati generati all’interno del contenitore e inseriti nel volume di dati vengono archiviati in modo sicuro nella directory host e posso accedervi dall’OS dell’host con la shell o con Nautilus.

La sintassi di base per il montaggio di una directory host è:

$ docker run -v "$(pwd)":[volume_name] [docker_image]

L’attributo "$(pwd)" indica a Docker di montare la directory in cui si trova attualmente l’utente.

L’esempio seguente illustra come farlo.

1. Innanzitutto, crea una directory di esempio sull’host con il nome tmp e spostati in essa:

$ mkdir tmp && cd tmp

2. Una volta all’interno della directory, crea un file di test per vedere se sarà disponibile dal contenitore:

$ touch file.txt

3. Controllare se esiste l’immagine che vogliamo istanziare:

$ docker image ls
REPOSITORY    TAG       IMAGE ID       CREATED      SIZE
<none>        <none>    e7a5d4ae5d10   3 days ago   1.44GB
sent_crunch   latest    6dcb0bbb67c7   3 days ago   1.27GB

Nel mio caso l’immagine da usare è sent_crunch.

4. Quindi, usa il comando docker run per avviare un contenitore Ubuntu con la directory host collegata ad esso:

$ docker run -it -v "$(pwd)":/data1 [docker_image]

data1 è il nome del volume visto dal contenitore Docker. In definitiva, questo comando avvia il contenitore in modalità interattiva e monta un volume con il nome data1.

Nel mio esempio:

$ cd IdeaProjects/Python/SentCrunch
$ docker run -it -v "$(pwd)":/src sent_crunch
root@fbaaee970e3c:~# pwd
/app
root@fbaaee970e3c:~# ls
root@fbaaee970e3c:~# cd ..
root@fbaaee970e3c:/# ls
app  bin  boot	dev  etc  home	lib  lib32  lib64  libx32  media  mnt  opt  proc  root	run  sbin  src	srv  sys  tmp  usr  var
root@fbaaee970e3c:~# cd /src  <----- ho montato qui la directory sorgente
root@fbaaee970e3c:/src# ls -l
total 260
-rw-rw-r-- 1 root root   1626 Aug 12 13:52 Dockerfile
-rwxrwxr-x 1 root root  10758 Feb 17 15:23 GPF_Smart_Agri-sat_Cruncher.xml
-rw-rw-r-- 1 root root    359 Dec 24  2021 Logger.py
-rw-rw-r-- 1 root root   2523 Oct 12  2021 Resample.xml
-rw-rw-r-- 1 root root    542 Nov 22  2021 SentinelCruncher.iml
-rw-rw-r-- 1 root root 104523 Jul  8 15:02 SentinelCruncher.py
-rw-rw-r-- 1 root root      0 Feb  1  2022 __init__.py
drwxrwxr-x 2 root root   4096 Jul  8 15:21 __pycache__
drwxrwxr-x 2 root root   4096 Jul 11 09:19 archexport
-rw-rw-r-- 1 root root  10396 May 10 09:50 changelog.txt
-rw-rw-r-- 1 root root   1536 May  9 09:22 constant.local.py
-rw-rw-r-- 1 root root   1563 Jul  8 15:14 constant.py
-rw-rw-r-- 1 root root   1926 May  9 09:18 constant.stage.py
-rw-rw-r-- 1 root root    665 Nov  9  2021 country.xml
-rw-rw-r-- 1 root root   1086 Aug  9 13:08 docker-compose.yml
-rw-rw-r-- 1 root root    851 Jul 27 06:14 docker_compose.yml
-rw-rw-r-- 1 root root      0 Jul  8 10:21 exportFile.dmp
-rw-rw-r-- 1 root root  16799 Oct  7  2021 install_pg.log
drwxrwxr-x 3 root root   4096 Nov 18  2021 latex
-rw-rw-r-- 1 root root  34498 Feb 25 16:02 libcruncher.py
-rw-rw-r-- 1 root root      0 Nov 18  2021 math
-rw-rw-r-- 1 root root    132 Jul 11 08:37 shp2psql.cmd
drwxrwxr-x 3 root root   4096 Jul 25 14:58 sql
drwxrwxr-x 2 root root   4096 Jul 11 16:34 test
-rw-rw-r-- 1 root root  10785 Jan 28  2022 xmlOut.xml

root@fbaaee970e3c:/src# cd test
root@fbaaee970e3c:/src/test# ./test001_dbconnect.py 
Ciao Docker!

Riferimenti

Compilare una nuova immagine Docker: il Dockerfile

docker - Dockerfile
docker – Dockerfile

Se non hai ancora letto questo articolo, ti consiglio vivamente di farlo perché la sua comprensione precede lo studio del presente articolo.

Introduzione

Un Dockerfile è uno script con le istruzioni su come creare un’immagine Docker. Queste istruzioni sono, infatti, un gruppo di comandi eseguiti automaticamente in ambiente Docker per costruire un’immagine Docker specifica.

In questo tutorial, scopri come creare un’immagine Docker con un Dockerfile.

Salto la parte relativa all’installazione di Docker già trattata altrove nel blog.

Creare un Dockerfile

La prima cosa che devi fare è creare una directory in cui puoi archiviare tutte le immagini Docker che crei.

Ad esempio, creeremo una directory denominata ~/docker/images , e un file Dockerfile al suo interno con il comando:

$ mkdir -p ~/docker/images && cd ~/docker/images && touch Dockerfile

Ora occorre editare il Dockerfile e questo apre un grande capitolo sulla sintassi dei Dockerfile: i Dockerfile sono degli script che servono ad allestire l’immagine (che programmi usare? ad esempio Python, PostgreSQL… che librerie/dipendenze mi servono? ad ese GDAL, … che software devo fare girare? ad esempio quello che ho sviluppato io che dovrà trovarsi nella cartella /app/ del container). Un semplice esempio tratto dal blog di Sofija è questo che dà un primo esempio di template che delinea alcune funzionalità (mutatis mutandis):

FROM ubuntu  # <--- 1

MAINTAINER Marco Barbato <marco@betaingegneria.it>  # <--- 2

RUN apt-get update  # <--- 3

CMD ["echo", "Ciao, Mondo!"]  # <--- 4
  1. FROM: Definisce le fondamenta dell’immagine che stai creando. Puoi iniziare da un’immagine padre (come nell’esempio sopra) o da un’immagine base.
    Quando si utilizza un’immagine padre, si utilizza un’immagine esistente su cui ne basi una nuova.
    Usare un’immagine base significa invece che stai partendo da zero (che è esattamente come la definiresti: FROM scratch).
  2. MANTAINER: Specifica l’autore dell’immagine. Qui puoi digitare il tuo nome e/o cognome (o anche aggiungere un indirizzo email). Puoi anche utilizzare l’istruzione LABEL per aggiungere metadati a un’immagine.
  3. RUN: istruzioni per eseguire un comando mentre si costruisce un’immagine in un livello sopra di essa. In questo esempio, il sistema cerca gli aggiornamenti del repository una volta iniziata la creazione dell’immagine Docker. Puoi avere più di un’istruzione RUN in un Dockerfile.
  4. CMD: Può esserci solo un’istruzione CMD all’interno di un Dockerfile. Il suo scopo è fornire i valori predefiniti per un container in esecuzione. Con esso, imposti un comando predefinito. Il sistema lo eseguirà se si esegue un container senza specificare un comando.

Creare una immagine da un Dockerfile

La sintassi generale per costruire (build) una immagine a partire da un file Dockerfile è questa:

$ docker build [OPTIONS] PATH | URL | -

Pertanto fondamentalmente dovrai lanciare questo comando

$ docker build /path/to/dockerfile

Se sei già nella direcotry del Dockerfile, semplicemente scriverai:

$ docker build .

Siccome ogni immagine che viene creata viene battezzata con uno stringone esadecimale, che è poco mnemonico, si può associare un tag all’immagine per aiutarsi ad organizzare le immagini:

$ docker build -t my_image .
Sending build context to Docker daemon  2.048kB
Step 1/4 : FROM ubuntu
 ---> 27941809078c
Step 2/4 : MAINTAINER marco Barbato <marco@betaingegneria.it>
 ---> Running in 7cd4f42b5e84
Removing intermediate container 7cd4f42b5e84
 ---> d0377afa0d1b
Step 3/4 : RUN apt-get update
 ---> Running in b223682b3e0f
Get:1 http://security.ubuntu.com/ubuntu jammy-security InRelease [110 kB]
Get:2 http://archive.ubuntu.com/ubuntu jammy InRelease [270 kB]
Get:3 http://security.ubuntu.com/ubuntu jammy-security/restricted amd64 Packages [313 kB]
Get:4 http://security.ubuntu.com/ubuntu jammy-security/main amd64 Packages [332 kB]
Get:5 http://security.ubuntu.com/ubuntu jammy-security/multiverse amd64 Packages [4644 B]
Get:6 http://security.ubuntu.com/ubuntu jammy-security/universe amd64 Packages [131 kB]
Get:7 http://archive.ubuntu.com/ubuntu jammy-updates InRelease [114 kB]
Get:8 http://archive.ubuntu.com/ubuntu jammy-backports InRelease [99.8 kB]
Get:9 http://archive.ubuntu.com/ubuntu jammy/restricted amd64 Packages [164 kB]
Get:10 http://archive.ubuntu.com/ubuntu jammy/universe amd64 Packages [17.5 MB]
Get:11 http://archive.ubuntu.com/ubuntu jammy/multiverse amd64 Packages [266 kB]
Get:12 http://archive.ubuntu.com/ubuntu jammy/main amd64 Packages [1792 kB]
Get:13 http://archive.ubuntu.com/ubuntu jammy-updates/universe amd64 Packages [258 kB]
Get:14 http://archive.ubuntu.com/ubuntu jammy-updates/multiverse amd64 Packages [7791 B]
Get:15 http://archive.ubuntu.com/ubuntu jammy-updates/restricted amd64 Packages [354 kB]
Get:16 http://archive.ubuntu.com/ubuntu jammy-updates/main amd64 Packages [631 kB]
Get:17 http://archive.ubuntu.com/ubuntu jammy-backports/universe amd64 Packages [5814 B]
Fetched 22.3 MB in 6s (4013 kB/s)
Reading package lists...
Removing intermediate container b223682b3e0f
 ---> 719d25fccd26
Step 4/4 : CMD ["echo", "Ciao, Mondo!"]
 ---> Running in aa4e9ccb74e0
Removing intermediate container aa4e9ccb74e0
 ---> ade2f7ecc592
Successfully built ade2f7ecc592
Successfully tagged my_image:latest

Ora si possono vedere tutte le immagini create con il comando docker image ls (oppure docker images):

$ docker image ls
REPOSITORY    TAG       IMAGE ID       CREATED      SIZE
<none>        <none>    e7a5d4ae5d10   4 days ago   1.44GB
sent_crunch   latest    6dcb0bbb67c7   4 days ago   1.27GB

Creare un container da una immagine

Il passo successivo è avviare un nuovo contenitore Docker in base all’immagine che hai creato nei passaggi precedenti. Chiameremo il contenitore “test” e lo creeremo con il comando:

$ sudo docker run --name test my_image
Ciao, Mondo!

Il messaggio Ciao, Mondo! dovrebbe apparire come output nella shell, come effetto dell’ultimo comando del Dockerfile:

CMD ["echo", "Ciao, Mondo!"]

Conclusione

L’utilizzo di Dockerfile è il modo più semplice e veloce per creare un’immagine Docker. Automatizza il processo passando attraverso lo script con tutti i comandi per assemblare un’immagine.

Quando crei un’immagine Docker, assicurati anche di mantenere le dimensioni dell’immagine Docker contenute. Evitare immagini di grandi dimensioni velocizza la creazione e la distribuzione dei container. Pertanto, è fondamentale ridurre al minimo le dimensioni dell’immagine.

Riferimenti

Docker: terminologia base

Partire con Docker non è difficile ma mi sono trovato subito a fare confusione tra questi tre termini: image, container, volume. Provo a spiegarmeli con l’aiuto di questi articoli veramente chiari di Sofija Simic che trovate linkati alla fine del post e che rimastico quà e là un po’ a modo mio.

Differenza tra image e container

Cos’è un’immagine Docker?

Un’immagine Docker (image) è un file immutabile (non modificabile) che contiene il codice sorgente, le librerie, le dipendenze, gli strumenti e altri file necessari per l’esecuzione di un’applicazione.

A causa della loro qualità di sola lettura, queste immagini sono talvolta denominate istantanee (snapshots). Rappresentano un’applicazione e il suo ambiente virtuale in un momento specifico. Questa coerenza è una delle grandi caratteristiche di Docker. Consente agli sviluppatori di testare e sperimentare software in condizioni stabili e uniformi.

Poiché le immagini sono, in un certo senso, solo modelli (templates), non è possibile avviarle o eseguirle. Quello che puoi fare è usare quel modello come base per costruire un contenitore.

Un contenitore è, in definitiva, solo un’immagine in esecuzione.

Una volta creato un contenitore, viene aggiunto un livello scrivibile sopra l’immagine immutabile, il che significa che ora puoi modificarlo.

L’immagine di base dalla quale crei un contenitore esiste separatamente e non può essere modificata. Quando si esegue un ambiente containerizzato, si crea essenzialmente una copia di lettura e scrittura di quel filesystem (immagine docker) all’interno del container. Ciò aggiunge un livello contenitore che consente le modifiche dell’intera copia dell’immagine.

Docker: container e uno stack di images
Docker: container e uno stack di images

Puoi creare un numero illimitato di immagini Docker da un’unica immagine di base. Ogni volta che modifichi lo stato iniziale di un’immagine e salvi lo stato esistente, crei un nuovo modello con un livello aggiuntivo sopra di esso, in una struttura a pila (stack).

Le immagini Docker possono, quindi, essere costituite da una serie di livelli, ciascuno diverso ma anche originato dal precedente. I livelli immagine rappresentano file di sola lettura a cui viene aggiunto un livello contenitore una volta utilizzati per avviare un ambiente virtuale.

Cos’è un container Docker?

Un container Docker è un ambiente di runtime virtualizzato in cui gli utenti possono isolare le applicazioni dal sistema sottostante. Questi contenitori sono unità compatte e portatili in cui è possibile avviare un’applicazione in modo rapido e semplice.

Una caratteristica preziosa è la standardizzazione dell’ambiente di calcolo in esecuzione all’interno del container. Non solo garantisce che la tua applicazione funzioni in circostanze identiche, ma semplifica anche la condivisione con altri collaboratori.

Poiché i container sono autonomi, essi forniscono un forte isolamento, assicurando che non interrompano altri container in esecuzione, nonché il server che li supporta. Docker afferma che queste unità “forniscono le più potenti capacità di isolamento del settore”. Pertanto, non dovrai preoccuparti di proteggere la tua macchina durante lo sviluppo di un’applicazione.

A differenza delle macchine virtuali (VM) in cui la virtualizzazione avviene a livello di hardware, i container vengono virtualizzati a livello di app. Possono utilizzare una macchina, condividere il suo kernel e virtualizzare il sistema operativo per eseguire processi isolati. Ciò rende i contenitori estremamente leggeri, consentendo di trattenere risorse preziose.

Differenza tra macchina virtuale (VM) e Container Docker
Differenza tra macchina virtuale (VM) e Container Docker

Contenitori contro Immagini

Quando si parla della differenza tra immagini e contenitori, un errore che capita di fare è quello di metterli in contrasto come entità antagoniste. Entrambi gli elementi sono invece strettamente correlati e fanno parte di un sistema definito dalla piattaforma Docker.

Se hai letto le due sezioni precedenti che definiscono le immagini e i contenitori Docker, potresti già avere una certa comprensione di come i due stabiliscono una relazione.

Le immagini possono esistere senza contenitori, mentre un contenitore deve eseguire un’immagine per esistere.

Pertanto, i contenitori dipendono dalle immagini e le utilizzano per creare un ambiente di runtime ed eseguire un’applicazione.

I due concetti esistono come componenti essenziali (o meglio come fasi) nel processo di esecuzione di un container Docker. Avere un contenitore in esecuzione è la “fase” finale di quel processo, a indicare che dipende dai passaggi e dai componenti precedenti. Ecco perché le immagini Docker essenzialmente governano e modellano i contenitori.

Dal Dockerfile all’image al container

Tutto inizia con uno script di istruzioni che definiscono come creare un’immagine Docker specifica. Questo script è chiamato Dockerfile. Eseguendo un Dockerfile, vengono eseguiti singolarmente i comandi elencati e viene creata un’immagine Docker.

Il comando per creare un’immagine da un Dockerfile è docker build.

L’immagine viene quindi utilizzata come modello (o base), che uno sviluppatore può copiare e utilizzarla per eseguire un’applicazione. L’applicazione necessita di un ambiente isolato in cui eseguire: un container.

Questo ambiente non è solo uno “spazio” virtuale. Si basa interamente sull’immagine che lo ha creato. Il codice sorgente, i file, le dipendenze e le librerie binarie, che si trovano tutti nell’immagine Docker, costituiscono un contenitore.

Per creare un contenitore da un’immagine, usa il comando docker create.

Infine, dopo aver avviato un contenitore da un’immagine esistente, ne avvii il servizio ed esegui l’applicazione.

Riutilizzo di un contenitore come immagine

Se apporti modifiche all’immagine iniziale e desideri conservarla per lavori futuri, puoi salvare l’immagine modificata facendo uno screenshot dello stato corrente del contenitore. In questo modo, alleghi un livello contenitore sopra l’immagine, costruendo infine una nuova immagine immutabile. Di conseguenza, ti ritroverai con due immagini Docker derivate dallo stesso filesystem.

Docker volumes: creazione e utilizzo

I volumi Docker sono strumenti ampiamente utilizzati e utili per garantire la persistenza dei dati mentre si lavora nei container. Sono un’alternativa migliore rispetto alla compilazione di livelli scrivibili aggiuntivi, che aumentano le dimensioni dell’immagine Docker.

In pratica si potrebbero scrivere i contenuti dentro ai container ma così facendo se distruggiamo il container perdiamo anche i dati.

Cosa sono i volumi Docker?

I volumi Docker sono file systems montati su container Docker per preservare i dati generati dal container in esecuzione.

È come se montassimo un hard disk USB nel nostro filesystem per utilizzare i dati anche a computer spento.

I volumi vengono archiviati nell’host, indipendentemente dal ciclo di vita del contenitore. Ciò consente agli utenti di eseguire facilmente il backup dei dati e condividere file system tra contenitori.

Primi passi con i volumi

Esistono diversi modi per montare un volume Docker durante l’avvio di un container. Si possono utilizzare in alternativa i flag -v e --mount, che vengono aggiunti al comando docker run.

Questo articolo mostra esempi di entrambi i flag in uso.

Funzioni di manutenzione dei volumi

Creare un volume Docker

Per creare un volume Docker utilizzare il comando:

$ docker volume create [volume_name]

In conseguenza a questo comando, Docker crea una directory per il volume sull’host nel percorso /var/lib/docker/volume/. È ora possibile montare questo volume su un container, garantendo la persistenza dei dati e la condivisione dei dati tra più container.

Ad esempio, per creare un volume con il nome data, eseguire il comando:

$ docker volume create data

Elenco dei volumi

Per verificare di aver creato correttamente un volume Docker, chiedi a Docker di elencare tutti i volumi disponibili con:

$ docker volume list

Nelle prove fatte finora ho creato un po’ di volumi e questo è il risultato

# docker volume ls
DRIVER    VOLUME NAME
local     0effd02df709acd840c431ad7ffa8c24f19107b0fc534cd86b47b2d7a2d38ded
local     83eac56a552efee4167050921b771395da0100f94df95b72bee7fdbfb95c9ff0
local     35409dbb5911bf3ad9454375a7d4c855f5fe46f7800acc3a0e8b204417b274f0
local     b6c20a389cc440e77208aa62c48792f3c3d381253ba200fbb7779048eb729d72

L’output visualizza un elenco di volumi, specificando la loro posizione (DRIVER) e il loro nome VOLUME NAME.

Si può vedere la stessa situazione dal file system dell’host, a parttire dalla posizione in cui Docker crea i volumi:

$ ls -l /var/lib/docker/volumes
totale 48
drwx-----x  6 root root  4096 ago  9 10:17 ./
drwx--x--- 13 root root  4096 ago  9 10:17 ../
drwx-----x  3 root root  4096 giu 29 17:25 0effd02df709acd840c431ad7ffa8c24f19107b0fc534cd86b47b2d7a2d38ded/
drwx-----x  3 root root  4096 giu 29 12:25 35409dbb5911bf3ad9454375a7d4c855f5fe46f7800acc3a0e8b204417b274f0/
drwx-----x  3 root root  4096 giu 29 12:12 83eac56a552efee4167050921b771395da0100f94df95b72bee7fdbfb95c9ff0/
drwx-----x  3 root root  4096 giu 29 17:08 b6c20a389cc440e77208aa62c48792f3c3d381253ba200fbb7779048eb729d72/
brw-------  1 root root  8, 1 ago  9 10:17 backingFsBlockDev
-rw-------  1 root root 32768 ago  9 10:17 metadata.db

Ispezionare i volumi

Per visualizzare ulteriori informazioni su un volume Docker, utilizzare il comando inspect:

# docker volume inspect b6c20a389cc440e77208aa62c48792f3c3d381253ba200fbb7779048eb729d72
[
    {
        "CreatedAt": "2022-06-29T17:13:42+02:00",
        "Driver": "local",
        "Labels": null,
        "Mountpoint": "/var/lib/docker/volumes/b6c20a389cc440e77208aa62c48792f3c3d381253ba200fbb7779048eb729d72/_data",
        "Name": "b6c20a389cc440e77208aa62c48792f3c3d381253ba200fbb7779048eb729d72",
        "Options": null,
        "Scope": "local"
    }
]

Il comando elenca i dettagli di un volume, inclusa la sua posizione nel filesystem host (Mountpoint). Tutto ciò che è memorizzato all’interno del volume di dati può essere trovato anche nella directory elencata sotto il percorso del punto di montaggio.

Montare un volume in un container

Come quando si monta un disco USB collegandolo ad una porta del computer, per montare un volume di dati in un contenitore, aggiungi il flag --mount al comando docker run. Docker aggiunge il volume al contenitore specificato, dove archivia i dati prodotti all’interno dell’ambiente virtuale.

Per eseguire un container e montarvi un volume di dati, segui la sintassi di base:

$ docker run --mount source=[volume_name],destination=[path_in_container] [docker_image]

Basta sostituire [path_in_container] con il percorso in cui desideri posizionare il volume di dati nel contenitore (ad esempio /app/data). Tutto ciò che è memorizzato in quella directory viene automaticamente salvato anche sul volume di dati sull’host.

Ad esempio, per avviare un contenitore Ubuntu e montarvi il volume di dati, eseguire:

$ docker run -it --name=example1 --mount source=data,destination=/app/data myimage

Il comando indica a Docker di eseguire (run) un contenitore in modalità interattiva (-it, in conseguneza a questa opzione si aprirà una shell sul contenitore) dall’immagine myimage, con il nome example1, mentre monta i dati del volume nella directory /app/data all’interno del contenitore.

Quindi, controlla per verificare che il volume sia stato montato correttamente elencando il contenuto del contenitore con il comando ls dalla console del contenitore.

Riferimenti

Network pills: traceroute

network traceroute
network traceroute

traceroute (per gli utenti Windows il programma si chiama tracert) è un programma di utilità per il network che troviamo nei sistemi operativi Linux e Mac che permette di seguire il percorso di un pacchetto di dati dal nostro computer ad un qualsiasi altro host per evidenziare criticità inm questa trasmissione.

Il percorso di un un pacchetto in Internet (ma anche in una rete chiusa, come ad esempio un’azienda) è un percorso fatto di tanti punti discreti in cui il primo punto (origine) è il PC che invia il pacchetto e l’ultimo punto (destinazione) è l’host che vogliamo raggiungere. Per andare dal’origine alla destinazione, il pacchetto attraversa (ogni device attraversato è un “hop” ovvero un salto) un certo numero di dispositivi che hanno il compito di fare avanzare il pacchetto: sono i router. Ogni router, quando riceve il pacchetto dal suo predecessore, guarda nell’intestazione TCP/IP qual è l’indirizzo di destinazione e decide in base ad un algoritmo di percorso minimo (shortest path) a quale router inviarlo per farlo progredire nel suo cammino.

Ogni hop comporta un tempo che va a sommarsi al tempo totale che il pacchetto impiegherà.

Inoltre, per evitare che un pacchetto giri all’infinito senza raggiungere la destinazione, il protocollo IP prevede di dargli un numero massimo di chance prima di essere scartato: è il “tempo di vita” (TTL Time To Live) che è un numero intero che viene diminuito di 1 ad ogni hop. Ogni device che prende in carico il pacchetto, prima di inoltrarlo al prossimo device prende il valore di TTL nell’intestazione TCP e lo diminuisce di 1:

TTL = TTL -1

Traceroute ci aiuta a diagnosticare eventuali problemi di rete nel raggiungere un certo host analizzando il percorso di un singolo pacchetto ICMP.

Un altro elemento chiave da comprendere è il “tempo di andata e ritorno” (RTT, Round Trip Time). Traceroute assicura che ogni hop sulla strada per un dispositivo di destinazione rilasci un pacchetto e restituisca un messaggio di errore ICMP. Ciò significa che traceroute può misurare la durata del tempo tra l’invio dei dati e la ricezione del messaggio ICMP per ogni hop, fornendo il valore RTT per ogni hop.

Ma come fa traceroute a ritornare un risultato ad ogni hop?

Prima cosa: traceroute fa affidamento sulla capacità di routing di ogni singolo nodo che separa l’origine dalla destinazione e, in realtà, lui non invia un solo pacchetto ma ne invia molti, almeno quanto è il numero massimo di hop previsto. Il funzionamento è questo

  1. traceroute invia un pacchetto ICMP verso la destinazione finale con TTL = 1.
  2. Ciò significa che appena il pacchetto ha raggiunto il primo router sarà già morto e il router informa di questo traceroute che mette via l’indirizzo IP del router raggiunto assieme al RTT e lo stampa.
  3. traceroute invia quindi un nuovo pacchetto ICMP verso la destinazione e gli assegna un TTL=2; il pacchetto raggiungerà il router 1 (che potrebbe anche essere diverso da quello di prima, ma non importa); questi a sua volta lo consegna al router 2 e qui il pacchetto muore. Quindi router 2 invia indietro un pacchetto ICMP all’origine con l’informazione dell’esaurimento del TTL e quindi traceroute pusha nella coda l’indirizzo IP del router2 insieme al RTT.

E così via. Quindi traceroute invia fino a 30 pacchetti, ogni singolo invio è detto probe, (a meno che non gli indichiamo noi il numero di pacchetti massimo con l’opzione -m max_ttl o --max-hops=max_ttl) e per ognuno registra l’indirizzo raggiunto e il tempo di andata e ritorno.

Leggere l’output di traceroute

Un esempio è il seguente

root@jsbach:/home/marcob# traceroute -m 30 www.google.com
traceroute to www.google.com (142.250.180.164), 30 hops max, 60 byte packets
 1  _gateway (192.168.83.125)  2.392 ms  2.375 ms  2.335 ms
 2  10.8.0.1 (10.8.0.1)  45.510 ms  38.503 ms  38.480 ms
 3  192.168.251.30 (192.168.251.30)  53.828 ms  54.004 ms  53.981 ms
 4  192.168.255.21 (192.168.255.21)  41.459 ms  53.936 ms  42.337 ms
 5  194.149.188.16 (194.149.188.16)  53.890 ms  53.868 ms  53.845 ms
 6  * 194.149.187.14 (194.149.187.14)  51.268 ms  51.217 ms
 7  * * partner-network-device.nflxvideo.net (66.197.165.233)  44.639 ms
 8  72.14.220.82 (72.14.220.82)  58.851 ms  55.300 ms  61.741 ms
 9  * * *
10  142.251.50.134 (142.251.50.134)  77.915 ms 142.251.50.138 (142.251.50.138)  76.934 ms 108.170.232.168 (108.170.232.168)  77.465 ms
11  142.250.211.23 (142.250.211.23)  77.647 ms mil04s44-in-f4.1e100.net (142.250.180.164)  77.586 ms  77.523 ms
root@jsbach:/home/marcob# 

Il comando stampa una intestazione con scritto il nome host (con il suo indirizzo IP che si è fatto dare dal DNS locale o dal file /etc/hosts), il numero massimo di probe che invierà e la dimensione del pacchetto (60 bytes).

Segue la descrizione di ogni probe (ogni probe si avanza di un hop con il meccanismo di aumento di 1 del TTL per ogni probe). Ogni riga ha questa struttura:

#hop | nome host (se esiste, altrimenti l'indirizzo IP) | (indirizzo IP) | RTT1 | RTT2 | RTT3 |

Hop 1: _gateway è il device che mi connette ad internet, in questo momento il mio cellulare utilizzato come hotspot; il dhcp mi ha dato come indirizzo 192.168.83.118, _gateway ha l’indirizzo 192.168.83.125 (sono chiaramete sulla stessa sottorete 192.168.83). Seguono poi tre misure temporali (2.392 ms 2.375 ms 2.335 ms) che sono rispettivamente tre round tript time per tre distinti pacchetti (quindi per ogni probe in realtà traceroute invia 3 pacchetti e non uno soltanto): questo per mostrare la coerenza, o una sua mancanza, nel percorso.

Hop 2: 10.8.0.1 è il primo router incontrato dal pacchetto

e così via.

Cosa sono gli asterischi?

In realtà, se leggiamo bene, questi asterischi sono scritti nella colonna sbagliata, dovrebbero essere scritti così:

 1  _gateway (192.168.83.125)  2.392 ms  2.375 ms  2.335 ms
 2  10.8.0.1 (10.8.0.1)  45.510 ms  38.503 ms  38.480 ms
 3  192.168.251.30 (192.168.251.30)  53.828 ms  54.004 ms  53.981 ms
 4  192.168.255.21 (192.168.255.21)  41.459 ms  53.936 ms  42.337 ms
 5  194.149.188.16 (194.149.188.16)  53.890 ms  53.868 ms  53.845 ms
 6  194.149.187.14 (194.149.187.14)  *          51.268 ms  51.217 ms
 7  partner-network-device.nflxvideo.net (66.197.165.233)  * * 44.639 ms
 8  72.14.220.82 (72.14.220.82)  58.851 ms  55.300 ms  61.741 ms
 9  * * *
10  142.251.50.134 (142.251.50.134)  77.915 ms 
    142.251.50.138 (142.251.50.138)  76.934 ms 
    108.170.232.168 (108.170.232.168)  77.465 ms
11  142.250.211.23 (142.250.211.23)  77.647 ms 
    mil04s44-in-f4.1e100.net (142.250.180.164)  77.586 ms  77.523 ms

cioè sono dati che vanno scritti nelle colonne dei RTT: sono pacchetti che si sono persi; a volte si perdono per un singolo invio (hop 6), a volte in due (hop 7) a volte in tutti e tre i probe (come si vede all’hop 9).

Questo a significare che statisticamente non tutti i router riusciranno a mandarci indietro il pacchetto e perderemo il tempo di transito di quell’hop.

Analisi dei risultati

Prima di addentrarci nell’analisi occorre rscrivere l’outut in un modo più ordinato:

root@jsbach:/home/marcob# traceroute -m 30 www.google.com
traceroute to www.google.com (142.250.180.164), 30 hops max, 60 byte packets
 1  _gateway (192.168.83.125)  				   2.392 ms   2.375 ms   2.335 ms
 2  10.8.0.1 (10.8.0.1)  45.510 ms  		  38.503 ms  38.480 ms
 3  192.168.251.30 (192.168.251.30)  		  53.828 ms  54.004 ms  53.981 ms
 4  192.168.255.21 (192.168.255.21)  		  41.459 ms  53.936 ms  42.337 ms
 5  194.149.188.16 (194.149.188.16)  		  53.890 ms  53.868 ms  53.845 ms
 6  194.149.187.14 (194.149.187.14)               *          51.268 ms  51.217 ms
 7  partner-network-de... (66.197.165.233)        *          *          44.639 ms
 8  72.14.220.82 (72.14.220.82)                   58.851 ms  55.300 ms  61.741 ms
 9  						  * 	     *          *
10  142.251.50.134 (142.251.50.134)  		  77.915 ms 
    142.251.50.138 (142.251.50.138)  		  76.934 ms 
    108.170.232.168 (108.170.232.168)  	          77.465 ms
11  142.250.211.23 (142.250.211.23)  		  77.647 ms 
    mil04s44-in-f4.1e10  (142.250.180.164)        77.586 ms  77.523 ms
root@jsbach:/home/marcob# 

In generale i tempi di latenza (i delta più grandi) si possono localizzare all’inizio, in mezzo o alla fine del percorso. Possono esserci problemi soltanto nei casi in cui la latenza riguarda l’inzio del percorso (problemi del gateway) oppure all’ultimo passaggio, che può essere sintomo di diversi problemi, per esempio uno può essere quello della presenza di un firewall in entrata (della destinazione) che rende difficile la consegna dei pacchetti all’ultimo step.

Riferimenti

Git, istruzioni di soccorso

Vi segnalo (e ricordo a me stesso) questo utilissimo sito web che descrive scenari molto comuni, generatori di ansia quando si lavora con Git.

Un guida scritta in stile what if? che potrà aiutarmi (-vi) di scuro in diverse occasioni. Il nome è molto esplicativo: Oh Shit, Git??

https://ohshitgit.com/