Basi di Docker

Una piccola guida introduttiva a Docker, per conoscerne almeno le basi quando ci si trova davanti ad applicazioni distribuite sotto forma di container.

Basi di Docker
Foto di frank mckenna su Unsplash

Nell'ambito della certificazione PCSAE (Palo Alto Networks Certified Automation Engineer), è richiesta una conoscenza basilare di Docker per la gestione dei container.

Il corso presente sul portale Beacon ne offre una buona panoramica, tuttavia è preferibile approfondire un po' l'argomento usando anche altre fonti, come la chiara ed esauriente documentazione ufficiale presente su docs.docker.com oppure i numerosi tutorial gratuiti che si trovano su Youtube, come quello (introduttivo) dell'ottimo Learn Linux TV.

Leggendo la documentazione e seguendo diversi tutorial, ho compilato una piccola lista di comandi che ritengo utile conoscere per non doverli cercare su Internet ogni volta che se ne ha bisogno.

Installazione

Per quanto concerne l'installazione di Docker, la cosa migliore è seguire la documentazione ufficiale relativamente alla distribuzione in uso. Per quanto riguarda Ubuntu e derivate, i repository contengono già il pacchetto Docker ma, dal mio punto di vista, è preferibile installare la versione Community (o Docker Engine). Le istruzioni si trovano sulla pagina https://docs.docker.com/engine/install/.

Post installazione

Dopo aver installato Docker Engine sul sistema, è bene eseguire alcune operazioni di hardening e ottimizzazione.

Eseguire Docker come utente non-root

Dopo l'installazione, per invocare il comando docker ed eseguire i container è necessario agire come utente root; per poterlo fare anche come utente normale, bisogna aggiungere il proprio utente al gruppo docker e avviare una nuova sessione sul sistema.

sudo usermod -aG docker <utente>
# su sistemi Debian e derivati si può usare il comando seguente:
sudo adduser <utente> docker

Limitare le risorse disponibili per i container

Limitare le risorse utilizzabili dai container è una precauzione utile ad evitare che un particolare container le esaurisca, a scapito dell'host e di altri container.

Tra le risorse da "proteggere" ci sono la memoria, le CPU, il numero di processi che il container può allocare e il numero di file descriptor che può utilizzare.

Quando si avvia un container (docker run), si possono specificare i limiti oltre i quali il container non può andare indicandoli tramite la riga di comando:

docker run -m 500m --cpus 2 --pids-limit 20 --ulimit nofile=4096:4096 -p 8080:80 nginx:latest

Gli stessi limiti possono essere specificati indicandoli nel file docker-compose.yml, tipicamente usato per avviare un'applicazione "multi container", come nell'esempio seguente (in questo caso è presente un solo container):

services:
  web:
    image: nginx:latest
    deploy:
      resources:
        limits:
          cpus: '1.5'
          memory: 500M
        reservations:
          cpus: '0.5'
          memory: 200M
    ports:
      - "8080:80"

Docker compose non è argomento di questo post tuttavia, una volta creato il file docker-compose.yml, i container in esso definiti si avviano con il comando docker compose up.

Se si vogliono impostare limiti a livello globale, quindi per tutti i container eseguiti su uno specifico host, è necessario intervenire sul file di configurazione del servizio docker, che di default è /etc/docker/daemon.json.

Un esempio di configurazione potrebbe essere il seguente:

{
  "default-ulimits": {
    "nofile": {
      "Name": "nofile",
      "Hard": 4096,
      "Soft": 1024
    },
    "nproc": {
      "Name": "nproc",
      "Hard": 1024,
      "Soft": 512
    }
  },
  "default-shm-size": "1G",
  "default-runtime": "runc"
}

Quando si apportano modifiche a questo file, bisogna sempre riavviare il servizio docker affinché abbiano effetto: sudo systemctl restart docker.service

Per approfondire l'argomento dei limit delle risorse: https://noviello.it/dei-limiti-di-cpu-e-memoria-in-docker/

Comandi fondamentali

Di seguito riporto una breve lista dei comandi, e relative opzioni, che trovo utili quando si inizia a lavorare con Docker. La lista non è esaustiva ed è soggetta a periodiche modifiche e correzioni. Alcuni degli esempi sono presi dal già citato tutorial di LLTV.

  • docker images: elenca le immagini presenti sul sistema
  • docker search <keyword>: cerca le immagini contenenti la keywork specificata
  • docker pull <image-name>: scarica l'immagine specificata
  • docker run <image-name>: avvia un container che esegue l'immagine specificata
  • docker ps: mostra i container attualmente in esecuzione sul sistema
  • docker ps -a: mostra tutti i container presenti sul sistema, anche quelli non in esecuzione
  • docker rm <container-id>: rimuove un container
  • docker rmi <image-id>: rimuove un'immagine (non deve essere in uso da nessun container)
  • docker run -it <image-name> [command]: avvia un container con l'immagine specificata, in modalità interattiva ed eseguendo l'eventuale comando specificato
  • docker run -it -d <image-name> [command]: come sopra, ma esegue il container in modalità detached, quindi in background e restituendo il controllo della shell
  • docker attach <container-id>|<container-name>: permette di entrare nel container precedentemente avviato in background ed interagire con esso
  • CTRL+p+q: combinazione per uscire da un container senza fermarlo (mantenere premuto CTRL quando si preme la combinazione p + q)
  • docker run -it -d -p 8080:80 <image-name>: il flag -p esegue il binding della porta 8080 dell'host locale con la porta 80 del container avviato; in questo modo, accedendo alla porta 8080 dell'host locale si accede ad una eventuale applicazione in esecuzione sulla porta 80 del container
  • docker start <container-id>: avvia un container precedentemente fermato
  • docker stop <container-id>: arresta un container attualmente in esecuzione
  • docker run -it -d -p 8080:80 --restart unless-stopped <image-name>: l'opzione --restart specifica la politica di restart; se la politica è unless-stopped, il container verrà sempre riavviato quando esce, a meno che non sia stato esplicitamente fermato con docker stop
  • docker commit <container-id> <repo>/<image-name>:<version>: crea una nuova immagine partendo da un container in esecuzione; questa nuova immagine comprenderà le modifiche fatte al container su cui è basata
  • ENTRYPOINT: un entrypoint specifica quale comando deve essere eseguito dal container quando viene avviato; l'entrypoint è una lista contenente il comando e le sue opzioni. Esempio: ENTRYPOINT=["apachectl", "-DFOREGROUND"]. Questa modalità è necessaria perché, in linea di massima, nei container non è incluso un gestore dei servizi come systemd
  • docker build -t <username>/<imagename>:version <dir del Dockerfile>: un modo alternativo per creare un'immagine è quella di usare un Dockerfile, ovvero un file in cui vengono specificate le caratteristiche dell'immagine da creare, quali l'immagine base, l'entrypoint ed eventuali comandi da eseguire