Docker ha rivoluzionato il mondo dello sviluppo software, permettendo di creare ambienti isolati e riproducibili. Il Dockerfile è il cuore di questo processo: un file di testo che contiene tutte le istruzioni necessarie per costruire un’immagine Docker.
Cos’è un Dockerfile
Un Dockerfile è un file di testo che contiene una serie di istruzioni che Docker utilizza per costruire automaticamente un’immagine.
Ogni istruzione nel Dockerfile crea un nuovo layer nell’immagine, permettendo un sistema di cache intelligente e build incrementali.
Struttura base di un Dockerfile
Un Dockerfile tipico segue questa struttura:
# Immagine base
FROM ubuntu:22.04
# Informazioni sul maintainer
LABEL maintainer="[email protected]"
# Variabili d'ambiente
ENV NODE_VERSION=18.17.0
# Creazione directory di lavoro
WORKDIR /app
# Aggiornamento del sistema e installazione dipendenze
RUN apt-get update && apt-get install -y \
curl \
git \
&& rm -rf /var/lib/apt/lists/*
# Copia dei file
COPY package*.json ./
RUN npm install
COPY . .
# Porta esposta
EXPOSE 3000
# Comando di avvio
CMD ["npm", "start"]
Principali istruzioni Dockerfile
FROM
Specifica l’immagine base da cui partire. Deve essere sempre la prima istruzione.
FROM node:18-alpine
FROM ubuntu:22.04
FROM python:3.11-slim
LABEL
La direttiva LABEL
in un Dockerfile serve per aggiungere metadati al tuo container - praticamente delle “etichette” informative che non influenzano il funzionamento dell’applicazione ma forniscono informazioni utili.
È come mettere un’etichetta adesiva su una scatola per descrivere cosa contiene.
ENV
La direttiva ENV
in un Dockerfile serve a impostare variabili d’ambiente che saranno disponibili sia durante la costruzione dell’immagine che quando il container viene eseguito.
ENV NODE_VERSION=18.17.0
WORKDIR
Imposta la directory di lavoro per le istruzioni successive.
WORKDIR /app
RUN
Esegue comandi durante la build dell’immagine.
RUN apt-get update && apt-get install -y curl
RUN npm install
COPY e ADD
Entrambe copiano file dall’host al container, ma hanno differenze importanti:
COPY - Copia semplice di file e directory:
COPY package.json ./
COPY src/ ./src/
ADD - Come COPY ma con funzionalità aggiuntive:
- Estrae automaticamente archivi compressi (tar, gzip, bzip2, xz)
- Può scaricare file da URL remoti
# Estrae automaticamente l'archivio
ADD app.tar.gz /app/
# Scarica da URL (sconsigliato)
ADD https://example.com/file.tar.gz /tmp/
Regola generale: usa sempre COPY a meno che non ti servano le funzionalità extra di ADD.
EXPOSE
Documenta quale porta l’applicazione utilizza.
EXPOSE 3000
CMD e ENTRYPOINT
Entrambi definiscono il comando da eseguire all’avvio del container, ma con comportamenti diversi:
CMD - Comando predefinito modificabile:
- Fornisce il comando di default per il container
- Può essere sovrascritto facilmente da linea di comando
- Se presente più volte, solo l’ultimo ha effetto
CMD ["python", "app.py"]
# Effettivamente il container lancierà 'python app.py'
# In caso di sovrascrittura con docker run il comando verrà COMPLETAMENTE sostituito
ENTRYPOINT - Comando fisso:
- Definisce il comando principale che sarà sempre eseguito
- Non può essere sovrascritto, solo integrato
- Parametri aggiuntivi vengono aggiunti al comando
ENTRYPOINT ["python", "app.py"]
# Effettivamente il container lancierà 'python app.py'
# In caso di sovrascrittura con docker run verrà aggiunto il parametro passato
# docker run <image name> --help
# Effettivamente il container lancierà 'python app.py --help'
Combinazione CMD + ENTRYPOINT:
ENTRYPOINT ["python", "app.py"]
CMD ["--port", "3000"]
# Risultato: python app.py --port 3000
# Con parametri: docker run myapp --debug
# Risultato: python app.py --debug
Regola generale: usa CMD per comandi semplici, ENTRYPOINT quando vuoi un comportamento più rigido.
Best Practice per Dockerfile efficaci
1. Utilizza immagini base leggere
Preferisci sempre immagini base minimal come Alpine Linux per ridurre le dimensioni e la superficie di attacco.
# Buona pratica
FROM node:18-alpine
# Evita
FROM node:18
2. Sfrutta la cache dei layer
Ordina le istruzioni dalla meno frequentemente modificata alla più frequentemente modificata.
# Buona pratica - prima le dipendenze, poi il codice
COPY package*.json ./
RUN npm install
COPY . .
# Evita - invalida la cache ad ogni cambio
COPY . .
RUN npm install
3. Combina comandi RUN
Riduci il numero di layer combinando comandi correlati.
# Buona pratica
RUN apt-get update && \
apt-get install -y \
curl \
git \
vim && \
rm -rf /var/lib/apt/lists/*
# Evita
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git
4. Utilizza .dockerignore
Crea un file .dockerignore per escludere file non necessari.
node_modules
.git
.gitignore
README.md
.env
.nyc_output
coverage
.env.local
5. Crea utenti non-root
Mai eseguire applicazioni come root per motivi di sicurezza.
# Crea un utente non-privilegiato
RUN groupadd -r appuser && useradd -r -g appuser appuser
# Imposta la proprietà dei file
RUN chown -R appuser:appuser /app
USER appuser
6. Gestisci le variabili d’ambiente
Comprendere la differenza tra ENV e ARG è fondamentale:
ARG - Variabili disponibili solo durante la build:
- Esistono solo durante la costruzione dell’immagine
- Non sono visibili nel container finale
- Possono essere passate con
docker build --build-arg
ARG NODE_VERSION=18
ARG BUILD_DATE
FROM node:${NODE_VERSION}-alpine
RUN echo "Build date: ${BUILD_DATE}"
ENV - Variabili d’ambiente persistenti:
- Disponibili sia durante la build che nel container finale
- Visibili quando il container è in esecuzione
- Possono essere sovrascritte con
docker run -e
ENV NODE_ENV=production
ENV PORT=3000
ENV DATABASE_URL=postgresql://localhost/myapp
Esempio combinato:
# Variabile di build
ARG NODE_VERSION=18
FROM node:${NODE_VERSION}-alpine
# Variabili d'ambiente persistenti
ENV NODE_ENV=production
ENV PORT=3000
Esempio completo: Applicazione Node.js
Ecco un esempio pratico che applica le best practice principali:
# Usa immagine base leggera
FROM node:18-alpine
# Crea directory di lavoro
WORKDIR /app
# Crea utente non-root
RUN addgroup -g 1001 -S nodejs && \
adduser -S appuser -u 1001 -G nodejs
# Copia prima i file package per sfruttare la cache
COPY package*.json ./
# Installa dipendenze e pulisci cache
RUN npm ci --only=production && \
npm cache clean --force
# Copia il resto del codice
COPY . .
# Cambia proprietario dei file
RUN chown -R appuser:nodejs /app
# Cambia utente
USER appuser
# Variabili d'ambiente
ENV NODE_ENV=production
ENV PORT=3000
# Esponi la porta
EXPOSE 3000
# Comando di avvio
CMD ["node", "server.js"]
Errori comuni da evitare
1. Installare pacchetti non necessari
# Evita
RUN apt-get install -y build-essential python3 curl wget vim nano
# Installa solo quello che serve
RUN apt-get install -y curl
2. Non specificare versioni
# Evita
FROM node:latest
# Specifica sempre la versione
FROM node:18.17.0-alpine
3. Copiare file sensibili
# Evita
COPY . .
# Usa .dockerignore o copia selettivamente
COPY src/ ./src/
COPY package*.json ./
4. Usare ADD invece di COPY
# Evita (a meno che non serva l'auto-extraction)
ADD app.tar.gz /app/
# Usa COPY per operazioni semplici
COPY app.js /app/
Conclusione
Scrivere un Dockerfile efficace richiede attenzione ai dettagli e conoscenza delle best practice. Ricorda i punti chiave:
- Usa immagini base leggere
- Sfrutta la cache dei layer
- Combina comandi RUN correlati
- Implementa multi-stage build
- Crea utenti non-root
- Usa .dockerignore
- Aggiungi health check
Seguendo queste pratiche otterrai immagini Docker più sicure, performanti e mantenibili. La containerizzazione non è solo una tecnologia, ma un approccio che richiede disciplina e attenzione ai dettagli per ottenere i migliori risultati.
Nelle prossime guide approfondiremo la scelta delle immagini base più appropriate per diversi casi d’uso, analizzando le differenze tra Ubuntu, Alpine, Debian e le immagini distroless, per aiutarti a scegliere la base perfetta per ogni progetto.