Server che si riparano da soli: fai il push e dimenticatelo
- infrastructure
- devops
- automation
- self-hosting
Spingi un commit e dimentica. Il server scarica, ricompila e si riavvia da solo. Ecco la configurazione esatta: daemon supervisor, un cron notturno, backup automatici e TLS che si rinnova senza di te.
L'obiettivo è semplice: fare il push di un commit, chiudere il laptop e fidarsi che la macchina faccia il resto. Nessuna sessione SSH da tenere d'occhio. Nessun npm run build manuale a mezzanotte. Il server scarica il nuovo codice, ricompila solo ciò che è cambiato, riavvia il processo interessato e continua a servire traffico. Quando la macchina si riavvia dopo un aggiornamento del kernel, tutti i siti tornano attivi da soli. Questo è il contratto.
Non è magia. Sono pochi piccoli pezzi componibili, ognuno dei quali fa un lavoro in modo affidabile. Un supervisor per app. Nginx e il supervisor abilitati all'avvio. Un cron che gira alle 3 del mattino e controlla le modifiche upstream. Backup automatici del database con copie off-site e un percorso di ripristino testato. Certificati TLS che si rinnovano da soli. Combinali e ottieni un server che si comporta come una piattaforma gestita senza il suo prezzo.

La filosofia: idempotente, osservabile, nessun passo manuale
Tre regole governano ogni decisione qui. Idempotente: eseguire qualsiasi script due volte produce lo stesso risultato che eseguirlo una volta. Osservabile: ogni azione automatizzata scrive una riga di log così sai cosa è successo senza dover scavare. Nessun passo manuale: se per recuperare da un crash serve input umano, il sistema è rotto per design.
Lo script di aggiornamento notturno esemplifica tutte e tre. Controlla se l'HEAD locale corrisponde a quello remoto. Se corrispondono, registra "nessuna modifica, salto" ed esce pulito. Se differiscono, fa il pull, ricompila e riavvia. Eseguirlo due volte di fila quando non ci sono nuovi commit è innocuo. Ogni riga che tocca va in un file di log. Nessuno deve fare SSH.
Supervisor: il processo che non lascia mai morire la tua app
Supervisord è un supervisor di processi per Linux. Descrivi la tua app in un blocco INI e mantiene il processo attivo a tempo indeterminato. Crash? Riavvio. Kill OOM? Riavvio. Server riavviato? Riavvio all'avvio. autostart=true e autorestart=true sono le due righe che sostituiscono un'intera categoria di incidenti on-call.
Eseguo quattro siti su questo server: jumpinotech, wegweiserlife, nearyou e luci. Ognuno ottiene il proprio blocco program supervisor, la propria porta e il proprio file di log. Nginx fa il proxy da 443 a quelle porte. Supervisor e Nginx sono entrambi abilitati come servizi systemd così si avviano all'avvio prima di tutto il resto.
[program:site-jumpinotech]
command=/usr/bin/node /var/www/jumpinotech/server.js
directory=/var/www/jumpinotech
user=deploy
autostart=true
autorestart=true
startretries=5
stopwaitsecs=10
stdout_logfile=/var/log/supervisor/jumpinotech.log
stderr_logfile=/var/log/supervisor/jumpinotech.err
environment=NODE_ENV="production",PORT="3001"Installa con sudo apt install supervisor, metti questo blocco in /etc/supervisor/conf.d/site-jumpinotech.conf, poi esegui sudo supervisorctl reread && sudo supervisorctl update. Un comando per controllare lo stato di tutti e quattro i siti: sudo supervisorctl status. Un comando per seguire un log specifico: sudo supervisorctl tail -f site-jumpinotech.
Il cron delle 3: ricompila solo ciò che è cambiato
Un cron si attiva alle 3 ogni notte. Scorre tutte e quattro le directory dei siti, scarica dal remoto e confronta l'HEAD locale con quello remoto. Se corrispondono, salta. Se differiscono, fa il pull, esegue npm ci e npm run build, poi riavvia quel specifico programma supervisor. I siti che non sono cambiati continuano a servire dalla build esistente.
Questo è importante. Se fai il push di un fix a jumpinotech alle 23 e il cron gira alle 3, solo jumpinotech viene ricompilato. Gli altri tre siti non vengono toccati. Il tempo di build è proporzionale alle modifiche effettive, non al numero di app sulla macchina.
#!/usr/bin/env bash
# nightly-update.sh -- runs at 03:00 via cron as user deploy
# Rebuilds and restarts only sites whose upstream changed.
set -euo pipefail
SITES="/var/www/jumpinotech /var/www/wegweiserlife /var/www/nearyou /var/www/luci"
LOG="/var/log/nightly-update.log"
echo "--- update run at $(date) ---" >> "$LOG"
for SITE in $SITES; do
cd "$SITE"
git fetch origin main --quiet
LOCAL=$(git rev-parse HEAD)
REMOTE=$(git rev-parse origin/main)
if [ "$LOCAL" = "$REMOTE" ]; then
echo "$SITE: no changes, skipping" >> "$LOG"
continue
fi
echo "$SITE: pulling and rebuilding..." >> "$LOG"
git pull origin main --quiet
npm ci --quiet
npm run build --quiet
PROG=$(basename "$SITE")
sudo supervisorctl restart "$PROG" >> "$LOG" 2>&1
echo "$SITE: restarted OK" >> "$LOG"
done
echo "--- done ---" >> "$LOG"Aggiungilo al crontab dell'utente deploy con crontab -e e la riga 0 3 * * * /home/deploy/scripts/nightly-update.sh. Lo script ha bisogno di sudo NOPASSWD per supervisorctl. Aggiungilo una volta a /etc/sudoers.d/deploy e hai finito.
Backup del database e un ripristino testato
Un backup da cui nessuno ha mai provato a ripristinare non è un backup, è una coperta di sicurezza. Il mio setup: pg_dump gira alle 2 del mattino, un'ora prima dell'aggiornamento notturno. Il dump viene compresso con gzip e caricato su un bucket R2 con rclone. I dump vecchi vengono eliminati dopo 14 giorni sia localmente che da remoto.
# Database backup -- add to deploy user crontab with: crontab -e
# Runs at 02:00 every night; keeps 14 days of compressed dumps off-site.
0 2 * * * /home/deploy/scripts/db-backup.sh >> /var/log/db-backup.log 2>&1
# db-backup.sh content:
# !/usr/bin/env bash
# set -euo pipefail
# STAMP=$(date +'%Y-%m-%d')
# DUMP="/tmp/db_backup_STAMP.sql.gz"
# pg_dump -U appuser appdb | gzip > "$DUMP"
# rclone copy "$DUMP" r2:my-backups/postgres/
# find /tmp -name 'db_backup_*.sql.gz' -mtime +1 -delete
# rclone delete --min-age 15d r2:my-backups/postgres/- pg_dump produce un dump SQL puro, leggibile e portabile tra versioni di Postgres
- gzip riduce la dimensione del file circa della metà per la maggior parte dei mix di schema e dati
- rclone gestisce il caricamento off-site; configuralo una volta con
rclone config - R2 ha zero costi di egress, il che conta quando si ripristina sotto pressione
- Testa il ripristino ogni trimestre: scarica l'ultimo dump, avvia un database temporaneo, esegui psql, verifica i conteggi delle righe
TLS che si rinnova da solo
I certificati scaduti sono imbarazzanti ed evitabili. Esistono due buone strade. Se il dominio è dietro Cloudflare, usa la modalità Full (strict) con un certificato di origine Cloudflare: è valido per 15 anni sull'origine e Cloudflare gestisce il certificato lato browser. Nessun rinnovo necessario, mai. Se preferisci certbot, installalo, esegui sudo certbot --nginx, e il timer systemd che installa gira due volte al giorno per rinnovare qualsiasi certificato in scadenza entro 30 giorni.
Uso Cloudflare per tutti e quattro i siti. Il certificato di origine si trova in /etc/ssl/cloudflare/origin.pem. Nginx vi punta. Finché Cloudflare è davanti, i browser vedono un certificato valido mantenuto da Cloudflare, e il certificato di origine non scade in pratica.

