Docker: dallo sviluppo alla produzione

Guida completa a Docker per containerizzare applicazioni. Dockerfile, Docker Compose, build multi-stage e deployment in produzione con esempi pratici.

Guida Docker dallo sviluppo alla produzione

Docker rivoluziona il modo in cui le applicazioni vengono sviluppate, testate e distribuite. Incapsulando un'applicazione e le sue dipendenze in un container portabile, Docker elimina il famigerato problema "funziona sulla mia macchina" e garantisce coerenza in tutti gli ambienti. Questa guida copre il percorso completo dal primo Dockerfile al deployment in produzione.

Docker nel 2026

Docker Desktop 5.x porta miglioramenti significativi delle prestazioni, incluso il supporto nativo di containerd, la gestione ottimizzata delle risorse e l'integrazione fluida con Kubernetes. Le immagini multi-architettura (ARM/x86) sono ormai pratica standard.

Fondamenti della containerizzazione

Un container è un'unità software leggera che racchiude codice, runtime, librerie di sistema e configurazioni. A differenza delle macchine virtuali che virtualizzano l'hardware, i container condividono il kernel del sistema host, rendendoli più veloci da avviare e meno esigenti in termini di risorse.

bash
# terminal
# Docker installation on Ubuntu
sudo apt update
sudo apt install -y docker.io

# Add user to docker group (avoids sudo)
sudo usermod -aG docker $USER

# Verify installation
docker --version
# Docker version 26.1.0, build 1234567

# First container: downloads image and runs
docker run hello-world

Questo comando scarica l'immagine hello-world da Docker Hub e avvia un container che mostra un messaggio di conferma.

bash
# terminal
# List running containers
docker ps

# List all containers (including stopped)
docker ps -a

# List downloaded images
docker images

# Remove a container
docker rm <container_id>

# Remove an image
docker rmi <image_name>

Questi comandi di base gestiscono il ciclo di vita di container e immagini.

Creare il primo Dockerfile

Un Dockerfile contiene istruzioni per costruire un'immagine Docker. Ogni istruzione crea un layer nell'immagine finale, abilitando caching e riutilizzo.

dockerfile
# Dockerfile
# Base image: Node.js 22 on Alpine Linux (lightweight)
FROM node:22-alpine

# Set working directory in the container
WORKDIR /app

# Copy dependency files first (cache optimization)
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production

# Copy source code
COPY . .

# Expose port (documentation)
EXPOSE 3000

# Startup command
CMD ["node", "server.js"]

L'ordine delle istruzioni è cruciale per l'ottimizzazione della cache. I file che cambiano raramente (package.json) devono essere copiati prima del codice sorgente.

bash
# terminal
# Build image with a tag
docker build -t my-app:1.0 .

# Run the container
docker run -d -p 3000:3000 --name my-app-container my-app:1.0

# Check logs
docker logs my-app-container

# Access container shell
docker exec -it my-app-container sh

Il flag -d esegue il container in background, -p mappa la porta 3000 del container alla porta 3000 dell'host.

Alpine vs Debian

Le immagini Alpine sono significativamente più piccole (circa 5 MB contro 120 MB per Debian). Tuttavia, utilizzano musl libc invece di glibc, il che può causare incompatibilità con alcune dipendenze native. In caso di problemi, si consiglia di preferire immagini basate su Debian (node:22-slim).

Build multi-stage per la produzione

I build multi-stage creano immagini di produzione ottimizzate separando l'ambiente di build dall'ambiente di esecuzione. Solo gli artefatti necessari vengono inclusi nell'immagine finale.

dockerfile
# Dockerfile.production
# ============================================
# Stage 1: Build
# ============================================
FROM node:22-alpine AS builder

WORKDIR /app

# Copy and install dependencies (including devDependencies)
COPY package*.json ./
RUN npm ci

# Copy source code
COPY . .

# Build the application (TypeScript, bundling, etc.)
RUN npm run build

# ============================================
# Stage 2: Production
# ============================================
FROM node:22-alpine AS production

# Non-root user for security
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

WORKDIR /app

# Copy only necessary files from builder stage
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./

# Switch to non-root user
USER nodejs

