Servere care se vindecă singure: fă push la cod și uită
- infrastructure
- devops
- automation
- self-hosting
Fă push la un commit și uită. Serverul descarcă, recompilează și se repornește singur. Iată configurația exactă: daemoni supervisor, un cron nocturn, backup-uri automate și TLS care se reînnoiește fără tine.
Scopul este simplu: faci push la un commit, închizi laptopul și ai încredere că mașina se ocupă de rest. Nicio sesiune SSH de supravegheat. Niciun npm run build manual la miezul nopții. Serverul descarcă noul cod, recompilează doar ce s-a schimbat, repornește procesul afectat și continuă să servească trafic. Când mașina repornește după o actualizare de kernel, toate site-urile revin singure. Acesta este contractul.
Nu e magie. Sunt câteva piese mici și componibile, fiecare făcând un singur lucru în mod fiabil. Un supervisor per aplicație. Nginx și supervisorul activați la pornire. Un cron care rulează la 3 dimineața și verifică modificările din upstream. Backup-uri automate ale bazei de date cu copii off-site și o cale de recuperare testată. Certificate TLS care se reînnoiesc singure. Combină-le și obții un server care se comportă ca o platformă gestionată fără prețul de platformă gestionată.

Filosofia: idempotent, observabil, fără pași manuali
Trei reguli guvernează fiecare decizie aici. Idempotent: rularea oricărui script de două ori produce același rezultat ca rularea o dată. Observabil: fiecare acțiune automatizată scrie o linie de log ca să știi ce s-a întâmplat fără să sapi. Fără pași manuali: dacă recuperarea dintr-un crash necesită input uman, sistemul este defect prin design.
Scriptul de actualizare nocturnă exemplifică toate trei. Verifică dacă HEAD-ul local corespunde celui remote. Dacă corespund, înregistrează "nicio modificare, sar" și iese curat. Dacă diferă, face pull, recompilează și repornește. A-l rula de două ori la rând când nu există noi commit-uri nu face nimic rău. Fiecare linie pe care o atinge merge într-un fișier de log. Nimeni nu trebuie să folosească SSH.
Supervisor: procesul care nu lasă niciodată aplicația ta să rămână moartă
Supervisord este un supervisor de procese pentru Linux. Descrii aplicația ta într-un bloc INI și el menține procesul viu pe termen nelimitat. Crash? Repornire. Kill OOM? Repornire. Server repornit? Repornire la boot. autostart=true și autorestart=true sunt cele două linii care înlocuiesc o întreagă categorie de incidente on-call.
Rulez patru site-uri pe acest server: jumpinotech, wegweiserlife, nearyou și luci. Fiecare primește propriul bloc program supervisor, propriul port și propriul fișier de log. Nginx face proxy de la 443 la acele porturi. Atât Supervisor cât și Nginx sunt activate ca servicii systemd pentru a porni la boot înaintea oricui altcuiva.
[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"Instalează cu sudo apt install supervisor, pune acest bloc în /etc/supervisor/conf.d/site-jumpinotech.conf, apoi rulează sudo supervisorctl reread && sudo supervisorctl update. O comandă pentru a verifica statusul tuturor celor patru site-uri: sudo supervisorctl status. O comandă pentru a urmări un log specific: sudo supervisorctl tail -f site-jumpinotech.
Cronul de la 3: recompilează doar ce s-a schimbat
Un cron job se declanșează la ora 3 în fiecare noapte. Parcurge toate cele patru directoare de site-uri, face fetch din remote și compară HEAD-ul local cu cel remote. Dacă corespund, sare. Dacă diferă, face pull, rulează npm ci și npm run build, apoi repornește acel program supervisor specific. Site-urile care nu s-au schimbat continuă să servească din build-ul existent.
Asta contează. Dacă faci push la un fix pentru jumpinotech la 23:00 și cronul rulează la 3:00, doar jumpinotech se recompilează. Celelalte trei site-uri rămân neatinse. Timpul de build este proporțional cu modificările reale, nu cu numărul de aplicații de pe mașină.
#!/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"Adaugă-l în crontab-ul utilizatorului deploy cu crontab -e și linia 0 3 * * * /home/deploy/scripts/nightly-update.sh. Scriptul are nevoie de sudo NOPASSWD pentru supervisorctl. Adaugă asta o dată la /etc/sudoers.d/deploy și ești gata.
Backup-uri de baze de date și un restore testat
Un backup din care nimeni nu a încercat vreodată să restaureze nu este un backup, este o pătură de confort. Setup-ul meu: pg_dump rulează la 2 dimineața, cu o oră înainte de actualizarea nocturnă. Dump-ul este comprimat cu gzip și încărcat într-un bucket R2 cu rclone. Dump-urile vechi sunt șterse după 14 zile atât local cât și remote.
# 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 simplu, lizibil și portabil între versiunile Postgres
- gzip reduce dimensiunea fișierului aproximativ la jumătate pentru majoritatea combinațiilor de schemă și date
- rclone gestionează upload-ul off-site; configurează-l o dată cu
rclone config - R2 are zero taxe de egress, ceea ce contează când restaurezi sub presiune
- Testează restore-ul trimestrial: descarcă ultimul dump, pornește o bază de date temporară, rulează psql, verifică numărul de rânduri
TLS care se reînnoiește singur
Certificatele expirate sunt jenante și evitabile. Există două căi bune. Dacă domeniul este în spatele Cloudflare, folosește modul Full (strict) cu un certificat de origine Cloudflare: este valid 15 ani pe origine și Cloudflare gestionează certificatul din partea browserului. Nu este nevoie niciodată de reînnoire. Dacă preferi certbot, instalează-l, rulează sudo certbot --nginx, iar timerul systemd pe care îl instalează rulează de două ori pe zi pentru a reînnoi orice certificat care expiră în 30 de zile.
Folosesc Cloudflare pentru toate cele patru site-uri. Certificatul de origine se află la /etc/ssl/cloudflare/origin.pem. Nginx pointează spre el. Atât timp cât Cloudflare este în față, browserele văd un certificat valid menținut de Cloudflare, iar certificatul de origine practic nu expiră niciodată.

A da unui agent AI un runbook ops persistent
Folosesc Claude Code ca agent de coding și ops pentru toate aceste proiecte. Problema cu agenții AI și infrastructura este memoria: fiecare sesiune pornește goală. Agentul nu știe alocările de porturi, numele programelor supervisor, procedura de recuperare sau de ce cronul rulează la 3 și nu la 2. Te repeți, sau primești comenzi halucinante care aproape corespund realității.
Soluția este un fișier AGENTS.md inclus în repository la rădăcina proiectului. Claude Code îl citește automat la pornirea sesiunii prin convenția sa CLAUDE.md. Fișierul conține runbook-ul ops: tabelul de porturi, numele supervisor, programul cron, configurarea TLS, pașii de restaurare, problemele cunoscute. Când ceva se schimbă, îi spun agentului să actualizeze fișierul. Devine singura sursă de adevăr care supraviețuiește reset-urilor ferestrei de context.
Formatul este deliberat concis. Fără proză. Doar fapte pe care agentul poate acționa direct.
# 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 deleteCu acel fișier în repo, deschid o sesiune și spun "repornește site-nearyou și verifică log-ul pentru erori." Agentul știe numele supervisor, sintaxa comenzii și unde se află log-ul. Nu întreabă. Nu ghicește. Acționează. La fel pentru "arată-mi timestamp-ul ultimului backup" sau "pe ce port rulează luci." Runbook-ul răspunde la aceste întrebări înainte să termin de tastat întrebarea.
Stack-ul complet, rezumat
Iată totul într-un singur loc. Patru site-uri Next.js, fiecare pe propriul port (de la 3001 la 3004), supravegheate de supervisord, proxiate de nginx. Atât supervisord cât și nginx sunt activate prin systemd la boot. Un cron la 3 verifică fiecare site pentru modificări upstream și recompilează doar cele care s-au schimbat. Un cron separat la 2 face dump la baza de date Postgres, o comprimă și o trimite la R2. TLS este Cloudflare în față cu un certificat de origine care nu expiră pe o scară temporală relevantă uman.
Faci push la un commit. Cronul îl ridică la 3. Dacă build-ul trece, supervisorul repornește procesul. Site-ul rulează pe noul cod înainte să se trezească cineva în Europa. Dacă build-ul eșuează, vechiul proces continuă să ruleze, log-ul înregistrează eroarea și nimic nu se stinge. Asta înseamnă cu adevărat self-healing: nu că eșecurile sunt imposibile, ci că sistemul degradează controlat și se recuperează fără tine.
"Automatizează calea de recuperare înainte să apară defecțiunea, nu după." Un sistem pe care îl poți repara de pe telefon la 6 dimineața este un sistem care îți permite să dormi toată noaptea.