Docker Compose: l'intero ambiente con un solo comando
- Docker
- DevOps
- Self-hosting
- Infra
Database, cache, object storage, reverse proxy e l'applicazione stessa, definiti una volta in un unico file e avviati insieme. Compose è il modo in cui uno sviluppatore in solitaria gestisce un vero stack di produzione senza un runbook.
"Funziona sulla mia macchina" è una confessione, non un aggiornamento di stato. La soluzione è smettere di descrivere l'ambiente in una wiki che nessuno legge e iniziare a definirlo in un file che tutti eseguono. Docker Compose è quel file. L'intero stack NearYou, l'app Next.js, il worker in background, MySQL, Redis, MinIO e Caddy, si avvia con docker compose up e nient'altro. Sul mio laptop, su CI, su un nuovo box Hetzner: lo stesso comando, lo stesso risultato.
Lo stack che ha motivato tutto questo
NearYou è una piattaforma di offerte basata sulla posizione. Il backend ha bisogno di un database relazionale per offerte e aziende, una cache per i dati di sessione e il rate limiting, object storage per i media caricati, un processo worker per l'invio delle notifiche e un reverse proxy che termina TLS e instrada il traffico. Sono cinque componenti in movimento prima di una singola riga di codice del prodotto. Senza Docker, sono cinque installazioni separate con cinque vincoli di versione che divergono nel momento in cui si aggiunge un secondo sviluppatore, o nel momento in cui si effettua il provisioning di un nuovo server.
Con Compose, quei cinque componenti diventano venti righe di YAML e un Dockerfile. L'intero ambiente è un artefatto che può essere committato in git. Un nuovo collaboratore clona il repository, esegue docker compose up ed è operativo in cinque minuti. Non cinque minuti di digitazione, cinque minuti di download dei container.

Il vero docker-compose.yml
Questa è una versione ridotta ma rappresentativa del file Compose di NearYou. Quella reale aggiunge variabili d'ambiente da un file .env e alcune annotazioni di label per Caddy, ma la struttura è esattamente questa:
services:
mysql:
image: mysql:8.4
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: nearyou
volumes:
- mysql_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
timeout: 5s
retries: 10
start_period: 20s
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
minio:
image: minio/minio:latest
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY}
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY}
volumes:
- minio_data:/data
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 10s
timeout: 5s
retries: 5
app:
build:
context: .
dockerfile: Dockerfile
target: runner
environment:
DATABASE_URL: mysql://root:${DB_ROOT_PASSWORD}@mysql:3306/nearyou
REDIS_URL: redis://redis:6379
MINIO_ENDPOINT: http://minio:9000
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
minio:
condition: service_healthy
worker:
build:
context: .
dockerfile: Dockerfile
target: worker
environment:
DATABASE_URL: mysql://root:${DB_ROOT_PASSWORD}@mysql:3306/nearyou
REDIS_URL: redis://redis:6379
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
caddy:
image: caddy:2-alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
depends_on:
- app
volumes:
mysql_data:
redis_data:
minio_data:
caddy_data:Healthcheck e depends_on: la parte che tutti saltano
La maggior parte dei tutorial Docker mostra depends_on come un elenco di nomi di servizi. Quella forma controlla solo l'ordine di avvio, non la prontezza. MySQL si avvia, Docker segna la dipendenza come soddisfatta e l'app inizia a connettersi prima che il database abbia terminato l'inizializzazione. Il risultato è un avvio instabile che fallisce circa una volta su cinque e produce uno stack trace che sembra un errore reale.
La soluzione è la forma condition: service_healthy, che richiede un healthcheck superato prima che Docker sblocchi il servizio dipendente. Scrivi l'healthcheck per ogni servizio con stato. Per MySQL è mysqladmin ping. Per Redis è redis-cli ping. Per MinIO è il comando mc ready local. Con tutti e tre in posizione, l'app si avvia solo quando tutti e tre i servizi di supporto sono genuinamente pronti ad accettare connessioni. L'avvio smette di essere una gara.
Il Dockerfile: una sola build, due target
Il file Compose fa riferimento a due target di build: runner per l'app Next.js e worker per il processo in background. Entrambi provengono dallo stesso Dockerfile usando build multi-stage. Gli stage app e worker condividono la stessa base e la stessa installazione Node, hanno semplicemente entrypoint diversi. Ciò significa che una sola build dell'immagine copre entrambi i servizi, e non c'è alcuna divergenza tra il runtime che serve le richieste e quello che elabora i job.
# Dockerfile (condensed for clarity)
FROM node:22-alpine AS base
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
FROM base AS builder
COPY . .
RUN npm run build
# The web server target
FROM base AS runner
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
ENV NODE_ENV=production
CMD ["node", "server.js"]
# The worker target
FROM base AS worker
COPY --from=builder /app/dist/worker ./worker
ENV NODE_ENV=production
CMD ["node", "worker/index.js"]In produzione, docker build --target runner e docker build --target worker producono due immagini leggere che condividono la stessa layer cache. Il worker non è uno stack diverso, sono le stesse dipendenze con un entry point diverso. Questa uniformità è importante quando si fa debugging: nessuna sorpresa del tipo "ma funziona nel container dell'app".
Named volume: dati che sopravvivono ai riavvii dei container
I container sono pensati per essere usa e getta. I dati che contengono no. I named volume sono il meccanismo che li tiene separati. Quando esegui docker compose down senza --volumes, i container si fermano e vengono rimossi, ma mysql_data, redis_data e minio_data persistono sull'host. Esegui di nuovo docker compose up e tutti e tre i database sono esattamente dove li avevi lasciati.
- I named volume risiedono in
/var/lib/docker/volumes/e sopravvivono al ciclo di vita dei container. - I bind mount (un percorso host nella definizione del volume) sono la scelta giusta per il Caddyfile, perché è una configurazione che si modifica, non dati da proteggere.
docker compose down --volumesè l'opzione nucleare: elimina i volume. Non eseguirla mai in produzione senza avere un backup a portata di mano.- Esegui il backup del volume MySQL esternamente. I named volume non sono backup. Un
mysqldumpnotturno inviato fuori dalla macchina è ciò che rende il volume affidabile.