# Environment variables
ENV NODE_ENV=production
ENV PORT=3000

EXPOSE 3000

# Startup command
CMD ["node", "dist/server.js"]

Questo approccio riduce significativamente la dimensione dell'immagine finale escludendo strumenti di build, devDependencies e file sorgente.

bash
# terminal
# Build with specific file
docker build -f Dockerfile.production -t my-app:production .

# Compare image sizes
docker images | grep my-app
# my-app    production    abc123    150MB
# my-app    1.0           def456    450MB

La riduzione delle dimensioni può raggiungere il 60-70% a seconda del progetto, migliorando i tempi di deployment e riducendo la superficie di attacco.

Docker Compose per l'orchestrazione locale

Docker Compose semplifica la gestione di applicazioni multi-container. Un file YAML dichiara tutti i servizi, le loro configurazioni e dipendenze.

yaml
# docker-compose.yml
version: "3.9"

services:
  # Main application
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgresql://postgres:secret@db:5432/myapp
      - REDIS_URL=redis://cache:6379
    volumes:
      # Mount source code for hot-reload
      - ./src:/app/src
      - ./package.json:/app/package.json
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    networks:
      - app-network

  # PostgreSQL database
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: myapp
    volumes:
      # Data persistence
      - postgres_data:/var/lib/postgresql/data
      # Initialization script
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - app-network

  # Redis cache
  cache:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    command: redis-server --appendonly yes
    networks:
      - app-network

# Named volumes for persistence
volumes:
  postgres_data:
  redis_data:

# Dedicated network for isolation
networks:
  app-network:
    driver: bridge

I servizi comunicano usando i loro nomi (db, cache) attraverso la rete interna di Docker. Gli healthcheck assicurano che le dipendenze siano pronte prima dell'avvio dell'applicazione.

bash
# terminal
# Start all services
docker compose up -d

# View logs from all services
docker compose logs -f

# Logs from a specific service
docker compose logs -f app

# Stop and remove containers
docker compose down

# Removal including volumes (caution: data loss)
docker compose down -v

# Rebuild after Dockerfile changes
docker compose up -d --build

Pronto a superare i tuoi colloqui su DevOps?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

Gestione di secret e variabili d'ambiente

La gestione sicura dei secret è cruciale in produzione. Docker offre diversi approcci a seconda del contesto.

yaml
# docker-compose.override.yml (development only)
version: "3.9"

services:
  app:
    env_file:
      - .env.development
    environment:
      - DEBUG=true

Per la produzione, Docker secrets offre maggiore sicurezza.

yaml
# docker-compose.production.yml
version: "3.9"

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.production
    secrets:
      - db_password
      - api_key
    environment:
      - NODE_ENV=production
      - DATABASE_PASSWORD_FILE=/run/secrets/db_password
      - API_KEY_FILE=/run/secrets/api_key

secrets:
  db_password:
    file: ./secrets/db_password.txt
  api_key:
    file: ./secrets/api_key.txt

Il codice dell'applicazione legge i secret da file montati.

config/secrets.jsjavascript
const fs = require('fs');
const path = require('path');

// Utility function to read Docker secrets
function readSecret(secretName) {
  const secretPath = `/run/secrets/${secretName}`;

  // Check if secret file exists
  if (fs.existsSync(secretPath)) {
    return fs.readFileSync(secretPath, 'utf8').trim();
  }

  // Fallback to classic environment variables
  const envVar = secretName.toUpperCase();
  return process.env[envVar];
}

module.exports = {
  databasePassword: readSecret('db_password'),
  apiKey: readSecret('api_key'),
};

Questo approccio evita di esporre i secret nelle variabili d'ambiente o nelle immagini Docker.

Ottimizzazione delle immagini Docker

Diverse tecniche riducono la dimensione dell'immagine e migliorano le prestazioni.

dockerfile
# Dockerfile.optimized
FROM node:22-alpine AS base

