Docker guida completa

🐳 Docker β€” Guida Completa

Di cosa parla questa guida

Docker Γ¨ la piattaforma di containerizzazione piΓΉ usata al mondo. Questa guida spiega cos'Γ¨, come funziona, come installarlo su Fedora/RHEL e Ubuntu, e include un laboratorio pratico per imparare facendo.


Indice


1. Cos'Γ¨ Docker e perchΓ© esiste

Il problema che Docker risolve

Prima di Docker esisteva un problema cronico nello sviluppo software, riassumibile in una frase che ogni sviluppatore ha detto o sentito almeno una volta:

"Sul mio computer funziona."

Il codice gira perfettamente sulla macchina dello sviluppatore, ma si rompe in produzione. PerchΓ©? Le cause sono sempre le stesse: versioni diverse di librerie, variabili d'ambiente diverse, sistemi operativi diversi, dipendenze mancanti o in conflitto.

La soluzione: il container

Docker risolve questo problema con il concetto di container: un pacchetto autonomo e isolato che contiene l'applicazione insieme a tutto ciΓ² di cui ha bisogno per girare β€” librerie, runtime, configurazioni, file di sistema.

Un container avviato su un portatile con Ubuntu si comporta esattamente allo stesso modo su un server con CentOS, su un Mac, su Windows o su qualsiasi cloud provider. L'ambiente Γ¨ sempre identico.

Container vs Macchina Virtuale

Spesso Docker viene confuso con la virtualizzazione. Sono tecnologie diverse con scopi diversi.

Macchina Virtuale Container Docker
Virtualizza Hardware completo Solo il sistema operativo
Include Kernel + OS completo Solo le librerie necessarie
Dimensione Gigabyte Megabyte
Avvio Minuti Secondi (o meno)
Isolamento Totale Parziale (condivide il kernel)
Overhead Alto Quasi zero

I container condividono il kernel del sistema operativo host. Non emulano hardware: usano quello reale. Per questo sono leggerissimi e veloci.

Quando usare Docker

Docker Γ¨ ideale per:


2. I concetti fondamentali

Capire questi quattro concetti Γ¨ capire Docker al 90%.

πŸ–ΌοΈ Immagine (Image)

Un'immagine è un modello di sola lettura da cui creare i container. È paragonabile a una fotografia o a un template: descrive il sistema operativo, le librerie installate, i file presenti e il comando da eseguire all'avvio.

Le immagini sono costruite a strati (layers). Ogni istruzione nel Dockerfile aggiunge un layer all'immagine. Questo sistema Γ¨ efficiente: se due immagini condividono gli stessi layer di base, quei layer vengono memorizzati una volta sola sul disco.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Layer 4: copia il codice   β”‚  ← tuo Dockerfile
β”‚  Layer 3: installa Node.js  β”‚  ← tuo Dockerfile
β”‚  Layer 2: aggiorna i pacch. β”‚  ← tuo Dockerfile
β”‚  Layer 1: ubuntu:22.04      β”‚  ← immagine base
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ“¦ Container

Un container Γ¨ un'istanza in esecuzione di un'immagine. Dall'immagine si creano i container, esattamente come da un programma si creano i processi.

Puoi avviare decine di container dalla stessa immagine: saranno tutti isolati tra loro, avranno il proprio filesystem, la propria rete, i propri processi. Modificare un container non modifica l'immagine da cui Γ¨ stato creato.

         Immagine nginx:latest
              β”‚
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β–Ό         β–Ό         β–Ό
Container 1  Container 2  Container 3
(porta 80)   (porta 81)   (porta 82)

πŸ“š Registry

Un registry Γ¨ un repository di immagini. Il registry ufficiale Γ¨ Docker Hub (hub.docker.com), dove trovi immagini ufficiali di praticamente qualsiasi software: nginx, postgres, redis, python, node, ubuntu, alpine e migliaia di altri.

# Le immagini hanno questo formato:
registry/utente/nome:tag

# Esempi:
ubuntu:22.04                    # immagine ufficiale Ubuntu
nginx:latest                    # immagine ufficiale Nginx
postgres:16                     # immagine ufficiale PostgreSQL
myuser/mia-app:v1.2.3           # immagine personale su Docker Hub
ghcr.io/org/repo:main           # GitHub Container Registry

πŸ“„ Dockerfile

Un Dockerfile è un file di testo con le istruzioni per costruire un'immagine. È la ricetta: parti da un'immagine base, aggiungi le tue personalizzazioni, definisci cosa deve fare il container all'avvio.

FROM ubuntu:22.04           # immagine di partenza
RUN apt update && apt install -y python3  # installa Python
COPY app.py /app/app.py     # copia il codice
CMD ["python3", "/app/app.py"]  # comando all'avvio

3. Architettura di Docker

Docker funziona con un'architettura client-server.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     Host Linux                       β”‚
β”‚                                                      β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚
β”‚  β”‚ Docker CLI  │◄──────►│    Docker Daemon          β”‚β”‚
β”‚  β”‚ (client)    β”‚  API   β”‚    (dockerd)              β”‚β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  REST  β”‚                          β”‚β”‚
β”‚                         β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚β”‚
β”‚                         β”‚  β”‚ Cont.1 β”‚ β”‚ Cont.2 β”‚   β”‚β”‚
β”‚                         β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚β”‚
β”‚                         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              β”‚
                              β–Ό
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚   Docker Hub     β”‚
                    β”‚  (o altro reg.)  β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Docker CLI β€” il comando docker che usi nel terminale. Invia comandi al daemon tramite API REST.