Dare a un agente AI la memoria a lungo termine dello stack
Uso Claude Code come agente di codice principale su questo progetto. Il modello conosce Docker in generale, ma non sa quali tag di immagine fissiamo, quali servizi hanno bisogno di healthcheck con quali comandi, come si chiamano i volume, o perché l'adattatore MariaDB è nella configurazione Prisma invece di quello MySQL. Senza quel contesto, ogni sessione dell'agente che tocca l'infrastruttura ricomincia da capo e produce output plausibile ma errato.
La soluzione è un file di memoria markdown persistente committato nel repository. AGENTS.md (o un DOCKER.md dedicato) registra le convenzioni, i comandi e le insidie in testo semplice. L'agente lo legge all'inizio di ogni sessione che coinvolge lo stack. Poiché il file vive in git, rimane accurato man mano che lo stack si evolve: aggiorna il file Compose, aggiorna il doc, committa entrambi.
Ecco il formato effettivo che uso. Non è un tutorial, è un riferimento denso che l'agente può assimilare in dieci secondi:
# Docker Stack Reference (NearYou)
## Services
| Service | Image | Role |
|---------|------------------|-------------------------------|
| mysql | mysql:8.4 | Primary database |
| redis | redis:7-alpine | Sessions, rate limits, queues |
| minio | minio/minio | Object storage (S3-compat) |
| app | build ./ | Next.js app (target: runner) |
| worker | build ./ | BullMQ worker (target: worker)|
| caddy | caddy:2-alpine | Reverse proxy, TLS |
## Key commands
- Start: `docker compose up -d`
- Rebuild app after code change: `docker compose build app worker && docker compose up -d app worker`
- Tail logs: `docker compose logs -f app worker`
- MySQL shell: `docker compose exec mysql mysql -u root -p nearyou`
- MinIO CLI: `docker compose exec minio mc ls local/uploads`
## Healthchecks
Every stateful service has a healthcheck. The app and worker use
`depends_on: condition: service_healthy`. Never remove them.
MySQL needs `start_period: 20s` because it is slow on first init.
## Volumes
Named volumes: mysql_data, redis_data, minio_data, caddy_data.
`docker compose down` is safe. `docker compose down --volumes` deletes data.
## Production rebuild (fresh Hetzner box)
1. Install Docker and Docker Compose plugin.
2. Clone repo, copy .env from secrets manager.
3. `docker compose pull && docker compose build`
4. `docker compose up -d`
5. Restore latest MySQL dump into mysql container.
## Gotchas
- Prisma uses the MariaDB adapter against MySQL 8.4. Do not swap to the MySQL
adapter without testing migrations first.
- MinIO healthcheck uses `mc ready local`, not curl. The mc binary is bundled
in the minio image.
- Caddy auto-provisions TLS. Ports 80 and 443 must be open. Caddyfile lives at
./Caddyfile in the repo root.Quando l'agente apre una sessione per aggiungere un nuovo servizio, fare il debug di un errore di avvio o scrivere uno script di migrazione, legge prima questo file. Conosce il tag dell'immagine fissato per MySQL prima di scrivere un comando healthcheck. Sa che non deve toccare i flag del volume prima di suggerire un teardown. La qualità dell'output è direttamente correlata alla qualità di questo file. Tenerlo aggiornato richiede cinque minuti di lavoro per ogni modifica. L'alternativa è rispiegare l'intero stack ad ogni sessione.
Ricostruire un server nuovo in pochi minuti
Il test finale di uno stack definito con Compose è: quanto tempo ci vuole per passare da un box Hetzner vuoto a un ambiente di produzione funzionante? Per NearYou la risposta è circa quindici minuti, e la maggior parte è in attesa che Docker scarichi le immagini.
- 01Effettua il provisioning del box, installa Docker Engine e il plugin Compose.
- 02Clona il repository e inserisci il file
.envdal gestore dei segreti. - 03
docker compose pullper pre-scaricare le immagini, poidocker compose buildper i target personalizzati. - 04
docker compose up -dper avviare tutto in modalità detached. - 05Ripristina l'ultimo dump MySQL:
docker compose exec -T mysql mysql -u root -p nearyou < backup.sql. - 06Punta il record DNS A al nuovo IP. Caddy fornisce TLS automaticamente alla prima richiesta.
Non c'è un runbook. Non c'è una checklist da cui potresti dimenticare un elemento. L'ambiente è definito nel repository, i comandi sono nel DOCKER.md e l'agente può eseguire la procedura di ripristino senza che io la narri passo per passo. Questo è il beneficio composto: il file Compose rende lo stack riproducibile, e la memoria markdown rende lo stack gestibile dall'agente. Insieme significano che un server morto è una seccatura di quindici minuti, non una crisi.
L'ambiente è codice. Se non è nel repository, non esiste.