Introduzione
Docker ha rivoluzionato lo sviluppo backend moderno. Nella mia esperienza con oltre 4 anni di lavoro con container in produzione, ho imparato che scrivere un Dockerfile funzionante è facile, ma scriverne uno ottimizzato richiede conoscenze specifiche.
In questo articolo condivido le best practices che ho applicato in produzione per ridurre dimensioni immagini del 70%, velocizzare build del 5x, e creare ambienti di sviluppo reproducibili.
Perché Docker per Backend?
Docker risolve il classico problema "funziona sul mio computer":
- Consistency: Stesso ambiente dev, staging, production
- Isolation: Dipendenze isolate per progetto
- Portability: Deploy su qualsiasi infrastruttura
- Scalability: Orchestrazione con Kubernetes
- CI/CD: Integrazione perfetta nelle pipeline
Best Practice #1: Multi-Stage Builds
La tecnica più impattante per ridurre dimensioni immagini Docker è il multi-stage build.
Esempio: Node.js Application
❌ Prima (1.2GB)
# Dockerfile non ottimizzato
FROM node:18
WORKDIR /app
# Copia tutto
COPY . .
# Installa dipendenze
RUN npm install
# Build
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
# Dimensione: ~1.2GB!
# Include node_modules development
# Include devDependencies
# Include source files non compilati
✅ Dopo (350MB)
# Dockerfile ottimizzato multi-stage
# Stage 1: Build
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# Stage 2: Production
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
EXPOSE 3000
CMD ["node", "dist/server.js"]
# Dimensione: ~350MB! (70% riduzione)
Esempio: PHP/Laravel Application
# Multi-stage build per Laravel
# Stage 1: Composer dependencies
FROM composer:2 AS composer
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist
COPY . .
RUN composer dump-autoload --optimize --no-dev
# Stage 2: Production
FROM php:8.2-fpm-alpine
# Install extensions
RUN docker-php-ext-install pdo pdo_mysql opcache
# Copy application
WORKDIR /var/www/html
COPY --from=composer /app .
# Optimize Laravel
RUN php artisan config:cache && \
php artisan route:cache && \
php artisan view:cache
EXPOSE 9000
CMD ["php-fpm"]
Best Practice #2: Layer Caching Intelligente
Docker usa layer caching per velocizzare i build. Ordina i comandi dal meno frequente al più frequente.
Ordine Ottimale dei Layer
# ✅ CORRETTO: Installa dipendenze PRIMA di copiare codice
FROM node:18-alpine
WORKDIR /app
# 1. Copia solo file dipendenze (cambia raramente)
COPY package*.json ./
# 2. Installa dipendenze (layer cached se package.json non cambia)
RUN npm ci
# 3. Copia codice sorgente (cambia frequentemente)
COPY . .
# 4. Build (invalidato solo quando cambia codice)
RUN npm run build
# Se modifichi solo il codice, npm ci usa la cache!
# Build 5-10x più veloci
# ❌ SBAGLIATO: Copia tutto subito
FROM node:18-alpine
WORKDIR /app
# Copia tutto (cache invalidata ad ogni modifica)
COPY . .
# npm install eseguito OGNI VOLTA anche se package.json non cambia!
RUN npm install
# Build lentissimi
Best Practice #3: .dockerignore
Come .gitignore, il file .dockerignore esclude file inutili dalla build context, riducendo dimensioni e velocizzando il build.
# .dockerignore essenziale
node_modules
npm-debug.log
.git
.gitignore
.env
.env.local
README.md
.vscode
.idea
*.md
dist
coverage
.DS_Store
Dockerfile
docker-compose.yml
# Non inviare a Docker daemon
# Build context più leggero
# Build più veloci
Best Practice #4: Usa Immagini Alpine
Le immagini Alpine Linux sono 5-10x più piccole delle immagini standard.
node:18→ 1.1GBnode:18-alpine→ 180MB (84% riduzione!)python:3.11→ 920MBpython:3.11-alpine→ 50MB (95% riduzione!)php:8.2-fpm→ 500MBphp:8.2-fpm-alpine→ 85MB (83% riduzione!)
Best Practice #5: Security Hardening
Non eseguire mai container come root in produzione.
# ✅ Security best practices
FROM node:18-alpine
# Crea user non-privilegiato
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
WORKDIR /app
# Installa dipendenze come root
COPY package*.json ./
RUN npm ci --only=production
# Copia applicazione
COPY --chown=nodejs:nodejs . .
# Cambia a user non-privilegiato
USER nodejs
EXPOSE 3000
CMD ["node", "server.js"]
# Container esegue come nodejs, non root!
# Limita danni in caso di breach
Best Practice #6: Health Checks
Aggiungi health checks per container monitoring e orchestrazione.
# Dockerfile con health check
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
# Health check HTTP
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node healthcheck.js
CMD ["node", "server.js"]
// healthcheck.js
const http = require('http');
const options = {
host: 'localhost',
port: 3000,
path: '/health',
timeout: 2000
};
const request = http.request(options, (res) => {
console.log(`STATUS: ${res.statusCode}`);
process.exit(res.statusCode === 200 ? 0 : 1);
});
request.on('error', (err) => {
console.error('ERROR:', err);
process.exit(1);
});
request.end();
Best Practice #7: Docker Compose per Dev Environment
Crea ambienti di sviluppo completi con docker-compose.
Esempio: Stack Backend Completo
# docker-compose.yml - Environment completo
version: '3.9'
services:
# Backend API
api:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgres://user:pass@db:5432/myapp
- REDIS_URL=redis://redis:6379
- NODE_ENV=development
volumes:
- .:/app
- /app/node_modules
depends_on:
- db
- redis
command: npm run dev
# PostgreSQL Database
db:
image: postgres:15-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: myapp
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
# Redis Cache
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
# Hasura GraphQL Engine
hasura:
image: hasura/graphql-engine:latest
ports:
- "8080:8080"
environment:
HASURA_GRAPHQL_DATABASE_URL: postgres://user:pass@db:5432/myapp
HASURA_GRAPHQL_ENABLE_CONSOLE: "true"
HASURA_GRAPHQL_ADMIN_SECRET: dev-secret
depends_on:
- db
# Mailhog (Email testing)
mailhog:
image: mailhog/mailhog
ports:
- "1025:1025" # SMTP
- "8025:8025" # Web UI
volumes:
postgres_data:
redis_data:
# Comandi utili:
# docker-compose up -d → Avvia tutto
# docker-compose logs -f api → Segui logs API
# docker-compose down → Ferma tutto
# docker-compose ps → Status servizi
Best Practice #8: Environment Variables e Secrets
Non includere mai API keys, password o secrets nel Dockerfile o nella build.
# ❌ MAI FARE QUESTO
FROM node:18-alpine
ENV DATABASE_PASSWORD=mysecretpassword123 # ESPOSTO IN IMMAGINE!
ENV API_KEY=sk_live_abc123xyz # CHIUNQUE PUÒ VEDERE!
# ✅ Passa secrets a runtime
docker run -e DATABASE_PASSWORD=$DB_PASS \
-e API_KEY=$API_KEY \
my-app
# ✅ O usa .env file (non committare!)
docker run --env-file .env my-app
# ✅ O usa Docker secrets (Swarm/Kubernetes)
docker secret create db_password password.txt
docker service create --secret db_password my-app
Best Practice #9: Ottimizzazione Build Cache
Usa BuildKit per build più veloci e cache avanzata.
# Abilita BuildKit (build 2-3x più veloci)
export DOCKER_BUILDKIT=1
# Build con cache mount (dependencies cache)
docker build \
--build-arg BUILDKIT_INLINE_CACHE=1 \
--cache-from myapp:latest \
-t myapp:dev .
# In Dockerfile usa cache mounts
RUN --mount=type=cache,target=/root/.npm \
npm ci --only=production
Best Practice #10: Monitoring e Logs
Configura logging strutturato per container observability.
// logger.js - Structured logging
const winston = require('winston');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json() // JSON per parsing facile
),
transports: [
new winston.transports.Console({
format: winston.format.simple()
})
]
});
// Log strutturati per container
logger.info('Server started', {
port: 3000,
environment: process.env.NODE_ENV,
version: process.env.APP_VERSION
});
// Error tracking
logger.error('Database connection failed', {
error: err.message,
stack: err.stack,
timestamp: new Date().toISOString()
});
Checklist Docker per Production
- ✅ Multi-stage build implementato
- ✅ Immagini Alpine per dimensioni ridotte
- ✅ .dockerignore configurato
- ✅ Non esegue come root user
- ✅ Health checks configurati
- ✅ Secrets gestiti correttamente
- ✅ Logging strutturato
- ✅ Security scan passato (Trivy, Snyk)
- ✅ Dimensione immagine < 500MB
- ✅ Build time < 5 minuti
Tools Essenziali per Docker Development
- Dive: Analizza layer e trova bloat (
dive image:tag) - Trivy: Security scanner per vulnerabilità
- Hadolint: Linter per Dockerfile
- Docker Scout: Analisi sicurezza immagini
- Lazydocker: TUI per gestione container
La Mia Esperienza in Produzione
Implementando queste best practices in diversi progetti:
Risultati Misurabili
- Dimensioni immagini: Da 1.2GB a 350MB (70% riduzione)
- Build time: Da 8 minuti a 90 secondi (5x più veloce)
- Deploy time: Da 5 minuti a 45 secondi (6x più veloce)
- Costi infrastruttura: -40% (meno storage, meno bandwidth)
- Developer onboarding: Da 1 giorno a 15 minuti
Errori Comuni da Evitare
- Non usare latest tag: Sempre usa tag versioned
- Container bloat: Rimuovi cache e file temporanei
- Too many layers: Combina comandi RUN correlati
- Build secrets leak: Usa multi-stage builds
- Porta hardcoded: Usa variabili ambiente
Risorse Utili
- Docker Documentation:
https://docs.docker.com/ - Docker Best Practices:
https://docs.docker.com/develop/dev-best-practices/ - Dive Tool:
https://github.com/wagoodman/dive - Hadolint:
https://github.com/hadolint/hadolint
Conclusione
Docker è uno strumento potentissimo, ma va usato correttamente. Queste best practices sono il risultato di anni di esperienza in produzione e ti faranno risparmiare tempo, denaro e mal di testa.
Hai bisogno di aiuto con Docker?
Se vuoi ottimizzare la tua infrastruttura Docker, implementare CI/CD con container, o migrare a Kubernetes, contattami per una consulenza. Ho esperienza con Docker in produzione in diversi contesti e posso aiutarti a implementare le best practices nel tuo progetto.