Docker Daemon (dockerd) β€” il processo server che gira in background. Gestisce immagini, container, reti e volumi. Su Linux comunica tramite il socket /var/run/docker.sock.

containerd β€” il runtime dei container, usato dal daemon per creare e gestire effettivamente i container usando le syscall del kernel Linux (namespaces, cgroups).

Le tecnologie kernel che rendono possibili i container

Docker non Γ¨ magia: usa funzionalitΓ  native del kernel Linux.

  • Namespaces β€” isolano i processi, la rete, il filesystem, gli utenti: ogni container "vede" solo se stesso
  • cgroups β€” limitano le risorse (CPU, RAM, I/O) assegnate a ogni container
  • Union filesystem (OverlayFS) β€” implementa il sistema a layer delle immagini

4. Installazione su Fedora e RHEL

Non usare il Docker nei repo di sistema

Fedora e RHEL includono docker nei propri repository, ma si tratta di Podman rinominato, non di Docker Engine originale. Per installare Docker Engine originale bisogna aggiungere il repository ufficiale di Docker.

4.1 Fedora (39, 40, 41, 42)

# Rimuovi eventuali versioni precedenti o conflittuali
sudo dnf remove -y docker \
    docker-client \
    docker-client-latest \
    docker-common \
    docker-latest \
    docker-latest-logrotate \
    docker-logrotate \
    docker-selinux \
    docker-engine-selinux \
    docker-engine

# Installa i prerequisiti
sudo dnf install -y dnf-plugins-core

# Aggiungi il repository ufficiale Docker
sudo dnf config-manager --add-repo \
    https://download.docker.com/linux/fedora/docker-ce.repo

# Installa Docker Engine, CLI e containerd
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

# Avvia il daemon e abilitalo all'avvio
sudo systemctl start docker
sudo systemctl enable docker

# Verifica che funzioni
sudo docker run hello-world

4.2 RHEL / CentOS Stream / Rocky Linux / AlmaLinux

# Rimuovi versioni precedenti
sudo dnf remove -y docker \
    docker-client \
    docker-client-latest \
    docker-common \
    docker-latest \
    docker-latest-logrotate \
    docker-logrotate \
    docker-engine

# Installa i prerequisiti
sudo dnf install -y yum-utils

# Aggiungi il repository ufficiale Docker per RHEL
sudo yum-config-manager --add-repo \
    https://download.docker.com/linux/rhel/docker-ce.repo

# Installa Docker Engine
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

# Avvia il daemon
sudo systemctl start docker
sudo systemctl enable docker

# Verifica
sudo docker run hello-world
SELinux e Docker su Fedora/RHEL

Docker Γ¨ compatibile con SELinux in modalitΓ  Enforcing. Se noti problemi di accesso ai volumi, aggiungi il suffisso :z o :Z ai mount dei volumi:

docker run -v /mio/path:/app:z nome-immagine
# :z = volume condiviso tra container (label shared)
# :Z = volume privato di questo container (label private)

4.3 Verifica dell'installazione su RHEL-based

# Versione di Docker
docker --version

# Informazioni complete sul daemon
docker info

# Versione dettagliata client e server
docker version

# Stato del servizio
systemctl status docker

# Il daemon Γ¨ in ascolto sul socket?
ls -la /var/run/docker.sock

5. Installazione su Ubuntu e Debian

5.1 Ubuntu (20.04, 22.04, 24.04)

# Rimuovi versioni precedenti
sudo apt remove -y docker docker-engine docker.io containerd runc

# Aggiorna la lista dei pacchetti
sudo apt update

# Installa i prerequisiti per HTTPS
sudo apt install -y \
    ca-certificates \
    curl \
    gnupg \
    lsb-release

# Aggiungi la chiave GPG ufficiale di Docker
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
    sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

# Aggiungi il repository Docker
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# Aggiorna e installa Docker Engine
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

# Avvia e abilita il daemon
sudo systemctl start docker
sudo systemctl enable docker

# Verifica
sudo docker run hello-world

5.2 Debian (11 Bullseye, 12 Bookworm)

# Rimuovi versioni precedenti
sudo apt remove -y docker docker-engine docker.io containerd runc

sudo apt update
sudo apt install -y ca-certificates curl gnupg

# Aggiungi la chiave GPG
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | \
    sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

# Aggiungi il repository
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/debian \
  $(lsb_release -cs) stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# Installa
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

sudo systemctl start docker
sudo systemctl enable docker
sudo docker run hello-world

6. Configurazione post-installazione

Questi passi sono uguali per tutte le distribuzioni e vanno fatti dopo l'installazione.

6.1 Usare Docker senza sudo

Per default il socket Docker Γ¨ accessibile solo da root. Per usare docker senza sudo bisogna aggiungere il proprio utente al gruppo docker.

# Aggiungi l'utente corrente al gruppo docker
sudo usermod -aG docker $USER

# Applica subito la modifica senza logout
newgrp docker

# Verifica (deve funzionare senza sudo)
docker run hello-world
Implicazioni di sicurezza

Il gruppo docker dΓ  accesso al socket del daemon, che gira come root. Un utente nel gruppo docker puΓ² di fatto ottenere privilegi root sul sistema host. Su server di produzione valuta se usare Rootless Docker o Podman (rootless by default).

6.2 Configurazione del daemon