Dare a un agente AI un runbook operativo persistente
Uso Claude Code come agente di coding e operazioni su tutti questi progetti. Il problema con gli agenti AI e l'infrastruttura è la memoria: ogni sessione parte da zero. L'agente non conosce le assegnazioni delle porte, i nomi dei programmi supervisor, la procedura di ripristino o perché il cron gira alle 3 e non alle 2. Ti ripeti, oppure ottieni comandi allucinati che quasi corrispondono alla realtà.
La soluzione è un file AGENTS.md incluso nel repository alla radice del progetto. Claude Code lo legge automaticamente all'avvio della sessione tramite la sua convenzione CLAUDE.md. Il file contiene il runbook operativo: tabella delle porte, nomi supervisor, pianificazione cron, configurazione TLS, passi di ripristino, problemi noti. Quando qualcosa cambia, dico all'agente di aggiornare il file. Diventa l'unica fonte di verità che sopravvive ai reset della finestra di contesto.
Il formato è volutamente conciso. Niente prosa. Solo fatti su cui l'agente può agire direttamente.
# Ops Runbook
## Sites and Ports
| App | Port | Supervisor name |
|----------------|------|-------------------|
| jumpinotech | 3001 | site-jumpinotech |
| wegweiserlife | 3002 | site-wegweiserlife|
| nearyou | 3003 | site-nearyou |
| luci | 3004 | site-luci |
## Nginx
- Config root: /etc/nginx/sites-enabled/
- Reload: sudo nginx -s reload
- All four sites proxied from 443 to the port above.
## Supervisor
- Config dir: /etc/supervisor/conf.d/
- Commands: sudo supervisorctl status | restart <name> | tail <name>
## Cron (deploy user)
- 03:00 nightly: /home/deploy/scripts/nightly-update.sh
- 02:00 nightly: /home/deploy/scripts/db-backup.sh
## TLS
- Provider: Cloudflare (proxied + Full strict mode)
- Fallback: certbot --nginx, timer: systemctl status certbot.timer
## Database Restore
1. Download latest dump from R2 bucket: rclone copy r2:my-backups/postgres/STAMP.sql.gz /tmp/
2. gunzip /tmp/STAMP.sql.gz
3. psql -U appuser appdb < /tmp/STAMP.sql
## Known Gotchas
- nightly-update.sh needs NOPASSWD sudo for supervisorctl (already in /etc/sudoers.d/deploy)
- Cloudflare origin cert lives at /etc/ssl/cloudflare/origin.pem -- do not deleteCon quel file nel repo, apro una sessione e dico "riavvia site-nearyou e controlla il suo log per gli errori." L'agente conosce il nome supervisor, la sintassi del comando e dove si trova il log. Non chiede. Non indovina. Agisce. Lo stesso per "mostrami il timestamp dell'ultimo backup" o "su quale porta gira luci." Il runbook risponde a queste domande prima che io finisca di digitare.
L'intero stack, in sintesi
Ecco tutto in un posto. Quattro siti Next.js, ognuno sulla propria porta (da 3001 a 3004), supervisionati da supervisord, proxiati da nginx. Sia supervisord che nginx sono abilitati tramite systemd all'avvio. Un cron alle 3 controlla ogni sito per le modifiche upstream e ricompila solo quelli che sono cambiati. Un cron separato alle 2 fa il dump del database Postgres, lo comprime e lo spedisce a R2. TLS è Cloudflare davanti con un certificato di origine che non scade su una scala temporale umana rilevante.
Fai il push di un commit. Il cron lo raccoglie alle 3. Se la build passa, il supervisor riavvia il processo. Il sito è live sul nuovo codice prima che qualcuno in Europa si svegli. Se la build fallisce, il vecchio processo continua a girare, il log registra l'errore e niente va spento. Questo è ciò che l'auto-guarigione significa davvero: non che i guasti siano impossibili, ma che il sistema degrada in modo controllato e si recupera senza di te.
"Automatizza il percorso di recupero prima che avvenga il guasto, non dopo." Un sistema che puoi riparare dal telefono alle 6 del mattino è un sistema che ti lascia dormire di notte.