# Install necessary tools in a single layer
RUN apk add --no-cache \
    dumb-init \
    && rm -rf /var/cache/apk/*

# ============================================
# Stage: Dependencies
# ============================================
FROM base AS deps

WORKDIR /app

# Copy only lock files for caching
COPY package.json package-lock.json ./

# Install with mounted npm cache (BuildKit)
RUN --mount=type=cache,target=/root/.npm \
    npm ci --only=production

# ============================================
# Stage: Builder
# ============================================
FROM base AS builder

WORKDIR /app

COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci

COPY . .
RUN npm run build

# ============================================
# Stage: Production
# ============================================
FROM base AS production

# Image metadata
LABEL maintainer="team@example.com"
LABEL version="1.0"
LABEL description="Production-ready Node.js application"

# Non-root user
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

WORKDIR /app

# Copy production dependencies
COPY --from=deps --chown=nodejs:nodejs /app/node_modules ./node_modules

# Copy build output
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/package.json ./

USER nodejs

ENV NODE_ENV=production

# dumb-init as PID 1 for signal handling
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/server.js"]

L'uso di dumb-init garantisce la corretta gestione dei segnali Unix, permettendo lo shutdown graceful del container.

bash
# terminal
# Enable BuildKit for advanced features
export DOCKER_BUILDKIT=1

# Build with cache and detailed output
docker build --progress=plain -t my-app:optimized .

# Analyze image layers
docker history my-app:optimized

# Detailed image inspection
docker inspect my-app:optimized
Sicurezza delle immagini

Le immagini devono essere scansionate regolarmente per individuare vulnerabilità usando strumenti come Trivy o Snyk. Le immagini base devono essere aggiornate periodicamente per includere le patch di sicurezza.

Reti avanzate in Docker

Docker offre diversi driver di rete per differenti casi d'uso.

yaml
# docker-compose.networking.yml
version: "3.9"

services:
  # Publicly accessible frontend
  frontend:
    build: ./frontend
    ports:
      - "80:80"
    networks:
      - frontend-network
      - backend-network

  # API accessible only from frontend
  api:
    build: ./api
    networks:
      - backend-network
      - database-network
    # No external ports exposed

  # Isolated database
  database:
    image: postgres:16-alpine
    networks:
      - database-network
    # Accessible only by API

networks:
  frontend-network:
    driver: bridge
  backend-network:
    driver: bridge
    internal: true  # No internet access
  database-network:
    driver: bridge
    internal: true

Questa configurazione isola i servizi seguendo il principio del minimo privilegio. Il database è accessibile solo dall'API.

bash
# terminal
# Inspect Docker networks
docker network ls

# Details of a specific network
docker network inspect app-network

# Create a custom network
docker network create --driver bridge --subnet 172.28.0.0/16 custom-network

# Connect a container to an existing network
docker network connect custom-network my-container

Volumi e persistenza dei dati

I volumi Docker preservano i dati oltre il ciclo di vita del container.

yaml
# docker-compose.volumes.yml
version: "3.9"

services:
  app:
    image: my-app:latest
    volumes:
      # Named volume for persistent data
      - app_data:/app/data
      # Bind mount for development
      - ./uploads:/app/uploads:rw
      # Read-only mount for configuration
      - ./config:/app/config:ro

  backup:
    image: alpine
    volumes:
      # Access same volume for backups
      - app_data:/data:ro
      - ./backups:/backups
    command: |
      sh -c "tar czf /backups/backup-$$(date +%Y%m%d).tar.gz /data"

volumes:
  app_data:
    driver: local
    driver_opts:
      type: none
      device: /path/to/host/data
      o: bind

La distinzione tra volumi nominati e bind mount è importante: i volumi sono gestiti da Docker mentre i bind mount usano direttamente il filesystem dell'host.

bash
# terminal
# List volumes
docker volume ls

# Inspect a volume
docker volume inspect app_data

# Remove orphaned volumes
docker volume prune

# Backup a volume
docker run --rm -v app_data:/data -v $(pwd):/backup alpine \
  tar czf /backup/volume-backup.tar.gz /data

Deployment in produzione

Un workflow di deployment robusto include build, test e push verso un registry.

bash
# deploy.sh
#!/bin/bash
set -e

# Variables
REGISTRY="registry.example.com"
IMAGE_NAME="my-app"
VERSION=$(git describe --tags --always)

echo "Building version: $VERSION"

# Build production image
docker build \
  -f Dockerfile.production \
  -t $REGISTRY/$IMAGE_NAME:$VERSION \
  -t $REGISTRY/$IMAGE_NAME:latest \
  --build-arg BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \
  --build-arg VERSION=$VERSION \
  .

# Security scan
echo "Running security scan..."
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
  aquasec/trivy image $REGISTRY/$IMAGE_NAME:$VERSION

# Push to registry
echo "Pushing to registry..."
docker push $REGISTRY/$IMAGE_NAME:$VERSION
docker push $REGISTRY/$IMAGE_NAME:latest

echo "Deployment complete: $REGISTRY/$IMAGE_NAME:$VERSION"

Per i deployment su server, un file compose di produzione separato adatta la configurazione.

yaml
# docker-compose.prod.yml
version: "3.9"

services:
  app:
    image: registry.example.com/my-app:latest
    restart: always
    deploy:
      replicas: 3
      resources:
        limits:
          cpus: "0.5"
          memory: 512M
        reservations:
          cpus: "0.25"
          memory: 256M
      update_config:
        parallelism: 1
        delay: 10s
        failure_action: rollback
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

Questa configurazione definisce l'allocazione delle risorse, la strategia di aggiornamento e gli healthcheck per un deployment affidabile.

bash
# terminal
# Production deployment with Docker Compose
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

# Zero-downtime update (rolling update)
docker compose -f docker-compose.yml -f docker-compose.prod.yml pull
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --no-deps app

# Rollback if issues occur
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --no-deps \
  --scale app=0 && \
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --no-deps app

Monitoraggio e debug dei container

Il monitoraggio dei container è essenziale in produzione.

bash
# terminal
# Real-time statistics for all containers
docker stats

# Statistics for a specific container with custom format
docker stats my-app --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}"

# Inspect processes in a container
docker top my-app

# Real-time Docker events
docker events --filter container=my-app

# Copy files from/to a container
docker cp my-app:/app/logs/error.log ./error.log

Per il debug avanzato, sono disponibili diverse tecniche.

bash
# terminal
# Interactive shell in a running container
docker exec -it my-app sh

# Execute a single command
docker exec my-app cat /app/config/settings.json

# Start a container in debug mode
docker run -it --rm --entrypoint sh my-app:latest

# Inspect environment variables
docker exec my-app printenv

# Analyze logs with filters
docker logs my-app --since 1h --tail 100 | grep ERROR
yaml
# docker-compose.monitoring.yml
version: "3.9"

services:
  app:
    # ... existing configuration
    labels:
      - "prometheus.scrape=true"
      - "prometheus.port=3000"
      - "prometheus.path=/metrics"

  prometheus:
    image: prom/prometheus:latest
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.retention.time=15d'

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3001:3000"
    volumes:
      - grafana_data:/var/lib/grafana
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=secret

volumes:
  prometheus_data:
  grafana_data:

Questo stack di monitoraggio permette di raccogliere e visualizzare le metriche dei container.

Conclusione

Docker trasforma il ciclo di sviluppo garantendo coerenza in tutti gli ambienti. La containerizzazione porta portabilità, isolamento e riproducibilità — qualità essenziali per le applicazioni moderne.

Checklist Docker per la produzione

  • ✅ Build multi-stage per immagini ottimizzate
  • ✅ Utente non-root nei container
  • ✅ Healthcheck configurati per tutti i servizi
  • ✅ Secret gestiti tramite Docker secrets o variabili d'ambiente sicure
  • ✅ Limiti di risorse (CPU, memoria) definiti
  • ✅ Volumi per la persistenza dei dati critici
  • ✅ Logging centralizzato con rotazione dei file
  • ✅ Scansione di sicurezza delle immagini prima del deployment
  • ✅ Strategia di aggiornamento senza downtime
  • ✅ Rete isolata tra i servizi

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

Padroneggiare Docker è una competenza fondamentale per ogni sviluppatore moderno. Dall'ambiente locale al deployment in produzione, Docker standardizza i workflow e semplifica le operazioni. I concetti presentati qui costituiscono una base solida per esplorare Kubernetes e l'orchestrazione di container su larga scala.

Tag

#docker
#containerization
#devops
#docker compose
#deployment

Condividi

Articoli correlati