Un immagine docker è la combinazione di più layer software integrati all'interno dell'Union File System. Un particolare di questi layer è che sono immutabili, ovvero accessibili in sola lettura, in questo modo più immagini in esecuzione possono condividere alcuni layer. Ovviamente sopra tutti questi c'è un layer scrivibile, che ci permette di interagire con l'immagine. Il layer scrivibile è detto Container layer, mentre i layer non scrivibili Image layer. Un'immagine è quindi una pila di layer in sola lettura, mentre un conainer generato da un immagine è la stessa pila di layer con l'aggiunta di un layer scrivibile. Con questo approccio è possbile avviare più container da una sola immagine. In sostanza i layer immagine saranno condivisi e si creeranno due layer container. Docker fornisce la possibilità di costruire le proprie immagini anche partendo da altre immagini, attraverso il dockerfile, ovvero un file di testo che ci permette di dare direttive su come modificare le immagini. in sostanza: - il dockerfile è la "ricetta" dell'immagine. - l'immagine è il risultato della "compilazione" del dockerfile. - un container è l'istanza della realizzazione di un immagine. docker è disponibile in due versioni: - docker community edition (CE) libero e open-source, comprende: - docker engine. - networking. - orchestrazione. - sicurezza di base. - docker enterprise edition (EE) soggetto a licenza, diviso in: - basic. - standard. - advanced. docker necessita di un kernel linux, anche le implementazioni su altri os si basano su una vm linux. REGISTRY un registry è un sistema di storage e content delivery di immagini docker. è possibile in un docker hub cercare informazioni e scaricare le immagini disponibili. i nomi delle immagini sono utilizzati nei comandi che interagiscono con regitry remoti, per esempio: docker pull < name >, indica a docker di fare il download dell'immagine "nome". possiamo anche specificare l'indirizzo di un hub specifico. è possibile che ci siano registry che necessitano di autenticazione, in questo caso prima di pull utilizziamo: docker login < url >, inseriamo username e password e abbiamo eseguito l'accesso. in seguito sarà paossibile scaricare tutte le immagini. è possibile fare il logout con: docker logout < url >. NB: un registry che non utilizza https è considerato insicuro, per utilizzarlo, specifichiamo il file /etc/docker/daemon.json inserendo: { "insecure-registries": [ "url1", "url2" ] } e riavviamo il demone: sudo systemctl restart docker REPOSITORY un repository è una collezione di immagini all'interno di un registry, ognuna con una versione diversa. per visualizzare la lista delle immagini disponibili possiamo usare il comando "docker images" che ci elenca tutte quelle disponibili. per ottenere il tag di quelle installate usiamo "docker images -q". per ricercare un'immagine nel docker hub possiamo usare il comando search, che ci elenca le immagini disponibili, per esempio, "docker search alpine" elenca tutti i repository corrispondenti alla ricerca. infine per fare il download usiamo quindi: "docker pull NAME[:tag|@DIGEST]", se non specifichiamo tag e digest viene usato il tag di default e l'ultima versione. per fare il download da un registry specifico usiamo "docker pull registry-url/NAME[:TAG|@DIGEST]. per ottenere il digest di un'immagine usiamo il comando "docker images --digest NAME" LAYER un'immagine può essere costituita da più layer, per esempio l'immagine di ubuntu è costituita da 5 strati, quando facciamo il pull vengono scaricati tutti e 5, ma con il comando docker images ne viene listata solo una. per visualizzare tutti i layer di un'immagine usiamo "docker inspect NAME". i layer vengono condivisi da più immagini, quindi se abbiamo dei layer già scaricati che vengono utilizzati da un'immagine che dobbiamo scaricare, questi verranno condivisi e verranno scaricati solo quelli mancanti. RIMOZIONE per rimuovere un'immagine scaricata utilizziamo il comando "docker rmi NAME[:TAG|@DIGEST]", se vogliamo eliminare tutte le immagini scaricate "docker rmi -f $(docker images -q)" RUN il comando "docker run NAME" avvia un container a partire da un immagine, i container non hanno un "processo init", infatti il processo con PID 1 all'interno del container sarà quello configurato per eseguire subito, detto entry point del container, nel caso di un container PostgreSQL, il processo 1 sarà quello del server del database. nel caso di un container, per esempio di ubuntu, il processo con PID 1 sarà bash e il saremo loggati in una shell come root e l'hostname sarà l'ID del container. in questo caso per uscire dalla shell senza fermare il container non utilizziamo exit ma "Ctrl-P Ctrl-Q", che ci riporta alla shell chiamante. possiamo anche creare un container senza avviarlo con il comando "docker create NAME". per startare il container ubuntu, o qualsiasi con un SO, usiamo gli argomenti -i -t (-it) che ci consente di abilitare lo stdin e una tty. possiamo eseguire un container con un comando che vogliamo come processo principale con "docker run NAME COMMAND" PS con il comando "docker ps" possiamo visualizzare l'elenco dei container attivi. ad ogni container creato viene assegnato un nome casuale di forma _, questo per facilitarci la referenziazione, che altrimenti avverrebbe con l'ID. con il comando "docker ps -a" elenchiamo tutti i container attivi e non attivi. EXEC e ATTACH il comando "docker exec CONTAINER COMMAND" esegue un comando in un container attivo. quando usciamo dal container senza terminarlo possiamo riconnetterci con il comando "docker attach NAME", con questo comando ci aggangiamo al stdin e stdout del processo 1 del container. START STOP RESTART PAUSE UNPAUSE il comando "docker stop NAME" ci permette di arrestare un container, terminando il processo con PID 1, il comando "docker start NAME" invece permette di ripristinare l'esecuzione di un container che è stato stoppato con il comando stop. con il comando "docker restart NAME" abbiamo una sequenza di stop e start. stop e restart possono avere l'opzione --time (-t), che indica il tempo in secondi da aspettare prima di eseguire effettivamente il comando. con il comando "docker pause NAME" possiamo invece congelare il container, in questo modo non terminiamo il processo ma viene solo sopspeso, può poi essere ripristinato con "docker unpause NAME". la differenza sta nel fatto che stop resetta lo stato dei processi, mentre pause lo mantiene. quando usciamo da un container con "exit", per riavviarlo utilizziamo "docker start -ai < name >" RIMOZIONE con il comando "docker rm NAME" si rimuove un container, non è possibile ovviamente rimuovere un container attivo, quindi, nel caso, dobbiamo prima arrestarlo con il comando stop. possiamo però forzare l'arresto con l'opzione -f che forza l'arresto e rimuove il container. ovviamente i dati interni al container saranno persi. con il comando "docker rm $(docker ps -a -q)" eliminiamo tutti i container arrestati. MODALITÀ DETATCHED quando mandiamo in esecuzione un container possiamo usare il parametro -d per abilitare la modalità detatched che ci permette di eseguirlo in background nel nostro terminale. per visualizzare l'output di un container in questa modalità dobbiamo utilizzare il comando "docker logs NAME". questa modalità viene usata per container che devono solo fornire un servizio, come database, web server, ecc. TEST DI QUANTO DETTO FINO AD ORA scarichiamo l'immagine di ubuntu: - docker pull ubuntu scriviamo questo script nell'host: - x=0 while true do x=$(( $x + 1 )) echo $x sleep 2 done e salviamolo come script.sh. questo script va avanti all'infinito e stampa un contatore eseguiamo il container: - docker run -d --name testContainer ubuntu /bin/bash "$(cat $(pwd)/script.sh)" in questo modo abbiamo lanciato in modalità detatched il container, ora verifichiamo cosa ci porta in stdout: - docker log --follow testContainer proviamo quindi a stoppare il container e ristartarlo con: - docker stop testContainer - docker start testContainer vediamo come il contatore si sia resettato dato che il processo è stato terminato. se utilizziamo pause, invece: - docker pause testContainer - docker unpause testContainer vediamo come il contatore continua dal punto in cui è stato interrotto. VARIABILI D'AMBIENTE per settare una variabile d'ambiente nel container possiamo utilizzare l'opzione "-e", per esempio: "docker run -it -e TEST=123 ubuntu" crea un container ubuntu con la variabile TEST con valore 123. NETWORKING per impostazione predefinita docker crea un'interfaccia di rete sull'host chiamata "docker0", che funge da bridge ethernet. tutto il traffico generato all'interno dei container verrà instradato in questo bridge e verrà gestito dal demone dockerd. ad ogni container viene assegnato un indirizzo nella subnet 172.17.0.0/16, che ovviamente, facendo parte solo della rete virtuale non sarà accessibile da remoto. per rendere un container accessibile dall'esterno possiamo fare il publish della porta, in questo modo la porta che ci serve del container verrà assegnata ad una porta casuale della macchina host. per farlo utilizziamo il comando: "docker run -d -p < port > < image > con il comando "docker ps" o il comando "docker port < name >" possiamo poi vedere quale porta è stata assegnata. per avere una specifica porta usiamo il comando "docker run -p < local port >:< container port > < name >" GESTIONE DATI uno svantaggio dell'isolamento tra i container è quello dell'accesso ai file, infatti: - i dati vengono eliminati all'eleiminazione di un container. - il layer di scrittura è strettamente legato alla macchina host su cui è in esecuzione. - le prestazioni sono penalizzate dall'utilizzo di un file system docker che si appoggia al kernel linux. bind mount con il bind mount si può mappare sul container un file o una directory dell'host. questo permette di avere file che si modificano in tempo reale. possiamo effettuare il bind con il comando: "docker run -v < host path >:< container path > < name >" in questo modo tutte le modifiche apportate all'interno del container o dell'host vengono apportate al file. è questo il modo in cui docker fornisce ai container la risoluzione DNS montando in ogni container il file /etc/resolve.conf nel caso di un database, per rendere i dati indipendeti dal ciclo di vita del container, facciamo il bind dei file contenuti in /var/lib/[mysql|mariadb|...]/... o del path configurato (grep -r datadir /etc/mysql) nel container, quindi per esempio: "docker run -v ~/docker_db_data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=pswd -d -p 3306:3306 mysql", in questo modo è come se "salviamo" tutti i file del database anche nell'host, rendendoli indipendenti dal ciclo di vita del container. volumi i volumi sono un altro meccanismo di persistenza dei dati indipendenti dalla struttura di directory dell'host, vengono infatti gestiti da docker nella directory /var/lib/docker/volumes. per creare un volume usiamo il comando "docker volume create < name >" per visualizzare l'elenco dei volumi usiamo "docker volume ls" per visualizzare i dettagli di un volume "docker volumes inspect < name >" per eliminare un volume "docker volume rm < name >" per visualizzare tutti i volumi non utilizzati "docker volume ls -q -f"dangling=true"" è possibile avviare un container con un volume non ancora creato con il comando "docker run [options] -v < vol name >:< container path > < name > ". quando eliminiamo il container il volume continuerà ad esistere, quindi potrà essere riutilizzato da altri container. per eliminare il volume insieme al container utilizziamo l'opzione "-v" durante l'eliminazione di un container. i volumi sono un ottimo modo per condividere dati tra conteiner e memorizzare informazioni. per rimouvere un volume bisogna prima arrestare tutti i container che lo utilizzano. come notiamo sia il bind mount che i volumi si specificano con l'ozione "-v", bisogna quindi prestare attenzione a questa sottigliezza: - < path >:< path > indica un bind mount, inizia sempre con /, ~ o ${PWD}. - < name >:< path > indica un volume, non ha mai / o ~ all'inzio. possiamo anche specificare il modo in cui il container accede al volume, per esempio: "-v < name >:< container path >:ro" permette al container di accedere al volume solo in lettura, di default abbiamo rw per lettura e scrittura. per specificare l'utilizzo di un volume usato da un altro container possiamo usare l'opzione "--volumes-from < container name >". CREARE IMMAGINI DOCKER per estendere un'immagine già disponibile o crearne una non esistente negli hub possiamo creare una nostra immagine in due modi: - manualmente usando il comando "docker commit" - utilizzando il descrittore "Dockerfile" docker commit il comando "docker commit < CONTAINER > < IMAGE >" ci permette di creare una nuova immagine a partire da un container. Il comando trasforma il layer container in un layer immagine così tutte le modifiche apportate all'immagine che stiamo usando saramno disponibili in un nuovo container. È da tenere a mente che ovviamente tutti i dati non saranno mantenuti e i processi saranno messi in pausa durante questa operazione. per provare possiamo installare il container di nginx e modificarlo, quindi: - "docker pull nginx" - "docker run -d --name "my_nginx" nginx" - verifichiamo con "curl < CONTAINER IP >" che il container ritorni la pagina di benvenuto di nginx - entriamo nel container con "docker exec -it my_nginx bash" - modifichiamo il file index.html con "echo "