Il daemon Docker si configura tramite /etc/docker/daemon.json.

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  },
  "storage-driver": "overlay2",
  "data-root": "/var/lib/docker",
  "dns": ["8.8.8.8", "1.1.1.1"]
}
# Crea o modifica il file di configurazione
sudo nano /etc/docker/daemon.json

# Riavvia il daemon per applicare le modifiche
sudo systemctl restart docker

# Verifica che le modifiche siano attive
docker info | grep -i "storage\|log\|data"

6.3 Proxy per Docker (ambienti aziendali)

# Crea la directory di override per il servizio
sudo mkdir -p /etc/systemd/system/docker.service.d/

# Crea il file di configurazione del proxy
sudo tee /etc/systemd/system/docker.service.d/http-proxy.conf << EOF
[Service]
Environment="HTTP_PROXY=http://proxy.azienda.it:8080"
Environment="HTTPS_PROXY=http://proxy.azienda.it:8080"
Environment="NO_PROXY=localhost,127.0.0.1,*.azienda.it"
EOF

sudo systemctl daemon-reload
sudo systemctl restart docker

7. I comandi essenziali

7.1 Immagini

# Scarica un'immagine dal registry
docker pull ubuntu:22.04
docker pull nginx:latest
docker pull postgres:16

# Elenca le immagini scaricate
docker images
docker image ls

# Cerca immagini su Docker Hub
docker search nginx

# Rimuovi un'immagine
docker rmi ubuntu:22.04
docker image rm ubuntu:22.04

# Rimuovi tutte le immagini non usate
docker image prune
docker image prune -a   # rimuove anche quelle non taggate

# Informazioni dettagliate su un'immagine (storia dei layer)
docker inspect ubuntu:22.04
docker history ubuntu:22.04

# Salva un'immagine su file
docker save nginx:latest -o nginx-backup.tar

# Carica un'immagine da file
docker load -i nginx-backup.tar

# Tagga un'immagine
docker tag nginx:latest mio-nginx:personalizzato

7.2 Container β€” avvio e gestione

# Avvia un container interattivo (entra dentro)
docker run -it ubuntu:22.04 bash

# Avvia un container in background (detached)
docker run -d nginx:latest

# Avvia con nome, porta e variabili d'ambiente
docker run -d \
    --name mio-nginx \
    -p 8080:80 \
    -e TZ=Europe/Rome \
    nginx:latest

# Avvia con limite di risorse
docker run -d \
    --name app-limitata \
    --cpus="0.5" \
    --memory="256m" \
    nginx:latest

# Avvia e rimuovi automaticamente quando termina
docker run --rm ubuntu:22.04 echo "ciao e arrivederci"

# Avvia con policy di riavvio automatico
docker run -d \
    --restart=always \
    --name sempre-attivo \
    nginx:latest

# Policy di restart disponibili:
# no            = non riavviare mai (default)
# on-failure    = riavvia solo se esce con errore
# always        = riavvia sempre, anche dopo reboot host
# unless-stopped = sempre, tranne se fermato manualmente

7.3 Container β€” monitoraggio

# Elenca i container in esecuzione
docker ps

# Elenca tutti i container (anche fermi)
docker ps -a

# Elenca solo gli ID dei container in esecuzione
docker ps -q

# Log di un container
docker logs mio-nginx
docker logs -f mio-nginx          # segui in tempo reale
docker logs --tail 50 mio-nginx   # ultime 50 righe
docker logs --since 10m mio-nginx # ultimi 10 minuti

# Statistiche di utilizzo risorse in tempo reale
docker stats
docker stats mio-nginx

# Processi in esecuzione dentro un container
docker top mio-nginx

# Ispeziona tutta la configurazione di un container
docker inspect mio-nginx

# Informazioni generali su Docker e il sistema
docker info
docker system df   # utilizzo disco di immagini, container, volumi

7.4 Container β€” interazione

# Entra in un container giΓ  in esecuzione
docker exec -it mio-nginx bash
docker exec -it mio-nginx sh      # se bash non Γ¨ disponibile

# Esegui un comando singolo senza entrare
docker exec mio-nginx ls /etc/nginx/
docker exec mio-nginx cat /etc/nginx/nginx.conf

# Copia file tra host e container
docker cp file.txt mio-nginx:/tmp/file.txt
docker cp mio-nginx:/etc/nginx/nginx.conf ./nginx.conf

# Ferma un container (SIGTERM, poi SIGKILL dopo 10s)
docker stop mio-nginx

# Ferma forzatamente (SIGKILL immediato)
docker kill mio-nginx

# Avvia un container fermato
docker start mio-nginx

# Riavvia un container
docker restart mio-nginx

# Metti in pausa (SIGSTOP)
docker pause mio-nginx
docker unpause mio-nginx

# Rimuovi un container fermato
docker rm mio-nginx

# Rimuovi forzatamente anche se in esecuzione
docker rm -f mio-nginx

# Rimuovi tutti i container fermati
docker container prune

7.5 Pulizia generale

# Rimuovi tutto il non utilizzato (container, reti, immagini dangling)
docker system prune

# Rimuovi tutto inclusi volumi e immagini non usate
docker system prune -a --volumes

# Pulizia selettiva
docker container prune   # container fermi
docker image prune -a    # immagini non usate
docker volume prune      # volumi non usati
docker network prune     # reti non usate

# Quanto spazio usa Docker?
docker system df
docker system df -v      # dettagliato

8. Dockerfile β€” costruire immagini custom

