Docker per principianti: il primo container senza paura
Primo container senza paura: differenza con le VM, installazione, hello-world, porte, volumi, Dockerfile, Compose e gli errori da non portare in produzione.
Indice
- Introduzione
- Cos'è un container (e cosa non è una VM)
- Container
- Macchina virtuale
- Installazione: Desktop o engine, documentazione ufficiale
- Il primo container: hello-world
- Vedere, leggere, fermare, togliere
- Porte: `-p` host e container
- Volumi: l'idea, non il trattato
- Un Dockerfile minimo che parte
- `.dockerignore`: non mandare tutto nel contesto
- Cosa non fare (soprattutto "dopo il tutorial")
- Compose: un yaml, un servizio web, niente di strano
- Problemi comuni
- "Cannot connect to the Docker daemon"
- "port is already allocated"
- "l'app in locale va, nel container no"
- "l'immagine pesa un giga"
- "funziona sul mio PC"
- Conclusione
Introduzione
Docker serve a far girare un'applicazione con le sue dipendenze in una scatola isolata: il container. Non è magia e non sostituisce "il server". È un modo ripetibile per dire: questa app, questa versione di runtime, queste porte, questi file.
Se hai provato a installare Node, PHP o un database "sul PC" e dopo tre mesi non sai più cosa c'è sopra, il container è la risposta pratica. Stesso comando sul portatile e su un VPS. Stesso risultato, se l'immagine è fatta bene.
In questa guida parti da zero:
- container contro macchina virtuale, senza folklore
- dove installare Docker (Desktop o engine su Linux) e dove leggere i passi ufficiali
hello-world,ps,logs,stop,rm- porte (
-p) e volumi, a livello di idea - un Dockerfile minimo che parte davvero, più
.dockerignore - cosa non fare: segreti nell'immagine, root per abitudine,
latestin produzione - un Compose con un solo servizio web, niente di pericoloso
Ti serve un PC su cui puoi installare software, o una macchina Linux. Se Linux lo stai ancora scegliendo, parti da Iniziare a usare Linux e torna qui dopo.
Cos'è un container (e cosa non è una VM)
Una macchina virtuale emula un computer: kernel proprio, init, spesso un desktop. Pesa giga. Ci metti minuti ad avviarla. Isola bene, perché sotto c'è un hypervisor.
Un container condivide il kernel dell'host. Isola processi, file e rete con i meccanismi del sistema (namespace, cgroup). L'immagine porta filesystem e programmi. Non porta un altro Windows o un altro kernel Linux. Per questo parte in secondi e occupa mega, non decine di giga.
Container
- Stesso kernel dell'host
- Avvio rapido, poco disco
- Immagine = app + runtime
- Ideale per un processo (web, worker)
Macchina virtuale
- Kernel e sistema propri
- Più isolamento, più peso
- Utile per un altro OS o per laboratori
- Non è il sostituto automatico di Docker
Conseguenze pratiche:
- su Windows e macOS, Docker Desktop alza comunque una piccola VM Linux. I container girano lì dentro. È normale
- un container non è "più sicuro per definizione". È isolato quanto lo configuri.
--privilegede socket Docker esposto annullano il vantaggio - non ci metti dentro "tutto l'ufficio". Ci metti un processo. Database a parte, se ti serve persistenza vera
La VM virtualizza una macchina. Il container impacchetta un processo. Se ti serve Windows su Linux, è una VM. Se ti serve la stessa app ovunque, è un container.
Installazione: Desktop o engine, documentazione ufficiale
Non incollare uno script apt lungo copiato da un blog del 2019. I repository cambiano. I passi ufficiali no, o almeno si aggiornano in un posto solo.
Due strade:
| Dove lavori | Cosa installare | Documentazione |
|---|---|---|
| Windows o macOS, sviluppo in locale | Docker Desktop | docs.docker.com/desktop |
| Linux (Ubuntu, Debian, Fedora…) | Motore Docker (engine) + plugin Compose | docs.docker.com/engine/install |
Su Linux, segui la pagina della tua distribuzione. Aggiungi l'utente al gruppo docker solo se capisci cosa significa: quel utente diventa equivalente a root sull'host, perché può montare dischi e uscire dal container. Molti si fermano a sudo docker ed è lecito.
Docker Desktop su Windows richiede WSL2 o Hyper-V, a seconda della setup. Se l'installazione fallisce, il log dell'installer e la pagina ufficiale valgono più di un forum. Non disinstallare antivirus a caso per "farlo partire".
Verifica quando hai finito:
docker version
docker compose version
Se docker version risponde con un numero di client e di server, l'engine è su. Se il server manca, il demone non gira: avvia Desktop, o sudo systemctl start docker su Linux.
Il primo container: hello-world
Il rituale esiste per un motivo: scarica un'immagine, crea un container, stampa un messaggio, esce. Se questo fallisce, non ha senso costruire un Dockerfile.
docker run hello-world
La prima volta Docker tira l'immagine hello-world dal registro (Docker Hub, di default). Poi avvia un container, scrive poche righe ("Hello from Docker!") e termina. Il container resta sul disco in stato exited, finché non lo cancelli.
Cosa sta succedendo, in ordine:
docker runchiede un'immagine- se non c'è in locale, la scarica
- crea un container (filesystem scrivibile sopra l'immagine)
- avvia il processo definito dall'immagine
- quando il processo muore, il container si ferma. Non si cancella da solo, salvo
--rm
Se vedi il messaggio di hello-world, installazione e registro funzionano. I prossimi comandi sono solo controllo e pulizia.
Vedere, leggere, fermare, togliere
Questi quattro comandi ti bastano per non accumulare spazzatura e per capire perché "non risponde".
docker ps
docker ps -a
docker ps elenca i container in esecuzione. docker ps -a include quelli fermi. Colonne utili: CONTAINER ID, IMAGE, STATUS, PORTS, NAMES.
Per un processo che resta su (non hello-world), i log sono la prima diagnosi:
docker logs nome-o-id
docker logs -f nome-o-id
-f segue l'output, come tail -f. Esci con Ctrl+C: interrompi solo il follow, non il container.
Fermare e cancellare:
docker stop nome-o-id
docker rm nome-o-id
stop manda un segnale di chiusura e aspetta. rm elimina il container (non l'immagine). Se è ancora in esecuzione, rm rifiuta: prima stop, o docker rm -f se sai di volerlo uccidere.
Pulizia minima, senza troppa foga
docker rmtoglie un container. I dati dentro il container (non in un volume) se ne vannodocker image lselenca le immagini. Occupano discodocker image rm hello-worldse non ti serve piùEvita
docker system prune -afinché non sai cosa stai cancellando: toglie immagini non usate, anche quelle che ti servono stasera
Un esempio che resta acceso abbastanza da poterlo fermare tu:
docker run --name prova-nginx -d nginx:1.27-alpine
docker ps
docker logs prova-nginx
docker stop prova-nginx
docker rm prova-nginx
-d è detached: torna al prompt. --name ti evita di copiare l'id. L'immagine ha un tag di versione (1.27-alpine), non latest. Ci torniamo.
Porte: -p host e container
Il processo nel container ascolta su una porta interna. Il tuo browser parla con localhost sull'host. Senza -p, le due cose non si toccano.
docker run --rm --name web-prova -p 8080:80 nginx:1.27-alpine
Apri http://localhost:8080. Dovresti vedere la pagina di default di nginx.
Lettura di -p 8080:80:
8080è la porta sull'host (il tuo PC)80è la porta dentro il container (nginx ascolta lì)- il traffico verso
localhost:8080viene inoltrato a80nel container
--rm cancella il container quando lo fermi (Ctrl+C in questo caso, perché non è -d). Utile per le prove. Non usarlo se ti servono i log dopo l'uscita.
Se 8080 è già occupata, Docker rifiuta l'avvio. Cambia la porta host:
-p 8081:80. La porta nel container resta 80, perché è lì che
ascolta nginx.
In produzione non esporre "tutte le porte" per comodità. Pubblica solo quella del servizio. 0.0.0.0:8080 è raggiungibile dalla rete, non solo da te: su un VPS è un'apertura vera. Un firewall davanti non è optional.
Volumi: l'idea, non il trattato
Il filesystem del container è effimero. docker rm e spesso anche un rebuild: i file scritti dentro se ne vanno. Un database che tiene i dati solo nel container è un incidente in attesa.
Un volume è una directory che vive fuori dal ciclo di vita del container. Due forme che incontri subito:
- volume nominato: Docker lo gestisce (
docker volume create dati). Comodo per un database in prova - bind mount: una cartella tua, es.
./sito:/usr/share/nginx/html. Comodo per sviluppare: editi il file, il container lo vede
Concettualmente:
docker run --rm -p 8080:80 -v "${PWD}/public:/usr/share/nginx/html:ro" nginx:1.27-alpine
Su PowerShell la variabile della directory corrente è diversa (${PWD} in bash, (Get-Location) in PowerShell). L'idea resta: monti public in sola lettura sulla root di nginx. Se la cartella public ha un index.html, lo servi. :ro evita che nginx (o un bug) riscriva i tuoi file.
Codice e dati non sono la stessa cosa. L'immagine versione il codice. Il volume tiene i dati. Se li inverti, o perdi il lavoro o non sai più cosa stai distribuendo.
Non montare l'intero disco di sistema "per comodità". Non montare ~/.ssh in un'immagine che poi pubblichi. Il mount è potente: è anche il modo più semplice per far uscire un container dal recinto.
Un Dockerfile minimo che parte
Un'immagine la puoi usare già fatta (nginx, hello-world). Quando è la tua app, scrivi un Dockerfile: la ricetta. Docker la esegue in layer. Il risultato è un'immagine tua.
Esempio Node che risponde in chiaro. Tre file, stessa cartella.
package.json:
{
"name": "ciao-container",
"private": true,
"version": "1.0.0",
"main": "server.js",
"scripts": {
"start": "node server.js"
}
}
server.js:
const http = require('http');
const port = process.env.PORT || 3000;
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('Ciao dal container\n');
});
server.listen(port, '0.0.0.0', () => {
console.log(`In ascolto su ${port}`);
});
Dockerfile:
FROM node:22-alpine
WORKDIR /app
COPY package.json ./
RUN npm install --omit=dev
COPY server.js ./
EXPOSE 3000
USER node
CMD ["node", "server.js"]
Perché è fatto così:
FROM node:22-alpine: runtime pinato, immagine piccola. Nonnode:latest- prima copi
package.jsone installi: il layer delle dipendenze si riusa se il JS cambia e il package no EXPOSE 3000è documentazione. Non pubblica la porta da solo. Serve ancora-pUSER node: il processo non gira come root nell'immagine ufficiale NodeCMDin forma exec (["node", "server.js"]): niente shell di mezzo, segnali più puliti
Build e avvio:
docker build -t ciao-container:1.0 .
docker run --rm -p 3000:3000 -e PORT=3000 ciao-container:1.0
Apri http://localhost:3000. Devi vedere Ciao dal container. Se non vedi nulla, docker logs sul container.
L'ascolto su 0.0.0.0 nel server non è uno sfizio. Se ascolti solo
127.0.0.1 dentro il container, il mapping delle porte
dall'host non arriva all'app. È un errore classico.
Variante statica, se Node non ti interessa: un public/index.html e
FROM nginx:1.27-alpine
COPY public /usr/share/nginx/html
EXPOSE 80
Stessa logica: versione pinata, un solo compito, niente segreti copiati.
.dockerignore: non mandare tutto nel contesto
docker build manda alla daemon una cartella (il contesto), di solito .. Senza ignore, ci finiscono node_modules, .git, .env, dump, chiavi. Rallenta la build. A volte copia i segreti nell'immagine.
.dockerignore nella stessa cartella del Dockerfile:
node_modules
.git
.gitignore
*.md
.env
.env.*
.DS_Store
npm-debug.log
Non è un .gitignore duplicato per moda. È il filtro di cosa può finire in un layer. Un .env con la connection string del database non deve comparire in COPY . .. Se usi COPY . . (qui abbiamo copiato file per nome, è più stretto), l'ignore è l'ultima rete.
Se un segreto è finito in un layer, cancellare il file nel commit dopo non basta. L'immagine vecchia e la cache lo tengono. Ruota il segreto. Ricostruisci senza cache. Non ripubblicare quel tag.
Cosa non fare (soprattutto "dopo il tutorial")
Il primo container è innocuo. I vizi presi il primo giorno arrivano in produzione.
Tre abitudini da non portare avanti
Segreti nell'immagine: password in
ENV,COPY .env, token nel codice. Usa variabili a runtime, un secret del orchestrator, o un file montato che non committi. L'immagine si spinge, si copia, finisce su un registryRoot per abitudine: molte immagini partono come root.
USERnon-root quando l'app lo consente. Non è paranoia: è il raggio di esplosione se l'app viene bucataTag
latestin produzione: oggi è nginx stabile, domani è un major. Pina
1.27.3o almeno1.27-alpine. Registra quale digest hai deployato
Altri no, in breve:
- non esporre il socket Docker (
/var/run/docker.sock) a un container "perché così gestisco Docker da Portainer" senza sapere che è root sull'host - non usare
--network hostper pigrizia. Salta l'isolamento di rete - non tenere dati solo nel container e chiamarlo backup
- non fare
docker runa mano su un VPS e chiamarlo "pipeline". Va bene per imparare. Poi fai Compose o una build ripetibile
Compose: un yaml, un servizio web, niente di strano
A mano, build + run + nome + porte diventano un copione. Compose è quel copione in un file. Un servizio. Niente database esposto su 0.0.0.0, niente privileged, niente stack da "microservizi" copiato da internet.
compose.yaml accanto al Dockerfile Node di sopra:
services:
web:
build: .
image: ciao-container:1.0
ports:
- '3000:3000'
environment:
PORT: '3000'
restart: unless-stopped
Comandi:
docker compose up --build
docker compose up -d --build
docker compose logs -f web
docker compose down
up crea la rete di default del progetto e avvia web. -d stacca. down ferma e toglie i container del progetto. I volumi nominati, se li aggiungi dopo, down di default non li cancella: è voluto.
Perché questo yaml è accettabile per imparare:
- un solo processo
- porta esplicita
- tag di immagine tuo, non
latestanonimo restart: unless-stoppedtiene su il servizio dopo un reboot, senza rinascere se lo hai fermato tu- niente
privileged, niente mount del socket, niente password nel file
Quando aggiungerai un database, il volume nominato e le credenziali non committate saranno il passo successivo. Non copiare un Compose con phpMyAdmin aperto al mondo "perché è comodo".
Compose non è Kubernetes. È un file per un host. Per un sito piccolo o un tool interno è spesso abbastanza. Non aggiungere orchestrator per sentirti al passo.
Problemi comuni
"Cannot connect to the Docker daemon"
Il client c'è, il demone no. Su Desktop: avvia l'applicazione e aspetta che sia Ready. Su Linux: servizio docker attivo, e o sudo o utente nel gruppo (poi nuovo login). WSL: integrazioni Desktop abilitate per quella distro.
"port is already allocated"
Qualcosa sull'host usa già quella porta. docker ps per vedere se è un container tuo. Cambia -p, o ferma l'altro processo. Non è un bug di nginx.
"l'app in locale va, nel container no"
Percorsi, variabili d'ambiente, ascolto su 127.0.0.1, database su localhost (dentro il container localhost è il container, non il PC). I log dicono il 80% di queste storie. Confronta PORT, HOST, connection string.
"l'immagine pesa un giga"
node:22 full non è node:22-alpine. COPY . . senza dockerignore porta node_modules dell'host. Multi-stage build è il passo dopo, non il primo. Inizia da alpine + ignore.
"funziona sul mio PC"
L'immagine dovrebbe essere la fonte di verità. Se "funziona" solo perché hai Node 18 in giro e nel Dockerfile c'è 22, non hai ancora vinto. Prova docker compose up --build su una macchina pulita, o almeno senza fare affidamento ai tool dell'host.
Hai chiuso il giro se: hello-world ok, un container con -p
risponde nel browser, il Dockerfile Node (o nginx) builda, Compose alza e
abbatte lo stesso servizio.
Conclusione
Un container è un processo impacchettato, non una VM in miniatura. Installi Docker dalla documentazione ufficiale, verifichi con hello-world, impari ps / logs / stop / rm, pubblichi una porta, tieni i dati fuori dall'immagine. Il Dockerfile pinato e il .dockerignore sono la differenza tra una prova e qualcosa che puoi mostrare a un collega. Compose fissa i flag in un yaml. I tre no (segreti, root pigro, latest) evitano il ticket di domenica.
Se ti manca una macchina Linux decente per far girare l'engine, la guida a Linux è il pre-requisito. Docker non risolve un host tenuto male. Lo rende solo più ripetibile. Se ti serve qualcuno che imposti Docker o Compose su un server reale, c'è gestione container.
Risorse utili (autorevoli):
- Docker Docs — riferimento ufficiale
- Get started — primi passi e concetti
- Install Docker Engine — Linux, per distribuzione
- Docker Desktop — Windows e macOS
- Dockerfile reference — istruzioni e semantica
- Compose specification — file e comandi
docker compose
Approfondimenti
- Gestione container — servizio
- Iniziare a usare Linux: guida completa per principianti — articolo
- Self-hosting: quando conviene (e quando no) — articolo
- Espressioni cron — strumento
- Docker — tecnologia
Parliamone
Questo articolo riguarda la tua azienda?
Se uno dei problemi descritti riguarda anche i tuoi sistemi, possiamo valutare insieme la situazione. Il primo confronto è gratuito.