DevOps

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.

14 min di letturaBaseGuidaDi Riccardo Sciortino
Indice
  1. Introduzione
  2. Cos'è un container (e cosa non è una VM)
  3. Container
  4. Macchina virtuale
  5. Installazione: Desktop o engine, documentazione ufficiale
  6. Il primo container: hello-world
  7. Vedere, leggere, fermare, togliere
  8. Porte: `-p` host e container
  9. Volumi: l'idea, non il trattato
  10. Un Dockerfile minimo che parte
  11. `.dockerignore`: non mandare tutto nel contesto
  12. Cosa non fare (soprattutto "dopo il tutorial")
  13. Compose: un yaml, un servizio web, niente di strano
  14. Problemi comuni
  15. "Cannot connect to the Docker daemon"
  16. "port is already allocated"
  17. "l'app in locale va, nel container no"
  18. "l'immagine pesa un giga"
  19. "funziona sul mio PC"
  20. 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, latest in 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. --privileged e 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 lavoriCosa installareDocumentazione
Windows o macOS, sviluppo in localeDocker Desktopdocs.docker.com/desktop
Linux (Ubuntu, Debian, Fedora…)Motore Docker (engine) + plugin Composedocs.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:

  1. docker run chiede un'immagine
  2. se non c'è in locale, la scarica
  3. crea un container (filesystem scrivibile sopra l'immagine)
  4. avvia il processo definito dall'immagine
  5. 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 rm toglie un container. I dati dentro il container (non in un volume) se ne vanno

  • docker image ls elenca le immagini. Occupano disco

  • docker image rm hello-world se non ti serve più

  • Evita docker system prune -a finché 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:8080 viene inoltrato a 80 nel 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. Non node:latest
  • prima copi package.json e 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 -p
  • USER node: il processo non gira come root nell'immagine ufficiale Node
  • CMD in 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 registry

  • Root per abitudine: molte immagini partono come root. USER non-root quando l'app lo consente. Non è paranoia: è il raggio di esplosione se l'app viene bucata

  • Tag latest in produzione

    : oggi è nginx stabile, domani è un major. Pina 1.27.3 o almeno 1.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 host per pigrizia. Salta l'isolamento di rete
  • non tenere dati solo nel container e chiamarlo backup
  • non fare docker run a 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 latest anonimo
  • restart: unless-stopped tiene 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):

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.