Docker: differenze tra le versioni

Da Webmobili Wiki.
 
(71 versioni intermedie di 2 utenti non mostrate)
Riga 9: Riga 9:


Inoltre, i container sono leggeri perché sfruttano i servizi offerti dal kernel del sistema operativo ospitante, invece di richiedere l’esecuzione di un kernel virtualizzato come avviene nel caso delle VM. Ciò permette di ospitare un gran numero di container nella stessa macchina fisica.
Inoltre, i container sono leggeri perché sfruttano i servizi offerti dal kernel del sistema operativo ospitante, invece di richiedere l’esecuzione di un kernel virtualizzato come avviene nel caso delle VM. Ciò permette di ospitare un gran numero di container nella stessa macchina fisica.
Data la sua stretta relazione con il kernel Linux, Docker è nativamente disponibile in ambiente GNU/Linux. Tuttavia, essendo uno strumento concepito per gli sviluppatori, si è resa necessaria la possibilità di eseguirlo direttamente sulle workstation, senza imporre limiti sul sistema operativo in uso. Docker è pertanto disponibile anche in ambiente macOS e Windows. In entrambi i casi, si ricorre ad una tecnologia di virtualizzazione per eseguire un kernel Linux al quale si appoggia l’engine Docker.
=== Funzionamento ===
Il modello di esecuzione basato su container si fonda su una serie di funzionalità sviluppate nel kernel Linux e successivamente adottate anche da altre piattaforme, espressamente studiate per questo scopo.
Un sistema basato su container inteso come alternativa alla virtualizzazione deve garantire prestazioni superiori pur offrendo le stesse caratteristiche in termini di flessibilità nella gestione delle risorse e di sicurezza.
L’incremento prestazionale è dovuto essenzialmente all’eliminazione di uno strato: a differenza di quanto avviene quando un processo viene eseguito all’interno di una macchina virtuale, i processi eseguiti da un container sono di fatto eseguiti dal sistema ospitante, usufruendo dei servizi offerti dal kernel che esso esegue. Viene quindi eliminato l’overhead dovuto all’esecuzione di ogni singolo kernel di ogni VM.
I requisiti di flessibilità e sicurezza in un sistema virtualizzato sono a carico dell’Hypervisor, lo strato software in esecuzione nella macchina ospitante che si occupa di gestire le risorse allocate a ciascuna VM e che adotta (anche con l’ausilio dell’hardware) tutte le politiche necessarie per isolare i processi in esecuzione su VM differenti.
In un ambiente basato su container, dove quindi non è presente un Hypervisor, queste funzionalità sono assolte dal kernel del sistema operativo ospitante. Linux dispone di due caratteristiche progettate proprio per questo scopo: <b>Control Groups (o cgroups)</b> e <b>Namespaces</b>.
==== I control groups (cgroups) ====
I control groups sono lo strumento utilizzato dal kernel Linux per gestire l’utilizzo delle risorse di calcolo da parte di un gruppo specifico di processi. Grazie ai cgroups è possibile limitare la quantità di risorse utilizzate da uno o più processi. Ad esempio, è possibile limitare il quantitativo massimo di memoria RAM che un gruppo di processi può utilizzare.
In un sistema Linux esistono più cgroups, ciascuno associato ad uno o più resource controllers (talvolta chiamati anche subsystems), in base alle risorse che gestiscono. Ad esempio, un cgroup può essere associato al resource controller memory per gestire la quantità di memoria allocabile da un certo insieme di processi.
I cgroup sono organizzati in modo gerarchico in una struttura ad albero. Ogni nodo dell’albero rappresenta una gruppo, definito dalle regole che gestiscono la risorsa a cui è associato (ad esempio la dimensione massima di memoria allocabile) e dalla lista dei processi che ne fanno parte. Ciascun processo in quella lista risponderà alle regole definite nel gruppo.
Si può interagire manualmente con i cgroup attraverso il filesystem virtuale /sys. I cgroup attualmente in uso dal kernel sono accessibili come subdirectory di /sys/fs/cgroup. Per creare un nuovo cgroup è sufficiente creare una subdirectory in quel ramo del filesystem.
Ad esempio, si supponga di voler limitare la memoria massima allocabile da un processo, il cui PID è 1234, a 100 MB. Per far ciò è possibile creare un nuovo cgroup sotto memory, impostare il limite, ed aggiungere il processo al cgroup, con i comandi:
<syntaxhighlight>
mkdir /sys/fs/cgroup/memory/miocgroup
echo 104857600 > /sys/fs/cgroup/memory/miocgroup/memory.limit_in_bytes
echo 1234 > /sys/fs/cgroup/memory/miocgroup/cgroup.procs
</syntaxhighlight>
Come si intuisce, questa caratteristica permette di gestire le risorse allocate ad un particolare container in esecuzione. Qualora sia necessario impostare un limite per una risorsa specifica, sarà sufficiente creare un cgroup configurandolo opportunamente.
<b>Docker</b> sfrutta questa caratteristica del kernel Linux per implementare i limiti delle risorse allocate ai container. Quando un container è configurato con un limite su una o più risorse, Docker crea i corrispondenti cgroups in /sys/fs/cgroup/ ed aggiunge automaticamente i PID dei processi in esecuzione nel container.
==== Namespaces ====
Oltre a gestire l’allocazione delle risorse, il kernel ospitante ha anche il compito di garantire l’isolamento dei processi in esecuzione in container differenti. Per ovvi motivi di sicurezza non deve essere possibile per un processo in esecuzione in un container accedere direttamente alla macchina ospite o ad altri container.
Questa funzionalità è implementata nel kernel Linux mediante l’uso dei namespaces. Un namespace è essenzialmente un “contenitore” che astrae le risorse offerte dal kernel. Quando un processo fa parte di un certo namespace esso potrà accedere soltanto alle risorse presenti nel namespace.
In particolare, esistono diversi namespaces di default, ciascuno associato ad una tipologia differente di risorse: cgroup, IPC, Network, Mount, PID, User, UTS.
Per ciascun container in esecuzione Docker crea un opportuno gruppo di namespaces ed associa i processi in esecuzione nel container a quel namespace. In questo modo, i processi in esecuzione nel container non accederanno ai namspace della macchina ospitate, né a quelli di altri container. Ciò permette effettivamente di isolare i processi in esecuzione in un container.
Se non si creassero dei namespace ad hoc per ogni container, i processi del container potrebbero accedere direttamente alle risorse dell’host. Ad esempio, se un container non fosse associato al proprio namespace “cgroups”, esso potrebbe accedere ai cgroups della macchina host, avendo così il potere di gestire le risorse ad esso assegnate a piacimento.
Analogamente, se un container non fosse ristretto al proprio namespace “Network”, esso potrebbe accedere allo stack di rete della macchina host, avendo il potere di interferire arbitrariamente con le socket attive sull’host.
Come si intuisce dal nome, il namespace “PID” permette di isolare l’albero dei processi in esecuzione: un processo che voglia enumerare i processi in esecuzione potrà solo vedere quelli nel proprio namespace. Inoltre ogni namespace PID gestisce una propria numerazione, quindi i processi enumerati all’interno di un container avranno un PID diverso rispetto a quello effettivamente assegnatogli nel namespace globale.
Assegnare dei namespace differenti ad ogni container significa quindi creare delle sandbox in grado di isolare i processi del container dal resto della macchina.
==== Conclusioni ====
Cgroups e Namespaces sono caratteristiche fondamentali per l’implementazione di un sistema basato su container. Esse sono considerate mature essendo presenti in Linux da più di un decennio e basandosi su concetti già concepiti durante lo sviluppo di OpenVZ (nell’ormai lontano 2005).
Funzionalità analoghe sono state successivamente integrate anche in ambiente Windows, come parte dei Windows Native Containers. Tuttavia, ricordiamo che non è possibile eseguire container Linux direttamente sul kernel Windows (dato che, per l’appunto, non si virtualizza il sistema operativo ospite). In questi casi (host Windows ed immagini Linux), Docker ricorre comunque ad una macchina virtuale Linux, in esecuzione su HyperV.
=== Union filesystem ===
Una peculiarità delle immagini Docker è la loro stratificazione in layer, ognuno dei quali contribuisce alla definizione di quello che sarà poi il file system del container. Particolare di questi layer è la loro immutabilità: i layer sono infatti accessibili in sola lettura e non sono modificabili direttamente.
Questa, che potrebbe sembrare una limitazione, è invece un grande punto di forza di Docker, che in questo modo offre la possibilità a più immagini di condividere un layer comune.
Ad esempio, le immagini redis:3.0.0 e nginx:1.7 si basano entrambe su un layer comune (debian:jessie), sebbene la prima immagine appartenga ad un key/value store e la seconda ad un webserver.
[[File:Docker_image_layer.jpg]]
L’immagine spiega abbastanza chiaramente la situazione, ma per dimostrarlo faremo una prova scaricando le immagini di cui sopra. Per iniziare, ci procureremo l’immagine di redis, e siccome non vogliamo eseguire il container utilizziamo il comando pull al posto del precedente run:
<syntaxhighlight>
$ docker pull redis:3.0.0
3.0.0: Pulling from library/redis
193224d99eda: Pull complete
a3ed95caeb02: Pull complete
5d614b26c26f: Pull complete
8274a6625da0: Pull complete
86d9ae0920b7: Pull complete
f4f11f46a20e: Pull complete
c3192ae156a0: Pull complete
317cb6aa0b20: Pull complete
a1a961e320bc: Pull complete
Digest: sha256:06ce8790b8f63ad1ee9eec1aec5513c34331a350f66a370572405cb15508ecdc
Status: Downloaded newer image for redis:3.0.0
</syntaxhighlight>
Completato questo comando, abbiamo disponibile in locale la nostra immagine. Si faccia attenzione al fatto che, in realtà, non abbiamo scaricato l’immagine redis:3.0.0 ma nove diversi layer (uno per ogni riga Pull complete) che sovrapposti daranno luogo all’immagine redis:3.0.0.
Procediamo a scaricare la seconda immagine dell’esempio:
<syntaxhighlight>
$ docker pull nginx:1.7
1.7: Pulling from library/nginx
193224d99eda: Already exists
a3ed95caeb02: Pull complete
eb250aa1fe8b: Pull complete
26547bfb8cca: Pull complete
9118cfaa8eaa: Pull complete
a6efe51e1a3b: Pull complete
a2318bfd27ef: Pull complete
Digest: sha256:02537b932a849103ab21c75fac25c0de622ca12fe2c5ba8af2c7cb23339ee6d4
Status: Downloaded newer image for nginx:1.7
</syntaxhighlight>
Come si vede, il layer 193224d99eda esiste già in locale (Already exists), essendo stato scaricato precedentemente durante il pull dell’immagine redis:3.0.0. Non sarà quindi necessario un nuovo download.
== Immagini e container ==
Abbiamo posto l’attenzione su come un’immagine rappresenti, di fatto, una serie di layer immutabili, e di come questa caratteristica permetta a due immagini di condividere un layer comune.
Una pila di layer accessibili in sola lettura, però, non ha molto senso di esistere, ragion per cui quello che avviene nel momento in cui si chiede a Docker di istanziare un container è la creazione, in cima a tutti gli altri layer, di un singolo layer scrivibile.
D’ora in avanti, tutte le modifiche apportate al container verranno memorizzate all’interno di questo layer, detto anche layer container per differenziarlo da quelli in sola lettura, detti invece layer immagine.
<b>Relazione tra immagini e container:</b>
un’immagine è una pila di layer accessibili in sola lettura; il container relativo è la stessa pila di layer con sopra un layer scrivibile. Il contenuto finale del container è ottenuto per sovrapposizione di tutti questi layer.
Così come da un negativo posso sviluppare più copie della stessa foto, a partire da un’immagine posso avviare più istanze dello stesso container.
Nel momento in cui arriverà la richiesta di avviare una seconda istanza di un container, Docker non farà altro che creare un nuovo layer container. Ad esempio, se eseguissimo due istanze dell’immagine nginx:1.7, si verrebbe a creare una situazione simile alla seguente:
[[File:Docker_istanze_container.jpg]]
Come possiamo osservare nella figura, i layer immagine sono condivisi ed hanno tutti lo stesso identificativo, essendo comuni e generati in base al loro contenuto. Gli identificativi dei due layer container, invece, sono diversi tra loro poichè, a differenza dei layer immagine sottostanti, questi vengono generati in maniera casuale (per ovvi motivi).
== Container come ambienti isolati ==
Docker, e i container in particolare, danno la possibilità di creare in maniera semplice degli ambienti isolati per le nostre applicazioni.
Sebbene questo offra da un lato grandi vantaggi, dall’altro pone delle restrizioni che dovremo conoscere e sapere gestire.
=== File system ===
A livello di filesystem, ogni container è completamente isolato dal sistema sottostante e da quello di tutti gli altri container.
Facciamo una prova:
<syntaxhighlight>
$ docker run -it ubuntu /bin/bash
root@9de56c415886:/# mkdir html_it
root@9de56c415886:/# ls
bin  boot  dev  etc  home  html_it  ... sys  tmp  usr  var
root@9de56c415886:/#  // CTRL + P + Q
$ docker run -it ubuntu /bin/bash
root@33a9ddca9af1:/# ls
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
</syntaxhighlight>
In questo caso abbiamo creato un container Ubuntu (id 9de56c415886) al cui interno abbiamo creato la directory html_it. Abbiamo quindi messo tale container in background, per poi creare una nuova istanza (id 33a9ddca9af1): come prevedibile, all’interno di quest’ultima istanza la directory precedentemente creata non esiste. Questo, vale la pena specificarlo ancora una volta, avviene in quanto <b>ogni modifica al container viene memorizzata all’interno del layer container</b> e non si riflette ai layer immagine sottostanti.
== Comandi ==
Per vedere tutti i container, anche quelli non più attivi:
<syntaxhighlight>
$ docker ps -a
CONTAINER ID    IMAGE          COMMAND  CREATED          STATUS  PORTS  NAMES
c3849603f184    hello-world    "/hello"  3 weeks ago      Exited          trusting_bell
0017d2a0b9d1    hello-world    "/hello"  6 seconds ago    Exited          boring_turing
</syntaxhighlight>
Il comando mostrerà a video l’id breve del container layer, il nome dell’immagine che l’ha generato, il processo al suo interno, la data di creazione e lo stato. Oltre all’id esadecimale, ogni container ha anche un nome più “user friendly”, che viene assegnato da Docker ed ha sempre la forma di un aggettivo, seguito dal nome di uno scienziato famoso.
Se volessimo scegliere noi un nome più significativo, potremmo farlo specificandolo all’interno del comando docker run, utilizzando il flag --name:
<syntaxhighlight>$ docker run --name hello_html_it hello-world</syntaxhighlight>
=== Modalità detached e modalità interattiva ===
Oltre al flag --name appena visto, il comando docker run supporta altre opzioni che condizionano fortemente il lifestyle del container.
Tra i più interessanti troviamo sicuramente <b>-d</b>, ad indicare un container detached. In questa modalità, il container appena avviato viene eseguito in background. Ad esempio:
<syntaxhighlight>$ docker run -d hello-world
b4ec94bf57023778c0ad191e4626f563cd86edfbb893cb701172e676462fe063</syntaxhighlight>
In questo caso, piuttosto che il solito saluto ci viene mostrata una stringa, rappresentante l’id in forma estesa del layer container. Per visualizzare l’output prodotto in background, possiamo utilizzare il comando docker logs seguito dall’identificativo del container (bastano anche i primi caratteri):
<syntaxhighlight>docker logs b4ec9</syntaxhighlight>
<b>L’utilizzo di tale modalità è particolarmente indicato per l’avvio di container che fungeranno poi da servizi, quali web server, DBMS e così via.</b>
I <b>flag -i e -t</b> (quasi sempre usati come singolo <b>flag -it</b>), invece, fanno sì che i canali standard di un container vengano rediretti, dando così l’impressione di essere fisicamente all’interno dello stesso.
Per questo esempio, utilizzeremo l’immagine di Ubuntu, ne istanzieremo un container e proveremo ad utilizzare la shell al suo interno in maniera interattiva.
Oltre ai vari flag appena visti, il comando docker run accetta in ingresso un eseguibile da lanciare una volta che il container è pronto (visualizzato nella colonna COMMAND dell’output di docker ps).
In questo esempio, vorremo visualizzare l’output di un semplice comando ls, e perciò avremo bisogno di una shell; pertanto, l’eseguibile che andremo ad indicare sarà appunto /bin/bash e il comando finale avrà questa forma:
<syntaxhighlight>
$ docker run -it ubuntu /bin/bash
root@996fc96427ff:/# ls
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
</syntaxhighlight>
Per evidenziare il cambio di ambiente, viene mostrato un prompt diverso dal precedente, nel nostro caso root@996fc96427ff:/# dove root è il nome utente e 996fc96427ff l’id breve del container. A questo punto abbiamo pieno accesso al container e possiamo utilizzare ls per listare file e directory.
Poichè, come detto precedentemente, un container resta in vita finché è in vita il processo ospitato al suo interno, per terminare la modalità interattiva sarà sufficiente eseguire il comando exit.
Per terminare, invece, la sola modalità interattiva (senza chiudere pertanto il container), utilizzeremo la combinazione di tasti CTRL + P + Q. Se vogliamo ristabilire un collegamento interattivo col container, possiamo utilizzare il comando docker attach, seguito dall’identificativo del container:
<syntaxhighlight>
$ docker run -it ubuntu /bin/bash
root@113e7c31aae9:/#  // CTRL + P + Q
$ docker ps
CONTAINER ID        IMAGE              COMMAND            CREATED            STATUS              PORTS              NAMES
113e7c31aae9        ubuntu              "/bin/bash"        12 seconds ago      Up 11 seconds                          awesome_bartik
$ docker attach 113e7c
root@113e7c31aae9:/#
</syntaxhighlight>
Come risulta anche dall’output di docker ps, la combinazione di tasti CTRL + P + Q non ha arrestato il container. Per riattaccarci al container useremo il comando docker attach seguito dal container id (anche in questo caso basta la porzione iniziale).
=== Eliminazione di container  ===
Cancellare un container vuol dire eliminare il container layer creato al momento dell’esecuzione del comando docker run: tutte le modifiche verranno perse irrimediabilmente, mentre i layer immagine sottostanti resteranno sul nostro hard disk.
rm seguito da container id o parte di esso:
<syntaxhighlight>docker rm 113e7c</syntaxhighlight>
oppure, se il container che si vuole eliminare è ancora in esecuzione:
<syntaxhighlight>docker rm --force 113e7c</syntaxhighlight>
=== Immagini ===
Elenco<br/>
docker images -a
<br/>
Eliminazione<br/>
docker rmi
=== Volumi ===
Eòenco<br/>
docker volume ls
<br/>
Eliminazione<br/>
docker volume rm volume_name volume_name
=== Ciclo di vita di un container ===
Se abbiamo l’esigenza di avviare un container attualmente spento, il semplice docker run non servirà più, e al suo posto va utilizzato il comando docker start seguito dall’id del container.
Supponiamo di aver terminato il container precedente (id 113e7c31aae9) e abbiamo ora bisogno di rientrarci; procederemo come segue:
<syntaxhighlight>
$ docker start 113e7c31aae9
113e7c31aae9
$ docker attach 113e7c31aae9
root@113e7c31aae9:/#
</syntaxhighlight>
Analogamente, esiste il comando docker stop per terminare un container attualmente in esecuzione, i comandi pause e unpause per sospendere e riattivare un container, nonché il comando restart per riavviarlo in caso di necessità.
Elenco Immagini<br/>
docker images -a
Elenco Volumi<br/>
docker volume ls
== Wordpress ==
=== Creazione Docker Worpress pulito ===
* docker-compose.yml
<syntaxhighlight lang="yaml">
services:
  db:
    image: mysql:8.4
    ports:
      - 3306:3306
    environment:
      MYSQL_DATABASE: webmobili
      MYSQL_USER: webmobili
      MYSQL_PASSWORD: w3bm0b1l1
      MYSQL_ROOT_PASSWORD: w3bm0b1l1
    volumes:
      - db:/var/lib/mysql
  wordpress:
    depends_on:
      - db
    build:
      context: ./
    ports:
      - 8000:80
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: webmobili
      WORDPRESS_DB_PASSWORD: w3bm0b1l1
      WORDPRESS_DB_NAME: webmobili
      WORDPRESS_DEBUG: 1
      TZ: "Europe/Rome"
    volumes:
      - wordpress:/var/www/html