my new container

" > /usr/share/nginx/html" il file di benvenuto - usciamo dalla shell e testiamo le modifiche con "curl < CONTAINER IP >" - creiamo il nuovo container con "docker commit my_nginx new_nginx" - stoppiamo e rimuoviamo il conainer attivo con "docker stop my_nginx", "docker rm -f my_nginx" - eseguiamo il nuovo container con "docker run -d --name "new_nginx_container" new_nginx" - testiamo il funzionamento con "curl < CONTAINER IP >" dockerfile fare tutto con la CLI non ci permette di tracciare tutti i cambiamenti che facciamo nei container, per questo per mantenere la tracciabilità si utilizzano i dockerfile, ovvero semplici file di testo con una propria sintassi che permette la definizione di una nuova immagine. una volta scritto il nostro file dockerfile possiamo utilizzare il comando "docker build -t imagename:tag ." nella directory dove abbiamo il dockerfile per creare un immagine a partire dal file. le principali istruzioni all'interno dei dockerfile sono: - FROM, ovvero l'unica istruzione che deve per forza esserci in un dockerfile, questa permette di specificare un'immagine di base per il nostro container da cui partire per personalizzarla. La scelta di un'immagine di base è una parte fodamentale. Possiamo riferirci ad un'immagine in tre modi: * FROM < nome_immagine > * FROM < nome_immagine >:< tag > * FROM < nome_immagine >@< hash > È possibile utilizzare un'immagine riservata vuota usando "FROM scratch". - #, ovvero l'istruzione per inserire commenti. - ENV, ovvero il comando per definire variabili d'ambiente valide per tutto il dockerfile. Per definirle si utilizza "ENV < chiave > < valore >" o "ENV < chiave >=< valore >" e per riferirci ad esse possiamo utilizzare il carattere "$(chiave)". Queste variabili saranno inoltre presenti nel container creato a partire dal dockerfile, per visualizzarle si può utilizzare "docker inspect" e per modificarle possiamo usare "docker inspect --env < chiave >=< valore >" - WORKDIR, ovvero l'istruzione che ci permette di impostare la directory di lavoro per qualsiasi altra istruzione, come RUN, CMD, ENTRYPOINT, ecc. che la segue nel dockerfile. sono supportati path relativi ed assoluti. per modificare la directory di default durante l'esecuzione di un container possiamo usare l'opzione "-w < directory >" - RUN, l'istruzione ci permette di eseguire comandi all'interno dell'immagine che andremo a generare. la sintassi è "RUN < comando > < parametro 1 > ... < parametro N >". È molto utile usare questa comando per installare pacchetti all'interno dei container. - ADD e COPY, queste istruzioni producono lo stesso risultato, ovvero spostare file e directory presenti sull'host nel file system del container. la sintassi è "ADD < src > < dest >", se abbiamo degli spazi nel path dobbiamo usare "ADD ["src", "dest"]". la differenza tra i due comandi è che COPY non supporta file remoti e archivi, mentre ADD si, nonostante questo la documentazione ufficiale consiglia l'uso di COPY. - LABEL, che ci permette di aggiungere metadati alla nostra immagine come coppia nome valore: "LABEL "< nome >"="< valore >"" in questo modo è possibile indicare informazioni utili legate alla definizione dell'immagine. Si può per esempio impostare il numero di versione o una descrizione dell'immagine. si possono poi visualizzare con il comando "docker inspect". - ENTRYPOINT, permette di eseguire un comando all'avvio del container, si ha la sintassi "ENTRYPOINT < comando > < parametro 1 > ... < parametro N >". la differenza con RUN è che in questo caso gli effetti saranno sul container, mentre con RUN gli effetti sono sull'immagine. se è presente questo comando il comando inserito durante il docker run verrà utilizzato come parametro e non eseguito. per sovrascriverlo possiamo usare il parametro "--entrypoint". - CMD, ovvero il comando per eseguire un comando nel container dopo l'avvio, come per RUN solo uno di questi comandi è ammesso. in questo caso il paramtro che inseriamo nel docker run sostituirà il comando nell'operazione CMD. - EXPOSE, con questa istruzione è possibile specificare su quali porte il container sarà in ascolto durante l'esecuzione. Questa istruzione non apre le porte del container ma fa il forwarding di quelle specificate. la sintassi è: "EXPOSE < porta 1 > [ < porta N > ]" di default le apre come porte TCP ma è possibile specificare il tipo di porta con la sintassi "< porta N/protocol >". Una volta specificato l'expose possiamo utilizzare il parametro "-P" per assegnare quelle porte a porte casuali della macchina, per sapere le porte aperte usiamo "docker port < porta esposta > < nome container >". - VOLUME, l'istruzione VOLUME ci permette di montare un volume nel container, la sintassi è: "VOLUME < path >", quando avviamo il container possiamo poi specificare a quale path locale far corrispondere il path del volume. building delle immagini il comando "docker build [ opt... ] < build context >", dove build context è solitamente il path in cui si trova il dockerfile, ci permette di creare un'immagine docker personalizzata. Il comando accetta anche una serie di parametri, come: - t (--tag), che ci permette di assegnare un nome all'immagine che vogliamo creare. - f (--file), che ci permette di specificare un path in cui è presente il docker file che vogliamo utilizzare nel caso non sia presente nel build context o il filename non sia "Dockerfile". esempio di build di un'applicazione nodejs creiamo una directory chiamata "node_container", al suo interno scriviamo un Dockerfile che contiene: "FROM node WORKDIR /usr/src/app COPY package*.json ./ RUN npm install COPY . . EXPOSE 3000 CMD npm start" scriviamo poi un breve file chiamato server.js: "const express = require("express"); const server = express(); const port = 3000; server.get("/", function(req, res){ res.send("Hello from my node container!"); }); server.listen(port, function(){ console.log("server listen on port " + port); });" installiamo i paccchetti e inizializziamo l'app, poi scriviamo un file .dockerignore: "node_modules" in modo da non copiare tutti i file dei moduli installati. lanciamo il comando "docker build -t my-node:1.0.0 ." verifichiamo che sia stata creata con "docker images" diamo il tag di latest: "docker tag my-node:1.0.0 my-node:latest" avviamo il container: "docker run -d -p 3000:3000 --name my-node-server my-node" verifichiamo che funzioni con: "curl localhost:3000" se volessimo creare una nuova versione, modifichiamo il file server.js, lanciamo: "docker build -t my-node:2.0.0 ." diamo il tag latest a quest'ultima: "docker tag my-node:2.0.0 my-node:latest" in questo modo avremo due immagini che coesistono con versione diversa e possiamo usarle entrambe. dockerfile multistage un dockerfile multistage è un dockerfile che contiene più istruzioni "FROM", in questo modo possiamo creare un container a partire da più immagini esistenti. DOCKER COMPOSE - cenni docker compose è un tool che ci permette di definire un'esecuzione di applicazioni multi-container, per questo viene detto "orchestratore". docker compose è utilizzato soprattutto per finalità di testing e non di produzione, al contrario di kubernetes, swarm, ecc., che possono gestire host multipli. Possiamo però usare docker compose per esecuzione di applicazioni che seguono uno stack, come XAMP o LAMP, ecc. Per eseguire un'applicazione multi-container occorre la stesura di un file chiamato "docker-compose.yml" e l'esecuzione del comando "docker-compose up". Questi file contengono istruzioni per: - lo start e stop dei servizi. - l'esame dello stato dei servizi. - l'ispezione dei log. - l'esecuzione di comandi nei container. Ovviamente un file docker compose può essere utilizzato anche per eseguire un singolo container. Per stoppare l'esecuzione di un docker compose usiamo: "docker-compose down".