Il contesto: un gestionale Django per enti pubblici
Skara è un gestionale web per la gestione di progetti di servizio civile: anagrafica, commissioni di selezione, mandati, API REST, generazione PDF. Un sistema con requisiti di affidabilità reali — dati sensibili, processi amministrativi, utenti non tecnici.
Questo articolo descrive come abbiamo strutturato il deploy: dall'immagine Docker alla pipeline CI/CD, passando per just come command runner unificato, fino agli script di backup e ripristino. Non è teoria: è la configurazione che gira in produzione.
Il Dockerfile: build multi-stage
Il primo problema di un'immagine Django è il peso. Se installi tutto in un unico layer — build tools, compilatori, dipendenze C — finisci con un'immagine da 800MB per un'applicazione che a runtime ha bisogno di un decimo di quelle librerie.
La soluzione è il build multi-stage: uno stage builder che compila le wheel Python con libffi-dev e build-essential, e uno stage runtime che riceve solo i pacchetti già compilati, senza compilatori né header. Il runtime installa esclusivamente le librerie di sistema necessarie a WeasyPrint per generare PDF — libpango, libcairo, libgdk-pixbuf — e nulla di più.
Il container gira come utente non-root (appuser, uid 1000). Non per fare teatro di sicurezza, ma perché un container che gira come root su un host condiviso è un rischio concreto.
Docker Compose: cinque servizi, una rete isolata
In produzione orchestriamo cinque servizi distinti:
- web — Gunicorn con 3 worker, esposto sulla porta 8000
- db — PostgreSQL 17 con volume persistente sull'host
- redis — broker Celery e backend cache
- celery — worker asincrono per task (geolocalizzazione, email, PDF)
- celery_beat — scheduler per task periodici
I volumi sono montati sull'host in posizioni fisse: /var/www/skara/media per i file caricati dagli utenti, /var/www/skara/staticfiles per gli asset serviti da Nginx, e ../../config per i file .cnf con le credenziali. I segreti non entrano mai nell'immagine: vengono montati a runtime dall'host.
Tre ambienti, un solo Dockerfile
La struttura deployments/ è divisa in tre ambienti con compose file e file .env separati:
- develop/ — sviluppo locale, Django su SQLite con venv, Redis e Celery in Docker
- staging/ — branch release, immagine con tag :staging
- production/ — branch main, immagine con tag :prod
just: un command runner per ogni contesto
Makefile per progetti Python è una scelta discutibile — la sintassi è fragile, l'indentazione con tab è una fonte costante di errori, e la semantica dei target non si adatta bene a comandi multi-riga con logica Bash. Abbiamo usato just, un command runner moderno che risolve questi problemi.
Il progetto ha due Justfile distinti con responsabilità diverse.
Sviluppo locale
Il Justfile in deployments/develop/ gestisce l'ambiente di sviluppo. Il comando principale è just start: avvia Docker Compose con Redis, Celery e MailDev, poi lancia il server Django sul virtualenv locale alla porta 5000 con supporto al debugger interattivo. just stop chiude tutto.
Gli altri comandi coprono il workflow quotidiano:
- just migrate — applica le migrazioni sul virtualenv locale
- just makemigrations — crea nuove migrazioni, con supporto ad argomenti opzionali
- just test — esegue la suite di test Django
- just shell — apre la shell Django interattiva
- just logs-celery, just logs-redis, just logs-maildev — log del singolo servizio
Setup e deploy su server
Il Justfile in deployments/setup/ gestisce il ciclo di vita sul server. Il comando just setup [environment] è quello che si esegue su un server nuovo: verifica i prerequisiti, configura un sparse checkout del repository Git, crea la struttura di directory necessaria e genera il file .env dal template.
Lo sparse checkout merita una nota: il server di produzione non ha bisogno dei file di configurazione staging, e viceversa. Il Justfile configura Git per scaricare solo deployments/production/ (o staging/) e config/, escludendo esplicitamente l'altro ambiente. Il server riceve esattamente i file che gli servono, niente di più.
Gli altri comandi operativi sono:
- just deploy [environment] — esegue deploy.sh per quell'ambiente, con supporto a tag immagine personalizzati
- just status — mostra lo stato dei container
- just logs [environment] [service] — log dell'ambiente o del singolo servizio
- just restart [environment] [service] — riavvio selettivo
- just ssl-setup [domain] — provisioning certificato Let's Encrypt via Certbot in container
- just generate-db-cnf — genera config/db.cnf a partire dal .env, così le credenziali database non devono essere inserite a mano nei file di configurazione Django
La pipeline GitHub Actions
Il workflow parte da un push su release o main. Il tag dell'immagine viene determinato automaticamente dal nome del branch: release produce :staging, main produce :prod. L'immagine viene costruita e pubblicata su GitHub Container Registry (GHCR), scelto perché incluso nell'account GitHub e autenticabile con lo stesso token del repository.
Lo script di deploy
Il deploy.sh automatizza la sequenza post-push: login a GHCR, docker compose pull, docker compose up -d --remove-orphans, attesa attiva che PostgreSQL sia pronto con retry, attesa che il container web risponda, migrate e collectstatic. Il container precedente viene sostituito solo quando quello nuovo è operativo.
Backup e ripristino
Il backup.sh esegue un pg_dump compresso con gzip direttamente sul container PostgreSQL tramite docker compose exec, più un archivio tar.gz dei file media. I file vengono nominati con timestamp preciso e salvati in backups/. Il restore.sh inverte il processo. Non è un sistema enterprise — è un sistema che si legge in dieci minuti, si schedula con cron, e si corregge senza documentazione.
L'entrypoint e il problema del timing
Docker Compose gestisce l'ordine di avvio ma non garantisce che PostgreSQL sia pronto quando Gunicorn parte. Senza un check esplicito il primo avvio fallisce sistematicamente. L'entrypoint risolve con un ciclo che verifica la disponibilità della porta ogni 100ms prima di eseguire il comando — tre righe di Bash che eliminano un'intera categoria di problemi.
La complessità giusta
La tentazione è sempre di aggiungere: Kubernetes, Terraform, un secrets manager. Per un team piccolo quella complessità costa cara in manutenzione senza portare benefici proporzionali. Docker Compose, GitHub Actions, GHCR, just e script Bash sono strumenti che ogni sviluppatore legge e corregge in autonomia — e questa capacità di manutenzione autonoma vale più di qualsiasi feature avanzata di orchestrazione.