volumes:
  db:
  wordpress:
</syntaxhighlight >
<pre>
build:
      context: ./
./ è il percorso per il Dockerfile
</pre>
* Dockerfile
<syntaxhighlight lang="bash">
FROM wordpress:6.8.2-php8.4-apache
RUN apt-get update && \
  pecl install xdebug \
  && docker-php-ext-enable xdebug
# Copy xdebug.ini to /usr/local/etc/php/conf.d/
COPY ./*.ini /usr/local/etc/php/conf.d/
# Copy plugin data to to /usr/src/wordpress/wp-content/plugins/
#COPY --chown=www-data:www-data ./designbest-esw/ /usr/src/wordpress/wp-content/plugins/designbest-esw/
#COPY --chown=www-data:www-data ./designbest-esw-lead/ /usr/src/wordpress/wp-content/plugins/designbest-esw-lead/
</syntaxhighlight>
* xdebug.ini
<syntaxhighlight lang="bash">
zend_extension=xdebug.so
[xdebug]
xdebug.mode=develop,debug
xdebug.discover_client_host = true
xdebug.start_with_request=yes
</syntaxhighlight>
* Aprire VSCODE e apri la cartella
* F1 -> Add Dev Container Configuration Files
* Add configuration to workspace
* From docker-compose.yml
* wordpress
* additional -> niente
* additional files -> niente
* Viene creata cartella .devcontainer con relativo .json
* risistemarlo con i seguenti codici:
<syntaxhighlight lang="json">
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.238.0/containers/docker-existing-docker-compose
// If you want to run as a non-root user in the container, see .devcontainer/docker-compose.yml.
{
"name": "Designbest Commerce ESW",
// Update the 'dockerComposeFile' list if you have more compose files or use different names.
// The .devcontainer/docker-compose.yml file contains any overrides you need/want to make.
"dockerComposeFile": [
"../docker-compose.yml"
],
// The 'service' property is the name of the service for the container that VS Code should
// use. Update this value and .devcontainer/docker-compose.yml to the real service name.
"service": "wordpress",
// The optional 'workspaceFolder' property is the path VS Code should open by default when
// connected. This is typically a file mount in .devcontainer/docker-compose.yml
"workspaceFolder": "/var/www/html",
"customizations": {
"vscode": {
"extensions": [
"xdebug.php-pack", "xdebug.php-debug", "bmewburn.vscode-intelephense-client","neilbrayfield.php-docblocker","olback.es6-css-minify"
]
}
},
// Add the IDs of extensions you want installed when the container is created.
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [8000]
// Uncomment the next line if you want start specific services in your Docker Compose config.
// "runServices": [],
// Uncomment the next line if you want to keep your containers running after VS Code shuts down.
// "shutdownAction": "none",
// Uncomment the next line to run commands after the container is created - for example installing curl.
// "postCreateCommand": "apt-get update && apt-get install -y curl",
// Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root.
// "remoteUser": "vscode"
}
</syntaxhighlight>
* F1 - Dev Containers: Rebuild and reopen in container
* Dopo aver caricato il container bisogna impostare Xdebug
* Create a .json file -> PHP
=== Aumentare la dimensione degli uploads ===
Di default l'immagine di Wordpress ha un limite di upload dei file di 2MB.
Per aumentare questo limite creare un file <code>uploads.ini</code> dove è presente il docker-compose:
<syntaxhighlight lang="bash">
file_uploads = On
memory_limit = 500M
upload_max_filesize = 500M
post_max_size = 500M
max_execution_time = 600
</syntaxhighlight>
Nel docker-compose aggiungere la seguente istruzione:
<syntaxhighlight>
volumes:
  - ./uploads.ini:/usr/local/etc/php/conf.d/uploads.ini
</syntaxhighlight>

Versione attuale delle 10:57, 31 lug 2025

Introduzione

[modifica]

Docker è un progetto open source nato con lo scopo di automatizzare la distribuzione di applicazioni sotto forma di “contenitori” leggeri, portabili e autosufficienti che possono essere eseguiti su cloud (pubblici o privati) o in locale.

I “contenitori” di Docker, d’ora in poi chiamati con il termine inglese Container, sono quindi l’insieme dei dati di cui necessita un’applicazione per essere eseguita: librerie, altri eseguibili, rami del file system, file di configurazione, script, ecc..

Il processo di distribuzione di un’applicazione si riduce quindi alla semplice creazione di una Immagine Docker, ovvero di un file contenente tutti i dati di cui sopra. L’immagine viene utilizzata da Docker per creare un Container, un’istanza dell’immagine che eseguirà l’applicazione in essa contenuta.

I container sono quindi autosufficienti perché contengono già tutte le dipendenze dell’applicazione e non richiedono quindi particolari configurazioni sull’host, ma sono soprattuto portabili perché essi sono distribuiti in un formato standard (le immagini appunto), che può essere letto ed eseguito da qualunque server Docker.

Inoltre, i container sono leggeri perché sfruttano i servizi offerti dal kernel del sistema operativo ospitante, invece di richiedere l’esecuzione di un kernel virtualizzato come avviene nel caso delle VM. Ciò permette di ospitare un gran numero di container nella stessa macchina fisica.

Data la sua stretta relazione con il kernel Linux, Docker è nativamente disponibile in ambiente GNU/Linux. Tuttavia, essendo uno strumento concepito per gli sviluppatori, si è resa necessaria la possibilità di eseguirlo direttamente sulle workstation, senza imporre limiti sul sistema operativo in uso. Docker è pertanto disponibile anche in ambiente macOS e Windows. In entrambi i casi, si ricorre ad una tecnologia di virtualizzazione per eseguire un kernel Linux al quale si appoggia l’engine Docker.

Funzionamento

[modifica]

Il modello di esecuzione basato su container si fonda su una serie di funzionalità sviluppate nel kernel Linux e successivamente adottate anche da altre piattaforme, espressamente studiate per questo scopo.

Un sistema basato su container inteso come alternativa alla virtualizzazione deve garantire prestazioni superiori pur offrendo le stesse caratteristiche in termini di flessibilità nella gestione delle risorse e di sicurezza.

L’incremento prestazionale è dovuto essenzialmente all’eliminazione di uno strato: a differenza di quanto avviene quando un processo viene eseguito all’interno di una macchina virtuale, i processi eseguiti da un container sono di fatto eseguiti dal sistema ospitante, usufruendo dei servizi offerti dal kernel che esso esegue. Viene quindi eliminato l’overhead dovuto all’esecuzione di ogni singolo kernel di ogni VM.

I requisiti di flessibilità e sicurezza in un sistema virtualizzato sono a carico dell’Hypervisor, lo strato software in esecuzione nella macchina ospitante che si occupa di gestire le risorse allocate a ciascuna VM e che adotta (anche con l’ausilio dell’hardware) tutte le politiche necessarie per isolare i processi in esecuzione su VM differenti.

In un ambiente basato su container, dove quindi non è presente un Hypervisor, queste funzionalità sono assolte dal kernel del sistema operativo ospitante. Linux dispone di due caratteristiche progettate proprio per questo scopo: Control Groups (o cgroups) e Namespaces.

I control groups (cgroups)

[modifica]

I control groups sono lo strumento utilizzato dal kernel Linux per gestire l’utilizzo delle risorse di calcolo da parte di un gruppo specifico di processi. Grazie ai cgroups è possibile limitare la quantità di risorse utilizzate da uno o più processi. Ad esempio, è possibile limitare il quantitativo massimo di memoria RAM che un gruppo di processi può utilizzare.

In un sistema Linux esistono più cgroups, ciascuno associato ad uno o più resource controllers (talvolta chiamati anche subsystems), in base alle risorse che gestiscono. Ad esempio, un cgroup può essere associato al resource controller memory per gestire la quantità di memoria allocabile da un certo insieme di processi.

I cgroup sono organizzati in modo gerarchico in una struttura ad albero. Ogni nodo dell’albero rappresenta una gruppo, definito dalle regole che gestiscono la risorsa a cui è associato (ad esempio la dimensione massima di memoria allocabile) e dalla lista dei processi che ne fanno parte. Ciascun processo in quella lista risponderà alle regole definite nel gruppo.

Si può interagire manualmente con i cgroup attraverso il filesystem virtuale /sys. I cgroup attualmente in uso dal kernel sono accessibili come subdirectory di /sys/fs/cgroup. Per creare un nuovo cgroup è sufficiente creare una subdirectory in quel ramo del filesystem.

Ad esempio, si supponga di voler limitare la memoria massima allocabile da un processo, il cui PID è 1234, a 100 MB. Per far ciò è possibile creare un nuovo cgroup sotto memory, impostare il limite, ed aggiungere il processo al cgroup, con i comandi:

mkdir /sys/fs/cgroup/memory/miocgroup
echo 104857600 > /sys/fs/cgroup/memory/miocgroup/memory.limit_in_bytes
echo 1234 > /sys/fs/cgroup/memory/miocgroup/cgroup.procs

Come si intuisce, questa caratteristica permette di gestire le risorse allocate ad un particolare container in esecuzione. Qualora sia necessario impostare un limite per una risorsa specifica, sarà sufficiente creare un cgroup configurandolo opportunamente.

Docker sfrutta questa caratteristica del kernel Linux per implementare i limiti delle risorse allocate ai container. Quando un container è configurato con un limite su una o più risorse, Docker crea i corrispondenti cgroups in /sys/fs/cgroup/ ed aggiunge automaticamente i PID dei processi in esecuzione nel container.

Namespaces

[modifica]

Oltre a gestire l’allocazione delle risorse, il kernel ospitante ha anche il compito di garantire l’isolamento dei processi in esecuzione in container differenti. Per ovvi motivi di sicurezza non deve essere possibile per un processo in esecuzione in un container accedere direttamente alla macchina ospite o ad altri container.

Questa funzionalità è implementata nel kernel Linux mediante l’uso dei namespaces. Un namespace è essenzialmente un “contenitore” che astrae le risorse offerte dal kernel. Quando un processo fa parte di un certo namespace esso potrà accedere soltanto alle risorse presenti nel namespace.

In particolare, esistono diversi namespaces di default, ciascuno associato ad una tipologia differente di risorse: cgroup, IPC, Network, Mount, PID, User, UTS.

Per ciascun container in esecuzione Docker crea un opportuno gruppo di namespaces ed associa i processi in esecuzione nel container a quel namespace. In questo modo, i processi in esecuzione nel container non accederanno ai namspace della macchina ospitate, né a quelli di altri container. Ciò permette effettivamente di isolare i processi in esecuzione in un container.

Se non si creassero dei namespace ad hoc per ogni container, i processi del container potrebbero accedere direttamente alle risorse dell’host. Ad esempio, se un container non fosse associato al proprio namespace “cgroups”, esso potrebbe accedere ai cgroups della macchina host, avendo così il potere di gestire le risorse ad esso assegnate a piacimento.

Analogamente, se un container non fosse ristretto al proprio namespace “Network”, esso potrebbe accedere allo stack di rete della macchina host, avendo il potere di interferire arbitrariamente con le socket attive sull’host.

Come si intuisce dal nome, il namespace “PID” permette di isolare l’albero dei processi in esecuzione: un processo che voglia enumerare i processi in esecuzione potrà solo vedere quelli nel proprio namespace. Inoltre ogni namespace PID gestisce una propria numerazione, quindi i processi enumerati all’interno di un container avranno un PID diverso rispetto a quello effettivamente assegnatogli nel namespace globale.

Assegnare dei namespace differenti ad ogni container significa quindi creare delle sandbox in grado di isolare i processi del container dal resto della macchina.

Conclusioni

[modifica]

Cgroups e Namespaces sono caratteristiche fondamentali per l’implementazione di un sistema basato su container. Esse sono considerate mature essendo presenti in Linux da più di un decennio e basandosi su concetti già concepiti durante lo sviluppo di OpenVZ (nell’ormai lontano 2005).

Funzionalità analoghe sono state successivamente integrate anche in ambiente Windows, come parte dei Windows Native Containers. Tuttavia, ricordiamo che non è possibile eseguire container Linux direttamente sul kernel Windows (dato che, per l’appunto, non si virtualizza il sistema operativo ospite). In questi casi (host Windows ed immagini Linux), Docker ricorre comunque ad una macchina virtuale Linux, in esecuzione su HyperV.


Union filesystem

[modifica]

Una peculiarità delle immagini Docker è la loro stratificazione in layer, ognuno dei quali contribuisce alla definizione di quello che sarà poi il file system del container. Particolare di questi layer è la loro immutabilità: i layer sono infatti accessibili in sola lettura e non sono modificabili direttamente.

Questa, che potrebbe sembrare una limitazione, è invece un grande punto di forza di Docker, che in questo modo offre la possibilità a più immagini di condividere un layer comune.

Ad esempio, le immagini redis:3.0.0 e nginx:1.7 si basano entrambe su un layer comune (debian:jessie), sebbene la prima immagine appartenga ad un key/value store e la seconda ad un webserver.

L’immagine spiega abbastanza chiaramente la situazione, ma per dimostrarlo faremo una prova scaricando le immagini di cui sopra. Per iniziare, ci procureremo l’immagine di redis, e siccome non vogliamo eseguire il container utilizziamo il comando pull al posto del precedente run:

 $ docker pull redis:3.0.0
 3.0.0: Pulling from library/redis
 193224d99eda: Pull complete
 a3ed95caeb02: Pull complete
 5d614b26c26f: Pull complete
 8274a6625da0: Pull complete
 86d9ae0920b7: Pull complete
 f4f11f46a20e: Pull complete
 c3192ae156a0: Pull complete
 317cb6aa0b20: Pull complete
 a1a961e320bc: Pull complete
 Digest: sha256:06ce8790b8f63ad1ee9eec1aec5513c34331a350f66a370572405cb15508ecdc
 Status: Downloaded newer image for redis:3.0.0

Completato questo comando, abbiamo disponibile in locale la nostra immagine. Si faccia attenzione al fatto che, in realtà, non abbiamo scaricato l’immagine redis:3.0.0 ma nove diversi layer (uno per ogni riga Pull complete) che sovrapposti daranno luogo all’immagine redis:3.0.0.

Procediamo a scaricare la seconda immagine dell’esempio:

 $ docker pull nginx:1.7
 1.7: Pulling from library/nginx
 193224d99eda: Already exists
 a3ed95caeb02: Pull complete
 eb250aa1fe8b: Pull complete
 26547bfb8cca: Pull complete
 9118cfaa8eaa: Pull complete
 a6efe51e1a3b: Pull complete
 a2318bfd27ef: Pull complete
 Digest: sha256:02537b932a849103ab21c75fac25c0de622ca12fe2c5ba8af2c7cb23339ee6d4
 Status: Downloaded newer image for nginx:1.7

Come si vede, il layer 193224d99eda esiste già in locale (Already exists), essendo stato scaricato precedentemente durante il pull dell’immagine redis:3.0.0. Non sarà quindi necessario un nuovo download.

Immagini e container

[modifica]

Abbiamo posto l’attenzione su come un’immagine rappresenti, di fatto, una serie di layer immutabili, e di come questa caratteristica permetta a due immagini di condividere un layer comune.

Una pila di layer accessibili in sola lettura, però, non ha molto senso di esistere, ragion per cui quello che avviene nel momento in cui si chiede a Docker di istanziare un container è la creazione, in cima a tutti gli altri layer, di un singolo layer scrivibile.

D’ora in avanti, tutte le modifiche apportate al container verranno memorizzate all’interno di questo layer, detto anche layer container per differenziarlo da quelli in sola lettura, detti invece layer immagine.

Relazione tra immagini e container: un’immagine è una pila di layer accessibili in sola lettura; il container relativo è la stessa pila di layer con sopra un layer scrivibile. Il contenuto finale del container è ottenuto per sovrapposizione di tutti questi layer.

Così come da un negativo posso sviluppare più copie della stessa foto, a partire da un’immagine posso avviare più istanze dello stesso container.

Nel momento in cui arriverà la richiesta di avviare una seconda istanza di un container, Docker non farà altro che creare un nuovo layer container. Ad esempio, se eseguissimo due istanze dell’immagine nginx:1.7, si verrebbe a creare una situazione simile alla seguente:

Come possiamo osservare nella figura, i layer immagine sono condivisi ed hanno tutti lo stesso identificativo, essendo comuni e generati in base al loro contenuto. Gli identificativi dei due layer container, invece, sono diversi tra loro poichè, a differenza dei layer immagine sottostanti, questi vengono generati in maniera casuale (per ovvi motivi).

Container come ambienti isolati

[modifica]

Docker, e i container in particolare, danno la possibilità di creare in maniera semplice degli ambienti isolati per le nostre applicazioni.

Sebbene questo offra da un lato grandi vantaggi, dall’altro pone delle restrizioni che dovremo conoscere e sapere gestire.

File system

[modifica]

A livello di filesystem, ogni container è completamente isolato dal sistema sottostante e da quello di tutti gli altri container. Facciamo una prova:

$ docker run -it ubuntu /bin/bash
root@9de56c415886:/# mkdir html_it
root@9de56c415886:/# ls
bin  boot  dev  etc  home  html_it  ... sys  tmp  usr  var
root@9de56c415886:/#  // CTRL + P + Q
$ docker run -it ubuntu /bin/bash
root@33a9ddca9af1:/# ls
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

In questo caso abbiamo creato un container Ubuntu (id 9de56c415886) al cui interno abbiamo creato la directory html_it. Abbiamo quindi messo tale container in background, per poi creare una nuova istanza (id 33a9ddca9af1): come prevedibile, all’interno di quest’ultima istanza la directory precedentemente creata non esiste. Questo, vale la pena specificarlo ancora una volta, avviene in quanto ogni modifica al container viene memorizzata all’interno del layer container e non si riflette ai layer immagine sottostanti.

Comandi

[modifica]

Per vedere tutti i container, anche quelli non più attivi:

 $ docker ps -a
 CONTAINER ID    IMAGE          COMMAND   CREATED          STATUS   PORTS   NAMES
 c3849603f184    hello-world    "/hello"  3 weeks ago      Exited          trusting_bell
 0017d2a0b9d1    hello-world    "/hello"  6 seconds ago    Exited          boring_turing

Il comando mostrerà a video l’id breve del container layer, il nome dell’immagine che l’ha generato, il processo al suo interno, la data di creazione e lo stato. Oltre all’id esadecimale, ogni container ha anche un nome più “user friendly”, che viene assegnato da Docker ed ha sempre la forma di un aggettivo, seguito dal nome di uno scienziato famoso.

Se volessimo scegliere noi un nome più significativo, potremmo farlo specificandolo all’interno del comando docker run, utilizzando il flag --name:

$ docker run --name hello_html_it hello-world

Modalità detached e modalità interattiva

[modifica]

Oltre al flag --name appena visto, il comando docker run supporta altre opzioni che condizionano fortemente il lifestyle del container.

Tra i più interessanti troviamo sicuramente -d, ad indicare un container detached. In questa modalità, il container appena avviato viene eseguito in background. Ad esempio:

$ docker run -d hello-world
b4ec94bf57023778c0ad191e4626f563cd86edfbb893cb701172e676462fe063

In questo caso, piuttosto che il solito saluto ci viene mostrata una stringa, rappresentante l’id in forma estesa del layer container. Per visualizzare l’output prodotto in background, possiamo utilizzare il comando docker logs seguito dall’identificativo del container (bastano anche i primi caratteri):

docker logs b4ec9

L’utilizzo di tale modalità è particolarmente indicato per l’avvio di container che fungeranno poi da servizi, quali web server, DBMS e così via.

I flag -i e -t (quasi sempre usati come singolo flag -it), invece, fanno sì che i canali standard di un container vengano rediretti, dando così l’impressione di essere fisicamente all’interno dello stesso.

Per questo esempio, utilizzeremo l’immagine di Ubuntu, ne istanzieremo un container e proveremo ad utilizzare la shell al suo interno in maniera interattiva.

Oltre ai vari flag appena visti, il comando docker run accetta in ingresso un eseguibile da lanciare una volta che il container è pronto (visualizzato nella colonna COMMAND dell’output di docker ps).

In questo esempio, vorremo visualizzare l’output di un semplice comando ls, e perciò avremo bisogno di una shell; pertanto, l’eseguibile che andremo ad indicare sarà appunto /bin/bash e il comando finale avrà questa forma:

 $ docker run -it ubuntu /bin/bash
 root@996fc96427ff:/# ls
 bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

Per evidenziare il cambio di ambiente, viene mostrato un prompt diverso dal precedente, nel nostro caso root@996fc96427ff:/# dove root è il nome utente e 996fc96427ff l’id breve del container. A questo punto abbiamo pieno accesso al container e possiamo utilizzare ls per listare file e directory.

Poichè, come detto precedentemente, un container resta in vita finché è in vita il processo ospitato al suo interno, per terminare la modalità interattiva sarà sufficiente eseguire il comando exit.

Per terminare, invece, la sola modalità interattiva (senza chiudere pertanto il container), utilizzeremo la combinazione di tasti CTRL + P + Q. Se vogliamo ristabilire un collegamento interattivo col container, possiamo utilizzare il comando docker attach, seguito dall’identificativo del container:

 $ docker run -it ubuntu /bin/bash
 root@113e7c31aae9:/#   // CTRL + P + Q
 $ docker ps
 CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
 113e7c31aae9        ubuntu              "/bin/bash"         12 seconds ago      Up 11 seconds                           awesome_bartik
 $ docker attach 113e7c
 root@113e7c31aae9:/#

Come risulta anche dall’output di docker ps, la combinazione di tasti CTRL + P + Q non ha arrestato il container. Per riattaccarci al container useremo il comando docker attach seguito dal container id (anche in questo caso basta la porzione iniziale).

Eliminazione di container

[modifica]

Cancellare un container vuol dire eliminare il container layer creato al momento dell’esecuzione del comando docker run: tutte le modifiche verranno perse irrimediabilmente, mentre i layer immagine sottostanti resteranno sul nostro hard disk.

rm seguito da container id o parte di esso:

docker rm 113e7c

oppure, se il container che si vuole eliminare è ancora in esecuzione:

docker rm --force 113e7c


Immagini

[modifica]

Elenco
docker images -a
Eliminazione
docker rmi

Volumi

[modifica]

Eòenco
docker volume ls
Eliminazione
docker volume rm volume_name volume_name

Ciclo di vita di un container

[modifica]

Se abbiamo l’esigenza di avviare un container attualmente spento, il semplice docker run non servirà più, e al suo posto va utilizzato il comando docker start seguito dall’id del container.

Supponiamo di aver terminato il container precedente (id 113e7c31aae9) e abbiamo ora bisogno di rientrarci; procederemo come segue:

$ docker start 113e7c31aae9
113e7c31aae9
$ docker attach 113e7c31aae9
root@113e7c31aae9:/#

Analogamente, esiste il comando docker stop per terminare un container attualmente in esecuzione, i comandi pause e unpause per sospendere e riattivare un container, nonché il comando restart per riavviarlo in caso di necessità.

Elenco Immagini
docker images -a

Elenco Volumi
docker volume ls

Wordpress

[modifica]

Creazione Docker Worpress pulito

[modifica]
  • docker-compose.yml
services:

  db:
    image: mysql:8.4
    ports:
      - 3306:3306
    environment:
      MYSQL_DATABASE: webmobili
      MYSQL_USER: webmobili
      MYSQL_PASSWORD: w3bm0b1l1
      MYSQL_ROOT_PASSWORD: w3bm0b1l1
    volumes:
      - db:/var/lib/mysql

  wordpress:
    depends_on:
      - db
    build:
      context: ./
    ports:
      - 8000:80
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: webmobili
      WORDPRESS_DB_PASSWORD: w3bm0b1l1
      WORDPRESS_DB_NAME: webmobili
      WORDPRESS_DEBUG: 1
      TZ: "Europe/Rome"
    volumes:
      - wordpress:/var/www/html

volumes:
  db:
  wordpress:
build:
      context: ./

./ è il percorso per il Dockerfile


  • Dockerfile
FROM wordpress:6.8.2-php8.4-apache
RUN apt-get update && \
  pecl install xdebug \
  && docker-php-ext-enable xdebug
# Copy xdebug.ini to /usr/local/etc/php/conf.d/
COPY ./*.ini /usr/local/etc/php/conf.d/
# Copy plugin data to to /usr/src/wordpress/wp-content/plugins/
#COPY --chown=www-data:www-data ./designbest-esw/ /usr/src/wordpress/wp-content/plugins/designbest-esw/
#COPY --chown=www-data:www-data ./designbest-esw-lead/ /usr/src/wordpress/wp-content/plugins/designbest-esw-lead/
  • xdebug.ini
zend_extension=xdebug.so

[xdebug]
xdebug.mode=develop,debug
xdebug.discover_client_host = true
xdebug.start_with_request=yes
  • Aprire VSCODE e apri la cartella
  • F1 -> Add Dev Container Configuration Files
  • Add configuration to workspace
  • From docker-compose.yml
  • wordpress
  • additional -> niente
  • additional files -> niente
  • Viene creata cartella .devcontainer con relativo .json
  • risistemarlo con i seguenti codici:
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.238.0/containers/docker-existing-docker-compose
// If you want to run as a non-root user in the container, see .devcontainer/docker-compose.yml.
{
	"name": "Designbest Commerce ESW",

	// Update the 'dockerComposeFile' list if you have more compose files or use different names.
	// The .devcontainer/docker-compose.yml file contains any overrides you need/want to make.
	"dockerComposeFile": [
		"../docker-compose.yml"
	],

	// The 'service' property is the name of the service for the container that VS Code should
	// use. Update this value and .devcontainer/docker-compose.yml to the real service name.
	"service": "wordpress",

	// The optional 'workspaceFolder' property is the path VS Code should open by default when
	// connected. This is typically a file mount in .devcontainer/docker-compose.yml
	"workspaceFolder": "/var/www/html",
	"customizations": {
		"vscode": {
			"extensions": [
				"xdebug.php-pack", "xdebug.php-debug", "bmewburn.vscode-intelephense-client","neilbrayfield.php-docblocker","olback.es6-css-minify"
			]
		}
	},
	
	// Add the IDs of extensions you want installed when the container is created.

	// Use 'forwardPorts' to make a list of ports inside the container available locally.
	"forwardPorts": [8000]

	// Uncomment the next line if you want start specific services in your Docker Compose config.
	// "runServices": [],

	// Uncomment the next line if you want to keep your containers running after VS Code shuts down.
	// "shutdownAction": "none",

	// Uncomment the next line to run commands after the container is created - for example installing curl.
	// "postCreateCommand": "apt-get update && apt-get install -y curl",

	// Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root.
	// "remoteUser": "vscode"
}
  • F1 - Dev Containers: Rebuild and reopen in container
  • Dopo aver caricato il container bisogna impostare Xdebug
  • Create a .json file -> PHP

Aumentare la dimensione degli uploads

[modifica]

Di default l'immagine di Wordpress ha un limite di upload dei file di 2MB.

Per aumentare questo limite creare un file uploads.ini dove è presente il docker-compose:

file_uploads = On

memory_limit = 500M

upload_max_filesize = 500M

post_max_size = 500M

max_execution_time = 600

Nel docker-compose aggiungere la seguente istruzione:

volumes: 
   - ./uploads.ini:/usr/local/etc/php/conf.d/uploads.ini