Torna al blog
Agosto 19, 2026Guide

Come distribuire applicazioni con Docker Compose su un VPS

Guida pratica di produzione a Docker Compose v2 su Ubuntu: installare il motore, scrivere un file Compose con reti e volumi, gestire i segreti, mettere un reverse proxy davanti, aggiornare senza panico e fare il backup dei volumi con nome.

Come distribuire applicazioni con Docker Compose su un VPS

Docker Compose è il modo usuale di far girare uno stack piccolo su un VPS: un’app web, un database, una cache Redis e un reverse proxy, descritti in un file YAML. Non è Kubernetes. Non avrai failover multi-nodo. Avrai un impianto ripetibile e revisionabile, ricreabile in minuti su un nuovo VPS Hiddence: esattamente ciò di cui hanno bisogno la maggior parte dei side project e dei prodotti piccoli.

Questo articolo presuppone che tu voglia qualcosa più vicino alla produzione di un docker run il venerdì sera. Installiamo Docker Engine e il plugin Compose, creiamo un utente di deploy non root, scriviamo un file Compose con healthcheck e policy di riavvio, teniamo i segreti fuori da Git, pubblichiamo su internet solo le porte del reverse proxy e definiamo una routine di aggiornamento e backup. Lo stack di esempio è una tipica app web più PostgreSQL più Caddy, ma lo stesso schema vale per Node, PHP, Python o un gruppo di worker.

Perché Compose su un singolo VPS ha ancora senso

Si salta a Kubernetes perché è di moda, poi si passa il weekend su YAML per un sito. Compose resta leggibile. Versionate il file, documentate le variabili d’ambiente e potete fare diff di una modifica prima di applicarla. L’isolamento basta: un container dell’app compromesso non dovrebbe tenere il socket UNIX del database se avete usato reti e utenti con il minimo privilegio. I limiti di risorse impediscono a un memory leak di congelare l’intero VPS.

  • Un file descrive l’intero stack
  • I volumi con nome sopravvivono alla ricreazione dei container
  • Le reti Docker interne tengono Postgres fuori da internet pubblica
  • restart: unless-stopped copre la maggior parte dei riavvii
  • Facile da copiare su un secondo VPS quando il primo diventa stretto

Requisiti

Usa un VPS con RAM sufficiente per l’app più Postgres. Un piano da 2 GB è un minimo realistico per app + database + proxy. Si assume Ubuntu 24.04. Non installare Docker da uno Snap a caso senza leggere cosa fa ai cgroup; i passi sotto usano il repository apt ufficiale di Docker.

  • Ubuntu 22.04/24.04, root o sudo
  • 2 GB di RAM consigliati (1 GB solo per stack minuscoli senza Postgres)
  • Un dominio che punta al VPS se vuoi HTTPS automatico
  • Git o un altro modo di copiare il progetto sul server

Passo 1: Installare Docker Engine e Compose v2

Compose v2 è un plugin invocato come docker compose (con uno spazio), non il vecchio binario Python docker-compose. Installa entrambi dal repository di Docker per avere le patch di sicurezza aggiornate.

bash
ssh root@YOUR_VPS_IP
apt update && apt -y install ca-certificates curl gnupg
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null

apt update
apt -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

docker version
docker compose version

Passo 2: Utente di deploy e struttura delle directory

Eseguire Compose come root funziona, ma un utente dedicato nel gruppo docker è più pulito. Metti il progetto in /opt o /srv, non in /root, così backup e permessi sono evidenti. Non rendere mai scrivibile da tutti la directory che contiene .env.

bash
adduser --disabled-password --gecos '' deploy
usermod -aG docker deploy
mkdir -p /srv/app
chown deploy:deploy /srv/app
chmod 750 /srv/app

# Log in as deploy for the rest of the file editing:
# su - deploy
# cd /srv/app

Passo 3: Scrivere un file Compose pensato per la produzione

Il file sotto è un modello. Sostituisci l’immagine dell’app con la tua. Postgres non è pubblicato su 0.0.0.0:5432: solo Caddy è pubblicato su 80 e 443. Gli healthcheck impediscono a un reverse proxy di mandare traffico a un’app che sta ancora migrando il database. Fissa i tag delle immagini; latest è come arrivano i guasti a sorpresa.

bash
# /srv/app/compose.yaml
services:
  caddy:
    image: caddy:2.8-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      app:
        condition: service_healthy

  app:
    image: ghcr.io/example/webapp:1.4.2
    restart: unless-stopped
    env_file: .env
    environment:
      DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8000/health"]
      interval: 10s
      timeout: 3s
      retries: 10
    networks:
      - frontend
      - backend

  db:
    image: postgres:16-alpine
    restart: unless-stopped
    env_file: .env
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 10
    networks:
      - backend

networks:
  frontend: {}
  backend: {}

volumes:
  pgdata:
  caddy_data:
  caddy_config:

Passo 4: Segreti, .env e Caddyfile

