La guida a Docker che avresti dovuto leggere prima di dire "ma sulla mia macchina funzionava".
Da "cos'è un container" a "perché non usavo Docker prima?"
Alla fine Docker non è poi così terribile, basta che qualcuno te lo spieghi senza il tono da "senior architect che ha fatto 14 certificazioni".
"Funziona sulla mia macchina" — Il problema che Docker risolve.
Immagina di mandare una pizza per posta. Senza Docker mandi la ricetta e speri che dall'altra parte abbiano gli stessi ingredienti, lo stesso forno, la stessa acqua. Con Docker mandi la pizza già fatta dentro una scatola standard. Funziona ovunque: sul tuo PC, su un server, nel cloud, sulla luna. La scatola è il container.
Un ambiente isolato e leggero che contiene la tua app con TUTTO quello che le serve: codice, runtime, librerie, configurazione. È come una VM ma senza il peso di un intero sistema operativo.
La piattaforma che crea, distribuisce e esegue i container. Non è l'unica (c'è Podman, containerd) ma è lo standard de facto. Quello che tutti usano e tutti conoscono.
"Installa Python 3.9, poi pip install, poi configura Postgres, poi... ah aspetta, tu hai Python 3.11 e rompe tutto. E su Linux serve una lib diversa. E il collega usa Windows e..."
docker compose up e tutto funziona. Su Mac, Linux, Windows, CI/CD, produzione. Stesso ambiente. Sempre. Fine della discussione.
Immagine, Container, Layer: le 3 parole che devi sapere
Dockerfile = la ricetta della pizza.
Image = la pizza surgelata (pronta, immutabile, distribuibile).
Container = la pizza nel forno (in esecuzione, viva).
Registry = il supermercato dove prendi le pizze surgelate (Docker Hub).
Da una image puoi creare N container. Come stampare copie di un libro.
File di testo con le istruzioni per creare un'immagine. FROM, COPY, RUN, CMD.
Template read-only a layer. Ogni istruzione crea un layer. I layer sono cachati.
Un'istanza di un'image in esecuzione. Ha il suo filesystem, rete, processi.
Ogni istruzione Dockerfile = 1 layer. I layer sono condivisi tra immagini. Risparmi spazio.
Storage persistente. Senza volume, i dati muoiono col container.
Rete virtuale tra container. Si parlano per nome, come magia.
3 minuti e hai Docker
# macOS
brew install --cask docker
# Apri Docker Desktop dall'applicazione, aspetta che la balena sia ferma
# Linux (Docker Engine - senza GUI)
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER # così non serve sudo ogni volta
newgrp docker # applica subito
# Verifica
docker --version
Docker version 27.x.x, build abc123
# Il test definitivo
docker run hello-world
Hello from Docker!
This message shows that your installation appears to be working correctly.
Da zero a "ho un container che gira" in 5 minuti
# Scarica e avvia nginx
docker run -d --name mio-web -p 8080:80 nginx
# -d = detached (in background)
# --name = dagli un nome (sennò Docker inventa nomi assurdi)
# -p 8080:80 = porta 8080 del tuo PC → porta 80 del container
# Apri http://localhost:8080 🎉 Nginx funziona!
# Container attivi
docker ps
CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES
a1b2c3d4e5f6 nginx "/docker-entrypoint" Up 2 min 0.0.0.0:8080->80/tcp mio-web
# Tutti i container (anche quelli stoppati)
docker ps -a
# Log del container
docker logs mio-web
docker logs -f mio-web # -f = follow (live)
# Entra dentro il container
docker exec -it mio-web /bin/bash
# Ferma
docker stop mio-web
# Riavvia
docker start mio-web
# Cancella (deve essere stoppato)
docker rm mio-web
# Ferma E cancella in un colpo
docker rm -f mio-web
# Cancella TUTTI i container stoppati
docker container prune
# Vedi le immagini scaricate
docker images
# Cancella un'immagine
docker rmi nginx
# Pulizia totale (container, immagini, network, cache)
docker system prune -a
# ⚠️ Cancella TUTTO quello che non è in uso. Usalo con giudizio.
La ricetta per creare le tue immagini personalizzate
# Parti da un'immagine base (il "sistema operativo")
FROM node:20-alpine # Alpine = versione leggera (~50MB vs ~1GB)
# Cartella di lavoro dentro il container
WORKDIR /app
# Copia PRIMA solo package.json (per sfruttare la cache dei layer)
COPY package*.json ./
# Installa le dipendenze (questo layer viene cachato!)
RUN npm ci --only=production
# ORA copia il resto del codice
COPY . .
# Esponi la porta (documentazione, non apre davvero)
EXPOSE 3000
# Utente non-root (sicurezza!)
USER node
# Comando di avvio
CMD ["node", "server.js"]
# Costruisci l'immagine (il . è il contesto, la cartella con il Dockerfile)
docker build -t mia-app:1.0 .
# -t = tag (nome:versione)
# Eseguila
docker run -d --name mia-app -p 3000:3000 mia-app:1.0
# Vedi i layer e la dimensione
docker history mia-app:1.0
Compila in un container grosso, copia solo il risultato in uno piccolo. Immagine finale minuscola.
# Stage 1: Compila
FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o server .
# Stage 2: Immagine finale (solo il binario!)
FROM alpine:3.19
COPY --from=builder /app/server /server
EXPOSE 8080
CMD ["/server"]
# Risultato: immagine da ~15MB invece di ~1GB 🚀
node_modules
.git
.env
.env.*
*.md
.vscode
.idea
docker-compose*.yml
Dockerfile*
.DS_Store
Il vero game changer. Un file per domarli tutti.
Se Docker è uno strumento musicale, Docker Compose è lo spartito dell'orchestra. Un file YAML che dice: "fammi partire l'app, il database, Redis e Nginx tutti insieme, collegati tra loro, con i volumi giusti". Un comando: docker compose up. Fatto.
# docker-compose.yml (o compose.yml, entrambi funzionano)
services:
# La tua app
app:
build: . # Usa il Dockerfile nella cartella corrente
ports:
- "3000:3000" # host:container
environment:
- DATABASE_URL=postgres://user:pass@db:5432/mydb
- REDIS_URL=redis://cache:6379
depends_on: # Parte DOPO db e cache
- db
- cache
volumes:
- ./src:/app/src # Hot reload! Modifichi il codice e si aggiorna
restart: unless-stopped # Si riavvia da solo se crasha
# PostgreSQL
db:
image: postgres:16-alpine
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=mydb
volumes:
- db-data:/var/lib/postgresql/data # Dati persistenti!
ports:
- "5432:5432" # Opzionale: per connetterti da fuori
# Redis (cache)
cache:
image: redis:7-alpine
ports:
- "6379:6379"
# Volumi con nome (persistono tra i restart)
volumes:
db-data:
# Avvia tutto (build se necessario)
docker compose up -d # -d = background
# Avvia e forza rebuild delle immagini
docker compose up -d --build
# Ferma tutto
docker compose down
# Ferma e CANCELLA i volumi (⚠️ cancella i dati del DB!)
docker compose down -v
# Vedi i log di tutti i servizi
docker compose logs -f
# Log di un servizio specifico
docker compose logs -f app
# Stato dei servizi
docker compose ps
# Esegui un comando in un servizio
docker compose exec app /bin/sh
docker compose exec db psql -U user mydb
# Riavvia un singolo servizio
docker compose restart app
# Scala un servizio (più istanze)
docker compose up -d --scale app=3
Non mettere le password nel compose file. Usa un .env.
POSTGRES_USER=admin
POSTGRES_PASSWORD=super_segreta_123
POSTGRES_DB=produzione
APP_PORT=3000
services:
db:
image: postgres:16-alpine
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
app:
ports:
- "${APP_PORT}:3000"
docker compose up e hai l'intero stack locale. Un docker compose down e sparisce tutto senza lasciare traccia. Il sogno di ogni svogliato.
Come i container si parlano tra loro
# Vedi le reti
docker network ls
# Crea una rete custom
docker network create mia-rete
# Collega un container a una rete
docker run -d --name app --network mia-rete nginx
# Ispeziona una rete (chi c'è connesso)
docker network inspect mia-rete
# In Docker Compose non serve fare niente:
# i container si vedono per NOME del servizio automaticamente
# app raggiunge db con "db:5432", cache con "cache:6379"
Perché senza volume i dati spariscono come i tuoi weekend
Il container ha un filesystem effimero. Quando lo cancelli, tutto sparisce. Database, file uploadati, log. Tutto. Puff.
I dati vivono fuori dal container, in un volume Docker o in una cartella dell'host. Cancelli il container? I dati restano.
# 1. VOLUME (gestito da Docker - CONSIGLIATO per dati)
docker run -d -v db-data:/var/lib/postgresql/data postgres
# Docker gestisce dove salvare. Performante, portabile.
# 2. BIND MOUNT (cartella dell'host - CONSIGLIATO per sviluppo)
docker run -d -v $(pwd)/src:/app/src mia-app
# Monta la cartella src del tuo PC dentro il container.
# Modifichi un file sul PC → si aggiorna nel container. Hot reload!
# 3. TMPFS (solo in memoria - per dati temporanei)
docker run -d --tmpfs /tmp mia-app
# Comandi utili
docker volume ls # lista volumi
docker volume inspect db-data # dettagli
docker volume rm db-data # cancella
docker volume prune # cancella tutti quelli non in uso
docker compose down -v cancella i volumi. Se il database è li dentro, hai appena fatto DROP DATABASE senza volerlo. Usa docker compose down (senza -v) per fermare senza perdere dati.
Come condividere le tue immagini col mondo (o col server di produzione)
# 1. Login
docker login
# 2. Tagga l'immagine con il tuo username
docker tag mia-app:1.0 tuoutente/mia-app:1.0
# 3. Pusha
docker push tuoutente/mia-app:1.0
# 4. Da un altro PC, chiunque può scaricarla
docker pull tuoutente/mia-app:1.0
# GitHub Container Registry
docker login ghcr.io
docker tag mia-app:1.0 ghcr.io/tuoutente/mia-app:1.0
docker push ghcr.io/tuoutente/mia-app:1.0
# GitLab Container Registry
docker login registry.gitlab.com
docker tag mia-app:1.0 registry.gitlab.com/tuogruppo/tuorepo:1.0
docker push registry.gitlab.com/tuogruppo/tuorepo:1.0
# AWS ECR / Google GCR / Azure ACR: stessa logica, login diverso
mia-app:1.2.3 o mia-app:abc123 con l'hash del commit). Così sai esattamente cosa gira e puoi fare rollback.
Le regole per non farsi male quando fa sul serio
USER nel Dockerfiledocker scout cves mia-app--read-only dove possibile--memory=512m --cpus=1HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
services:
app:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 3s
retries: 3
start_period: 10s
# La tua app deve loggare su STDOUT/STDERR, non su file
# Docker li cattura automaticamente
# Vedi i log
docker logs mia-app
docker logs --since 1h mia-app # ultima ora
docker logs --tail 100 mia-app # ultime 100 righe
# Limita la dimensione dei log (in compose)
services:
app:
logging:
driver: json-file
options:
max-size: "10m" # Max 10MB per file di log
max-file: "3" # Max 3 file (rotazione)
Quando il container non parte e vuoi tornare a fare il pastore
Il processo principale crasha. Il container muore con lui.
# Guarda i log
docker logs nome-container
# Avvia in modo interattivo per debug
docker run -it mia-app /bin/sh
# Ora sei dentro e puoi capire cosa non va
# Controlla l'exit code
docker inspect nome-container --format='{{.State.ExitCode}}'
# 137 = OOM Killed (poca RAM)
# 1 = errore generico dell'app
# 126 = permesso negato sull'entrypoint
Qualcos'altro sta già usando quella porta.
# Chi usa la porta 3000?
lsof -i :3000 # macOS/Linux
netstat -tlnp | grep 3000 # Linux
# Opzione 1: uccidi il processo
kill <PID>
# Opzione 2: usa una porta diversa
docker run -p 3001:3000 mia-app
Docker mangia disco come un adolescente al buffet. Immagini, container stoppati, volumi orfani, cache di build.
# Quanto spazio usa Docker?
docker system df
# Pulizia conservativa (solo roba non in uso)
docker system prune
# Pulizia nucleare (TUTTO quello non in uso, incluse immagini)
docker system prune -a --volumes
# Sono sulla stessa rete?
docker network inspect <nome-rete>
# Stai usando "localhost"? SBAGLIATO!
# Tra container, usa il NOME del servizio come host
# ❌ http://localhost:5432
# ✅ http://db:5432
# Test di connessione dall'interno
docker exec app ping db
docker exec app wget -qO- http://db:5432
L'ordine delle istruzioni nel Dockerfile conta. Se cambi qualcosa nei primi layer, TUTTO quello dopo viene ricostruito.
# ❌ SBAGLIATO: copia tutto prima, poi installa
COPY . .
RUN npm install # Riesegue OGNI volta che cambi un file
# ✅ GIUSTO: copia prima le dipendenze, poi il codice
COPY package*.json ./
RUN npm ci # Riesegue SOLO se cambiano le dipendenze
COPY . . # Il codice cambia spesso ma npm ci è cachato!
I file creati dal container hanno UID 0 (root) sull'host. Classico problema con bind mount su Linux.
# Nel Dockerfile, crea un utente con lo stesso UID
RUN addgroup -g 1000 app && adduser -u 1000 -G app -s /bin/sh -D app
USER app
# Oppure nel docker-compose.yml
services:
app:
user: "1000:1000"
Il foglietto che salva la giornata
| Cosa | Comando |
|---|---|
| Avvia container | docker run -d --name X -p 8080:80 image |
| Lista container attivi | docker ps |
| Lista tutti | docker ps -a |
| Log | docker logs -f X |
| Shell dentro | docker exec -it X /bin/sh |
| Ferma | docker stop X |
| Riavvia | docker restart X |
| Cancella | docker rm -f X |
| Statistiche live | docker stats |
| Cosa | Comando |
|---|---|
| Build | docker build -t nome:tag . |
| Lista immagini | docker images |
| Scarica | docker pull image:tag |
| Pusha | docker push user/image:tag |
| Tagga | docker tag image:old user/image:new |
| Cancella | docker rmi image |
| Ispeziona layer | docker history image |
| Scansiona CVE | docker scout cves image |
| Cosa | Comando |
|---|---|
| Avvia tutto | docker compose up -d |
| Avvia + rebuild | docker compose up -d --build |
| Ferma tutto | docker compose down |
| Ferma + cancella volumi | docker compose down -v |
| Log | docker compose logs -f |
| Stato | docker compose ps |
| Shell in un servizio | docker compose exec app sh |
| Scala | docker compose up -d --scale app=3 |
| Cosa | Comando |
|---|---|
| Spazio usato | docker system df |
| Pulizia soft | docker system prune |
| Pulizia totale | docker system prune -a --volumes |
| Cancella container stoppati | docker container prune |
| Cancella immagini dangling | docker image prune |
| Cancella volumi orfani | docker volume prune |
alias d=docker
alias dc='docker compose'
alias dcu='docker compose up -d'
alias dcd='docker compose down'
alias dcl='docker compose logs -f'
alias dps='docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"'
alias dlog='docker logs -f'
alias dex='docker exec -it'
alias dnuke='docker system prune -a --volumes' # il pulsante rosso
Un processo per container. Non fare il tuttofare.
Docker Compose per tutto. Anche per un solo container.
Tag specifici, mai :latest. In produzione vuoi sapere cosa gira.
MAI root nei container. Neanche "per fare una prova veloce".
Volumi per i dati. Senza volume, i dati non esistono.
docker system prune una volta a settimana. Il tuo disco ti ringrazierà.