Torna al Blog

Docker per Backend Developers: Best Practices e Ottimizzazioni

Come containerizzare applicazioni backend in modo efficiente, ottimizzare le immagini Docker e implementare ambienti di sviluppo consistenti con best practices testate in produzione.

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.

Confronto Dimensioni
  • node:18 → 1.1GB
  • node:18-alpine → 180MB (84% riduzione!)
  • python:3.11 → 920MB
  • python:3.11-alpine → 50MB (95% riduzione!)
  • php:8.2-fpm → 500MB
  • php: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

⚠️ ATTENZIONE: Mai Secrets in Dockerfile!

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

Production Readiness Checklist
  • ✅ 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

  1. Non usare latest tag: Sempre usa tag versioned
  2. Container bloat: Rimuovi cache e file temporanei
  3. Too many layers: Combina comandi RUN correlati
  4. Build secrets leak: Usa multi-stage builds
  5. 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.

Sull'Autore

Leonardo Pedron è un Software Engineer con oltre 4 anni di esperienza specializzato in Backend Development e DevOps. Ha implementato Docker in produzione per applicazioni ad alto traffico con focus su performance e security.