Metti le password in .env sul server, chmod 600, e non fare mai commit di quel file. Genera le password con openssl rand -base64 32. Il Caddyfile deve solo fare reverse proxy verso il nome del servizio dell’app sulla rete Docker.

bash
# /srv/app/.env (permissions 600)
POSTGRES_USER=app
POSTGRES_PASSWORD=change-me-long-random
POSTGRES_DB=app
# plus any APP_SECRET / NEXTAUTH_SECRET your image needs

# /srv/app/Caddyfile
app.example.com {
    encode gzip
    reverse_proxy app:8000
}

chmod 600 /srv/app/.env
cd /srv/app
docker compose up -d
docker compose ps
docker compose logs -f --tail=100

Passo 5: Firewall, log e limiti di risorse

UFW dovrebbe consentire solo 22, 80 e 443. Docker a volte aggira UFW per le porte pubblicate; se ti serve un firewall host rigoroso, verifica l’interazione attuale Docker + UFW per la tua versione di Ubuntu, oppure pubblica le porte solo su 127.0.0.1 e metti un Caddy host davanti. Imposta limiti di memoria in Compose così Postgres non divora il VPS.

bash
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable

# Optional in compose.yaml under app or db:
#    deploy:
#      resources:
#        limits:
#          memory: 512M

# Logs (do not let json-file grow forever):
# { "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } }
# in /etc/docker/daemon.json then: systemctl restart docker

Passo 6: Aggiornamenti e rollback

L’aggiornamento noioso che funziona: scarica le nuove immagini, up -d, osserva gli healthcheck, tieni il tag precedente in git per poterlo rifissare. Non eseguire docker compose down su un database di produzione a meno che tu non intenda smettere di accettare traffico. down -v elimina i volumi: è una cancellazione, non un riavvio.

bash
cd /srv/app
git pull   # if the Compose file lives in git

# Change the image tag in compose.yaml, then:
docker compose pull
docker compose up -d
docker compose ps

# Rollback: set the old tag, pull, up -d again
# NEVER: docker compose down -v   # this deletes named volumes

Backup dei volumi con nome

Uno snapshot del VPS è buono. Un dump logico di Postgres è meglio se devi ripristinare su un’altra macchina. Programmalo con cron. Prova un ripristino una volta, altrimenti non hai backup: hai file che speri siano backup.

bash
# Postgres dump while the db container is running:
docker compose exec -T db pg_dump -U app app | gzip > /var/backups/app-$(date +%F).sql.gz

# Copy off-box
# scp /var/backups/app-*.sql.gz backup-host:~

# Restore sketch (maintenance window):
# gunzip -c app-2026-08-19.sql.gz | docker compose exec -T db psql -U app app

Risoluzione dei problemi

Leggi docker compose ps e docker compose logs service. I fallimenti nel pull delle immagini sono di solito limiti di frequenza ghcr/Docker Hub o un’immagine privata senza login. Un 502 da Caddy significa che l’app non è healthy o che l’hostname del proxy è sbagliato (usa il nome del servizio Compose, non localhost, dal container Caddy). Gli errori di permesso sui volumi spesso significano che l’immagine gira come uid 1000 mentre la directory host è di root.

  • compose: command not found — hai installato il vecchio nome del binario; usa docker compose
  • port is already allocated — qualcos’altro possiede 80/443 (Apache, Nginx, un altro Caddy)
  • database connection refused — l’app è partita prima che Postgres fosse pronto; usa healthcheck + condizione depends_on
  • disk full — docker system df, poi prune con cautela delle immagini inutilizzate
  • permission denied on docker.sock — utente non nel gruppo docker, o serve una nuova sessione di login

Sicurezza

Non pubblicare porte di database. Non usare privileged: true. Non montare /var/run/docker.sock in un container dell’app a meno che tu non stia scrivendo apposta un gestore di container. Tieni il motore aggiornato. Analizza le tue immagini se le costruisci. .env su disco resta un file segreto: limita chi può accedere via SSH alla macchina.

  • Niente 5432 / 3306 / 6379 pubblici
  • Fissa i digest delle immagini o almeno tag immutabili
  • chmod 600 .env
  • Unattended-upgrades per il sistema host contano ancora
  • Un progetto Compose per app riduce il raggio di esplosione

Consigli

  • Metti compose.yaml in git; tieni .env.example senza segreti veri
  • Usa i profili per i worker opzionali (docker compose --profile workers up -d)
  • Osserva docker stats quando dimensioni il VPS
  • I bind mount del codice sorgente non sono lo schema di produzione: cuoci un’immagine
  • Se lo stack cresce su più VPS, allora guarda l’orchestrazione, non prima

Un setup Compose quasi di produzione su un VPS è: pacchetti Docker ufficiali, un .env protetto, un YAML che non pubblica il database, healthcheck, un reverse proxy su 80/443, UFW, tag di immagine a cui tornare e dump Postgres che hai davvero ripristinato una volta. Parti dal modello di questo articolo, sostituisci l’immagine dell’app e tratta docker compose down -v come un comando distruttivo, con lo stesso rispetto di rm -rf.