8.1 Istruzioni principali

# ─── Istruzione di base ───────────────────────────────────────
FROM ubuntu:22.04
# Specifica l'immagine di partenza. Ogni Dockerfile deve iniziare con FROM.
# Usa "FROM scratch" per immagini completamente vuote.

# ─── Metadati ────────────────────────────────────────────────
LABEL maintainer="mario@esempio.it"
LABEL version="1.0"
LABEL description="La mia applicazione"

# ─── Variabili d'ambiente ─────────────────────────────────────
ENV APP_HOME=/opt/app
ENV NODE_ENV=production
ENV PORT=3000
# Accessibili durante il build E a runtime nel container.

# ─── Argomenti di build ───────────────────────────────────────
ARG VERSIONE=1.0
# Come ENV ma solo durante il build, non a runtime.
# Passato con: docker build --build-arg VERSIONE=2.0 .

# ─── Directory di lavoro ──────────────────────────────────────
WORKDIR /opt/app
# Imposta la directory corrente per le istruzioni successive.
# La crea se non esiste.

# ─── Esegui comandi durante il build ──────────────────────────
RUN apt-get update && apt-get install -y \
    curl \
    git \
    python3 \
    && rm -rf /var/lib/apt/lists/*
# Ogni RUN crea un nuovo layer. Concatena i comandi con &&
# per minimizzare i layer e la dimensione dell'immagine.

# ─── Copia file nell'immagine ─────────────────────────────────
COPY . .
# Copia file dall'host (contesto di build) nell'immagine.

ADD archivio.tar.gz /opt/
# Come COPY ma decomprime archivi automaticamente.
# Preferisci sempre COPY salvo casi specifici.

# ─── Utente ───────────────────────────────────────────────────
RUN groupadd -r appgroup && useradd -r -g appgroup appuser
USER appuser
# Non girare mai come root nei container di produzione.

# ─── Porte ────────────────────────────────────────────────────
EXPOSE 3000
# Documenta la porta che l'app usa. Non la pubblica davvero
# (quello si fa con -p in docker run).

# ─── Volumi ───────────────────────────────────────────────────
VOLUME ["/opt/app/data"]
# Crea un punto di mount per i dati persistenti.

# ─── Healthcheck ──────────────────────────────────────────────
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
    CMD curl -f http://localhost:3000/health || exit 1

# ─── Comando di avvio ─────────────────────────────────────────
ENTRYPOINT ["node"]
# Il comando "fisso" del container. Non sovrascrivibile da docker run
# (solo con --entrypoint). Ideale per il binario principale.

CMD ["server.js"]
# Argomenti di default per ENTRYPOINT, o il comando se ENTRYPOINT
# non Γ¨ definito. Sovrascrivibile passando argomenti a docker run.

8.2 Esempio completo β€” App Node.js

# Build stage β€” compila l'applicazione
FROM node:20-alpine AS builder

WORKDIR /build

# Copia prima i file delle dipendenze per sfruttare la cache dei layer
COPY package*.json ./
RUN npm ci --only=production

# Poi copia il codice sorgente
COPY src/ ./src/

# ───────────────────────────────────────────────────────────────
# Production stage β€” immagine finale minimale
FROM node:20-alpine AS production

# Aggiorna i pacchetti di sistema per sicurezza
RUN apk update && apk upgrade && apk add --no-cache tini

# Crea utente non-root
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

WORKDIR /app

# Copia solo l'output del build stage (non il codice sorgente)
COPY --from=builder /build/node_modules ./node_modules
COPY --from=builder /build/src ./src
COPY package.json ./

# Cambia proprietario dei file
RUN chown -R appuser:appgroup /app

USER appuser

EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
    CMD wget -qO- http://localhost:3000/health || exit 1

# tini come PID 1 per gestire correttamente i segnali
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "src/server.js"]

8.3 Il file .dockerignore

Come .gitignore ma per il contesto di build. Riduce la dimensione del contesto e impedisce di copiare file inutili nell'immagine.

# .dockerignore
node_modules/
.git/
.gitignore
*.log
.env
.env.local
dist/
coverage/
.DS_Store
README.md
docker-compose*.yml
Dockerfile*

8.4 Comandi docker build

# Build base (cerca Dockerfile nella directory corrente)
docker build -t mia-app:1.0 .

# Build da Dockerfile con nome diverso
docker build -f Dockerfile.prod -t mia-app:prod .

# Build con argomenti
docker build --build-arg VERSIONE=2.0 -t mia-app:2.0 .

# Build con target stage specifico (multi-stage)
docker build --target builder -t mia-app:builder .

# Build senza cache (forza il rebuild di tutti i layer)
docker build --no-cache -t mia-app:latest .

# Build e push in un colpo solo
docker build -t miouser/mia-app:latest . && docker push miouser/mia-app:latest

# Ispeziona i layer e la storia dell'immagine
docker history mia-app:1.0

9. Docker Compose

Docker Compose permette di definire e avviare applicazioni multi-container con un singolo file YAML.

9.1 Struttura del file compose

# docker-compose.yml
version: "3.9"

services:

  # ─── Applicazione web ───────────────────────────────────────
  webapp:
    build:
      context: .
      dockerfile: Dockerfile
    image: mia-webapp:latest
    container_name: webapp
    restart: unless-stopped
    ports:
      - "8080:3000"
    environment:
      - NODE_ENV=production
      - DB_HOST=database
      - DB_PORT=5432
      - DB_NAME=mioapp
      - DB_USER=appuser
      - DB_PASSWORD=${DB_PASSWORD}   # da file .env
    volumes:
      - ./uploads:/app/uploads
      - ./logs:/app/logs
    depends_on:
      database:
        condition: service_healthy
      redis:
        condition: service_started
    networks:
      - app-network
    labels:
      - "com.esempio.service=webapp"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  # ─── Database PostgreSQL ─────────────────────────────────────
  database:
    image: postgres:16-alpine
    container_name: postgres
    restart: unless-stopped
    environment:
      POSTGRES_DB: mioapp
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres-data:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    ports:
      - "5432:5432"   # rimuovi in produzione
    networks:
      - app-network
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d mioapp"]
      interval: 10s
      timeout: 5s
      retries: 5

  # ─── Cache Redis ─────────────────────────────────────────────
  redis:
    image: redis:7-alpine
    container_name: redis
    restart: unless-stopped
    command: redis-server --requirepass ${REDIS_PASSWORD}
    volumes:
      - redis-data:/data
    networks:
      - app-network

  # ─── Reverse proxy Nginx ─────────────────────────────────────
  nginx:
    image: nginx:alpine
    container_name: nginx
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
    depends_on:
      - webapp
    networks:
      - app-network

# ─── Volumi persistenti ──────────────────────────────────────
volumes:
  postgres-data:
    driver: local
  redis-data:
    driver: local

# ─── Reti ────────────────────────────────────────────────────
networks:
  app-network:
    driver: bridge

9.2 Comandi Docker Compose

# Avvia tutti i servizi (build se necessario)
docker compose up

# Avvia in background
docker compose up -d

# Avvia solo un servizio specifico
docker compose up -d database

# Avvia ricostruendo le immagini
docker compose up -d --build

# Ferma tutti i servizi
docker compose stop

# Ferma e rimuovi container, reti (NON i volumi)
docker compose down

# Ferma, rimuovi container, reti E volumi
docker compose down -v

# Ferma, rimuovi E rimuovi le immagini
docker compose down --rmi all

# Stato dei servizi
docker compose ps

# Log di tutti i servizi
docker compose logs
docker compose logs -f            # segui in tempo reale
docker compose logs -f webapp     # solo un servizio

# Entra in un container
docker compose exec webapp bash
docker compose exec database psql -U appuser -d mioapp

# Esegui un comando una-tantum (crea un container temporaneo)
docker compose run --rm webapp npm run migrate

# Scala un servizio (piΓΉ istanze)
docker compose up -d --scale webapp=3

# Rebuild solo di un servizio
docker compose build webapp
docker compose up -d --no-deps --build webapp

10. Networking in Docker

10.1 Tipi di rete

# Elenca le reti esistenti
docker network ls

# Le reti di default:
# bridge   = rete isolata per container sullo stesso host
# host     = il container usa direttamente la rete dell'host
# none     = nessuna rete (container completamente isolato)

Bridge (default) β€” Ogni container ha il suo IP privato in una sottorete interna. I container sulla stessa rete bridge si parlano tramite IP o nome container.

Host β€” Il container condivide lo stack di rete dell'host. Nessun isolamento di rete, ma massime performance. Usato per applicazioni che richiedono accesso diretto alle interfacce di rete.

None β€” Il container non ha alcuna interfaccia di rete. Usato per task di elaborazione isolati.

10.2 Gestione delle reti

# Crea una rete bridge personalizzata
docker network create mia-rete

# Crea con subnet specifica
docker network create \
    --driver bridge \
    --subnet 172.20.0.0/16 \
    --ip-range 172.20.240.0/20 \
    mia-rete-custom

# Avvia un container collegato a una rete specifica
docker run -d --network mia-rete --name app nginx

# Collega un container in esecuzione a una rete
docker network connect mia-rete container-esistente

# Scollega un container da una rete
docker network disconnect mia-rete container-esistente

# Ispeziona una rete (vedi container collegati, subnet, ecc.)
docker network inspect mia-rete

# Rimuovi una rete
docker network rm mia-rete

# Rimuovi tutte le reti non usate
docker network prune
Risoluzione DNS tra container

Nelle reti bridge create dall'utente (non quella default), Docker fornisce un DNS interno: i container si raggiungono usando il nome del container come hostname. Ad esempio, se hai un container database, un altro container nella stessa rete puΓ² connettersi usando database:5432 come host.

10.3 Pubblicare porte

# Pubblica la porta 80 del container sulla porta 8080 dell'host
docker run -p 8080:80 nginx

# Pubblica su tutte le interfacce dell'host
docker run -p 0.0.0.0:8080:80 nginx

# Pubblica solo su localhost (piΓΉ sicuro)
docker run -p 127.0.0.1:8080:80 nginx

# Pubblica UDP
docker run -p 5353:5353/udp dns-server

# Pubblica su una porta casuale dell'host
docker run -p 80 nginx
docker port container-name   # scopri su quale porta Γ¨ finito

11. Volumi e persistenza dei dati

I container sono effimeri: quando un container viene rimosso, tutti i suoi dati vengono persi. I volumi risolvono questo problema.

11.1 Tipi di storage

Volume Docker β€” gestito direttamente da Docker, memorizzato in /var/lib/docker/volumes/. Raccomandato per la persistenza dei dati.

Bind Mount β€” monta una directory specifica dell'host nel container. Utile in sviluppo per condividere il codice sorgente.

tmpfs Mount β€” dati in memoria RAM, non persistiti sul disco. Utile per dati sensibili temporanei.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Host                              β”‚
β”‚                                                      β”‚
β”‚  /var/lib/docker/volumes/     /mio/progetto/        β”‚
β”‚           β”‚                        β”‚                 β”‚
β”‚     Volume Docker             Bind Mount             β”‚
β”‚           β”‚                        β”‚                 β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚              Container                          β”‚ β”‚
β”‚  β”‚  /app/data        /app/src                     β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

11.2 Gestione dei volumi

# Crea un volume
docker volume create miei-dati

# Crea un volume con driver specifico
docker volume create \
    --driver local \
    --opt type=nfs \
    --opt o=addr=nfs-server,rw \
    --opt device=:/path/to/data \
    volume-nfs

# Elenca i volumi
docker volume ls

# Ispeziona un volume
docker volume inspect miei-dati

# Rimuovi un volume
docker volume rm miei-dati

# Rimuovi tutti i volumi non usati
docker volume prune

# Avvia un container con un volume Docker
docker run -d \
    -v miei-dati:/app/data \
    --name app \
    mia-app

# Avvia con un bind mount (directory dell'host)
docker run -d \
    -v /home/utente/progetto:/app \
    --name dev-app \
    mia-app

# Bind mount in sola lettura
docker run -d \
    -v /etc/config:/app/config:ro \
    mia-app

# Avvia con tmpfs (in memoria)
docker run -d \
    --tmpfs /app/temp:size=100m \
    mia-app

11.3 Backup e ripristino di volumi

# Backup di un volume in un file tar
docker run --rm \
    -v miei-dati:/source:ro \
    -v $(pwd):/backup \
    ubuntu \
    tar czf /backup/backup-$(date +%Y%m%d).tar.gz -C /source .

# Ripristino da backup
docker run --rm \
    -v miei-dati:/target \
    -v $(pwd):/backup \
    ubuntu \
    tar xzf /backup/backup-20260601.tar.gz -C /target

12. πŸ§ͺ Laboratorio pratico

Come usare questo laboratorio

Gli esercizi sono progressivi. Ogni esercizio si basa sul precedente. Esegui i comandi, osserva l'output, poi rispondi mentalmente alla domanda. Le soluzioni sono in callout chiudibili.


Esercizio 1 β€” Il tuo primo container

Obiettivo: capire il ciclo di vita base di un container.

# Passo 1: scarica l'immagine di Ubuntu
docker pull ubuntu:22.04

# Passo 2: avvia un container interattivo
docker run -it --name lab-ubuntu ubuntu:22.04 bash

# Dentro il container, esegui:
cat /etc/os-release
hostname
ps aux
ls /
exit

# Passo 3: guarda i container (anche quelli fermati)
docker ps -a

# Passo 4: riavvia il container e rientra
docker start lab-ubuntu
docker exec -it lab-ubuntu bash

# Dentro: crea un file
echo "ciao da Docker" > /tmp/test.txt
cat /tmp/test.txt
exit

# Passo 5: rimuovi il container
docker rm -f lab-ubuntu

# Passo 6: avvia di nuovo un container ubuntu
docker run -it --rm ubuntu:22.04 bash
# Cerca /tmp/test.txt β€” esiste ancora?
ls /tmp/
Cosa hai imparato

  • I container sono isolati dal sistema host
  • I dati dentro un container non persistono dopo la sua rimozione
  • docker exec permette di entrare in un container giΓ  in esecuzione
  • --rm rimuove automaticamente il container quando termina


Esercizio 2 β€” Nginx come web server

Obiettivo: esporre un servizio web, mappare porte, servire contenuto custom.

# Passo 1: avvia Nginx sulla porta 8080
docker run -d --name lab-nginx -p 8080:80 nginx:latest

# Passo 2: verifica che funzioni
curl http://localhost:8080
# oppure apri il browser su http://localhost:8080

# Passo 3: guarda i log in tempo reale
docker logs -f lab-nginx
# (esegui il curl di nuovo in un altro terminale)

# Passo 4: ispeziona il container
docker inspect lab-nginx | grep -A 10 "IPAddress"

# Passo 5: crea una pagina HTML custom
mkdir -p ~/docker-lab/html
cat > ~/docker-lab/html/index.html << 'EOF'
<!DOCTYPE html>
<html>
<body style="font-family: sans-serif; text-align: center; padding: 50px">
  <h1>🐳 Il mio primo container Nginx!</h1>
  <p>Servito da Docker β€” funziona!</p>
</body>
</html>
EOF

# Passo 6: rimuovi il container e ricrealo con bind mount
docker rm -f lab-nginx
docker run -d \
    --name lab-nginx \
    -p 8080:80 \
    -v ~/docker-lab/html:/usr/share/nginx/html:ro \
    nginx:latest

# Passo 7: verifica la pagina custom
curl http://localhost:8080

# Passo 8: modifica il file HTML sull'host
echo "<p>Aggiornato!</p>" >> ~/docker-lab/html/index.html
curl http://localhost:8080   # la modifica Γ¨ immediata, senza restart

# Pulizia
docker rm -f lab-nginx
Cosa hai imparato

  • Come pubblicare porte con -p host:container
  • Come servire file dall'host con bind mount -v
  • Le modifiche ai bind mount sono immediate nel container


Esercizio 3 β€” Costruire un'immagine personalizzata

Obiettivo: scrivere un Dockerfile e costruire la tua prima immagine.

# Passo 1: crea la directory del progetto
mkdir -p ~/docker-lab/app
cd ~/docker-lab/app

# Passo 2: crea l'applicazione Python
cat > app.py << 'EOF'
from http.server import HTTPServer, BaseHTTPRequestHandler
import json
import datetime

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header('Content-type', 'application/json')
        self.end_headers()
        response = {
            "messaggio": "Ciao da Docker!",
            "ora": str(datetime.datetime.now()),
            "path": self.path
        }
        self.wfile.write(json.dumps(response, indent=2).encode())

    def log_message(self, format, *args):
        print(f"[{datetime.datetime.now()}] {args[0]} {args[1]} {args[2]}")

print("Server avviato su porta 8000...")
HTTPServer(('0.0.0.0', 8000), Handler).serve_forever()
EOF

# Passo 3: crea il Dockerfile
cat > Dockerfile << 'EOF'
FROM python:3.12-slim

WORKDIR /app

COPY app.py .

EXPOSE 8000

CMD ["python3", "app.py"]
EOF

# Passo 4: crea il .dockerignore
cat > .dockerignore << 'EOF'
__pycache__/
*.pyc
.git/
EOF

# Passo 5: costruisci l'immagine
docker build -t mia-app-python:1.0 .

# Passo 6: guarda l'immagine creata
docker images mia-app-python

# Passo 7: guarda i layer
docker history mia-app-python:1.0

# Passo 8: avvia il container
docker run -d --name lab-python -p 8000:8000 mia-app-python:1.0

# Passo 9: testa l'applicazione
curl http://localhost:8000
curl http://localhost:8000/hello
curl http://localhost:8000/docker-lab

# Passo 10: guarda i log
docker logs lab-python

# Pulizia
docker rm -f lab-python
Cosa hai imparato

  • Come scrivere un Dockerfile base
  • Come costruire un'immagine con docker build
  • Come l'immagine Γ¨ composta da layer
  • Come avviare e testare un container dalla tua immagine


Esercizio 4 β€” Stack completo con Docker Compose

Obiettivo: mettere in piedi uno stack WordPress + MySQL con Compose.

# Passo 1: crea la directory
mkdir -p ~/docker-lab/wordpress
cd ~/docker-lab/wordpress

# Passo 2: crea il file .env con le password
cat > .env << 'EOF'
MYSQL_ROOT_PASSWORD=rootpassword123
MYSQL_DATABASE=wordpress
MYSQL_USER=wpuser
MYSQL_PASSWORD=wppassword123
EOF

# Passo 3: crea il docker-compose.yml
cat > docker-compose.yml << 'EOF'
version: "3.9"

services:

  database:
    image: mysql:8.0
    container_name: wp-mysql
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    volumes:
      - mysql-data:/var/lib/mysql
    networks:
      - wp-network
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

  wordpress:
    image: wordpress:latest
    container_name: wp-app
    restart: unless-stopped
    ports:
      - "8080:80"
    environment:
      WORDPRESS_DB_HOST: database:3306
      WORDPRESS_DB_NAME: ${MYSQL_DATABASE}
      WORDPRESS_DB_USER: ${MYSQL_USER}
      WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD}
    volumes:
      - wp-content:/var/www/html/wp-content
    depends_on:
      database:
        condition: service_healthy
    networks:
      - wp-network

volumes:
  mysql-data:
  wp-content:

networks:
  wp-network:
    driver: bridge
EOF

# Passo 4: avvia lo stack
docker compose up -d

# Passo 5: monitora l'avvio
docker compose logs -f

# Passo 6: aspetta che sia tutto up
docker compose ps

# Passo 7: quando tutto Γ¨ "healthy", apri il browser
echo "Apri http://localhost:8080 per installare WordPress"

# Passo 8: entra nel database e verifica
docker compose exec database mysql -u wpuser -pwppassword123 wordpress -e "SHOW TABLES;"

# Passo 9: scala il servizio wordpress (prova β€” serve load balancer per funzionare bene)
docker compose up -d --scale wordpress=2

# Passo 10: ferma tutto (i dati rimangono nei volumi)
docker compose stop

# Passo 11: riavvia — i dati sono ancora lì
docker compose start

# Pulizia completa (rimuove anche i volumi!)
docker compose down -v
Cosa hai imparato

  • Come orchestrare piΓΉ container con un singolo file
  • Come usare variabili d'ambiente da file .env
  • Come le dipendenze tra servizi si definiscono con depends_on
  • Come i volumi Docker persistono i dati tra restart
  • Come scalare un servizio


Esercizio 5 β€” Debugging e troubleshooting

Obiettivo: imparare a diagnosticare e risolvere problemi nei container.

# ─── Scenario 1: container che non parte ─────────────────────

# Crea un container con un errore intenzionale
docker run -d --name broken-app \
    -e DB_HOST=non-esiste \
    postgres:16

# Diagnosi
docker ps -a                         # stato del container
docker logs broken-app               # leggi i log
docker inspect broken-app | grep -A5 '"State"'

# ─── Scenario 2: container che si mangia la RAM ───────────────

# Avvia un container senza limiti
docker run -d --name hungry-app nginx:latest

# Monitora le risorse
docker stats hungry-app --no-stream  # snapshot
docker stats hungry-app              # live

# Riavvia con limiti di risorse
docker rm -f hungry-app
docker run -d --name hungry-app \
    --memory="128m" \
    --cpus="0.25" \
    nginx:latest

docker stats hungry-app --no-stream  # vedi i limiti applicati

# ─── Scenario 3: container senza shell ───────────────────────

# Alcune immagini minimali non hanno bash
docker run -d --name no-shell alpine:latest sleep 1000

# ash Γ¨ disponibile su Alpine
docker exec -it no-shell ash

# Se proprio non c'Γ¨ shell, usa nsenter
PID=$(docker inspect --format '{{.State.Pid}}' no-shell)
sudo nsenter -t $PID -m -u -i -n -p -- sh

# ─── Scenario 4: ispezionare i layer di un'immagine ──────────

docker history nginx:latest
docker inspect nginx:latest | python3 -m json.tool | head -50

# ─── Scenario 5: file system del container ───────────────────

# Vedi le differenze tra il filesystem del container e l'immagine base
docker run -d --name diff-test nginx:latest
# Crea qualche file dentro
docker exec diff-test touch /tmp/nuovo-file.txt
docker exec diff-test bash -c "echo test > /etc/test.conf"

# Vedi cosa Γ¨ cambiato
docker diff diff-test
# A = Added, C = Changed, D = Deleted

# ─── Scenario 6: esportare e importare un container ──────────

# Esporta il filesystem del container (snapshot)
docker export diff-test -o snapshot.tar

# Importa come nuova immagine
docker import snapshot.tar mio-snapshot:latest
docker run --rm mio-snapshot:latest ls /tmp/

# Pulizia
docker rm -f broken-app hungry-app no-shell diff-test
rm -f snapshot.tar
Cosa hai imparato

  • Come leggere i log e diagnosticare container che non partono
  • Come monitorare e limitare l'uso di risorse
  • Come ispezionare il filesystem di un container con docker diff
  • Come lavorare con container che non hanno shell
  • Come esportare/importare lo stato di un container


Esercizio 6 β€” Rete tra container

Obiettivo: capire come i container comunicano tra loro.

# Passo 1: crea una rete custom
docker network create lab-network

# Passo 2: avvia un server Redis
docker run -d \
    --name lab-redis \
    --network lab-network \
    redis:7-alpine

# Passo 3: avvia un client e testa la comunicazione
docker run -it --rm \
    --network lab-network \
    redis:7-alpine \
    redis-cli -h lab-redis ping
# Deve rispondere: PONG

# Passo 4: scrivi e leggi dati su Redis
docker run -it --rm \
    --network lab-network \
    redis:7-alpine \
    redis-cli -h lab-redis SET chiave "valore dal container"

docker run -it --rm \
    --network lab-network \
    redis:7-alpine \
    redis-cli -h lab-redis GET chiave

# Passo 5: dimostra l'isolamento tra reti diverse
docker run -d --name fuori-rete nginx:latest  # nessuna rete custom

docker run -it --rm \
    --network lab-network \
    redis:7-alpine \
    redis-cli -h fuori-rete ping
# Deve fallire: non risolve il nome

# Passo 6: collega il container alla rete (a posteriori)
docker network connect lab-network fuori-rete

docker run -it --rm \
    --network lab-network \
    redis:7-alpine \
    ping -c 3 fuori-rete
# Ora funziona

# Passo 7: ispeziona la rete
docker network inspect lab-network

# Pulizia
docker rm -f lab-redis fuori-rete
docker network rm lab-network
Cosa hai imparato

  • I container sullo stesso network si raggiungono per nome
  • Container su reti diverse sono isolati tra loro
  • Si puΓ² connettere un container a una rete anche dopo l'avvio


Checklist finale del laboratorio

Verifica le tue competenze

Dopo aver completato tutti gli esercizi dovresti essere in grado di:


Riferimenti rapidi

# ─── IMMAGINI ────────────────────────────────────────────────
docker pull IMAGE          # scarica immagine
docker images              # elenca immagini
docker build -t TAG .      # costruisci immagine
docker rmi IMAGE           # rimuovi immagine
docker image prune -a      # rimuovi immagini non usate

# ─── CONTAINER ───────────────────────────────────────────────
docker run -d -p H:C --name N IMAGE   # avvia in background
docker run -it --rm IMAGE bash         # interattivo e usa-e-getta
docker ps -a               # elenca tutti i container
docker logs -f NAME        # log in tempo reale
docker exec -it NAME bash  # entra in container
docker stop/start/rm NAME  # ferma, avvia, rimuovi
docker stats               # utilizzo risorse live
docker inspect NAME        # info complete

# ─── COMPOSE ─────────────────────────────────────────────────
docker compose up -d       # avvia stack in background
docker compose down -v     # ferma e rimuovi tutto
docker compose logs -f     # log di tutti i servizi
docker compose exec S bash # entra nel servizio S
docker compose ps          # stato dei servizi

# ─── SISTEMA ─────────────────────────────────────────────────
docker system df           # utilizzo disco
docker system prune -a     # pulizia completa
docker network ls          # elenca reti
docker volume ls           # elenca volumi

Prossimi passi

Dopo aver padroneggiato Docker Engine, i temi successivi naturali sono:

  • Docker Swarm β€” orchestrazione multi-host nativa di Docker
  • Kubernetes β€” orchestrazione enterprise per container in produzione
  • Podman β€” alternativa rootless e daemonless a Docker
  • Buildah/Skopeo β€” strumenti avanzati per build e gestione immagini
  • Harbor β€” registry privato self-hosted