Hai mai passato una domenica a inseguire un backup che doveva partire alle 3 di notte e invece è saltato perché il server era andato in sleep? O peggio, un certificato SSL scaduto che ha mandato offline il sito proprio il giorno del Black Friday? Succede. E quando succede, non è un problema tecnico: è un problema di fiducia con il cliente.
Noi, di Meteora Web, gestiamo server da oltre otto anni. E abbiamo imparato che la differenza tra un sistema che funziona e uno che regala notti insonni spesso è solo un cron job scritto male o un timer systemd configurato di fretta. In questa guida ti portiamo oltre il classico 0 3 * * * /script.sh: vedremo come rendere la schedulazione robusta, tracciabile e riparabile senza impazzire.
Parleremo di cron avanzato (logging, ambienti, lock) e di systemd timer, che per molti versi è il successore moderno di cron. Non per sostituirlo in ogni caso, ma per darti uno strumento in più quando serve affidabilità vera.
Perché non basta più il cron classico
Il demone cron è un cavallo di battaglia: semplice, leggero, universale. Ma ha limiti che emergono quando il progetto cresce. Errori silenziosi? Il logging di default è minimo. Variabili d'ambiente? Ogni job parte con un ambiente ridotto all'osso. Sovrapposizioni? Se uno script dura più del previsto, cron lo esegue comunque, creando due istanze che litigano sullo stesso file.
Esempio concreto: un cliente e-commerce aveva un cron che generava il feed Google Shopping ogni ora. Lo script impiegava 45 minuti, ma cron lo rilanciava ogni ora senza controllo. Dopo un paio di ore, quattro processi stavano scrivendo sullo stesso file XML. Feed corrotto, prodotti non indicizzati, vendite perse. Tutto perché mancava un semplice lock.
Sponsored Protocol
Systemd timer risolve molti di questi problemi: dipendenze, isolamento delle risorse, logging strutturato, possibilità di definire precise policy di fallimento. Ma non è una bacchetta magica: va capito e configurato.
Le fondamenta: come scrivere un cron job che non tradisce
Prima di parlare di timer, consolidiamo le best practice per i cron job tradizionali. Perché anche se passerai ai timer, queste regole restano valide per qualsiasi task schedulato.
1. Usa sempre lock per evitare sovrapposizioni
Il problema della doppia esecuzione è evitabile con un file di lock. Ecco uno script shell robusto:
#!/bin/bash
LOCKFILE="/tmp/mio_script.lock"
exec 200>"$LOCKFILE"
flock -n 200 || { echo "Si sta già eseguendo, esco"; exit 1; }
# Il tuo codice qui
sleep 60 # simulazione lavoro
rm -f "$LOCKFILE"
Attenzione: il lock deve essere rimosso, ma se lo script si blocca il file resta. Meglio usare flock con timeout o cancellazione automatica all'avvio. Noi preferiamo questa versione:
LOCKFILE="/var/lock/mio_script.lock"
if [ -f "$LOCKFILE" ]; then
pid=$(cat "$LOCKFILE")
if kill -0 "$pid" 2>/dev/null; then
echo "Processo $pid ancora in esecuzione. Annullo."
exit 1
fi
fi
echo $$ > "$LOCKFILE"
trap "rm -f $LOCKFILE" EXIT
# lavoro...
2. Logging a prova di errore
Cron di default scrive output su syslog e mails a root. Non è gestibile. Devi indirizzare standard output e stderr verso un file con data:
LOGDIR="/var/log/miei_jobs"
mkdir -p "$LOGDIR"
LOG="$LOGDIR/$(date +\%Y-\%m-\%d_\%H:\%M)-mio_script.log"
exec > >(tee -a "$LOG")
exec 2>&1
echo "$(date) - Inizio"
# lavoro
echo "$(date) - Fine"
Poi, nel crontab, non dimenticare di redirigere:
Sponsored Protocol
0 3 * * * /usr/local/bin/mio_script.sh >/dev/null 2>&1
Così eviti che cron invii email inutili e hai tutto il log centralizzato nello script stesso.
3. Variabili d'ambiente esplicite
Cron esegue i job con un ambiente minimo: PATH=/usr/bin:/bin, niente variabili personalizzate. Se il tuo script ha bisogno di JAVA_HOME o di un PATH esteso, caricali all'inizio dello script:
#!/bin/bash
source /etc/profile # o ~/.bashrc
export PATH="/usr/local/bin:$PATH"
4. Notifiche su fallimento
Aggiungi un meccanismo di notifica. Su un server di produzione, noi usiamo spesso curl verso un webhook Slack o Telegram alla fine del job, con l'esito. Esempio:
if [ $? -ne 0 ]; then
curl -s -X POST -H "Content-Type: application/json" \
-d '{"text":"Backup fallito su server X"}' \
https://hooks.slack.com/services/TTT/BBB/KKK
fi
Systemd timer: quando cron non basta
Systemd è il sistema di init di tutte le distribuzioni Linux moderne. I suoi timer permettono di schedulare unità di servizio con un livello di controllo che cron non offre. Quattro vantaggi concreti:
- Dipendenza dal tempo reale: puoi specificare che un job parta solo se un certo servizio è attivo (es. solo dopo che MySQL è up).
- RandomDelay: evita il “thundering herd” di job che partono tutti allo stesso secondo.
- Persistenza tra riavvii: se il server viene spento e riacceso, systemd recupera i job persi (opzione
Persistent=true). - Logging centralizzato in journald: tutti gli output dei job vanno nel journal, consultabili con
journalctl.
Struttura minima di un timer systemd
Hai bisogno di due file: un servizio (cosa fare) e un timer (quando farlo).
1. Il servizio: /etc/systemd/system/backup-db.service
[Unit]
Description=Backup giornaliero database
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup_db.sh
User=backup
2. Il timer: /etc/systemd/system/backup-db.timer
Sponsored Protocol
[Unit]
Description=Esegue backup-db ogni giorno alle 3
[Timer]
OnCalendar=daily
RandomizedDelaySec=30m
Persistent=true
[Install]
WantedBy=timers.target
Poi attivi il timer:
sudo systemctl daemon-reload
sudo systemctl enable backup-db.timer
sudo systemctl start backup-db.timer
Controlla lo stato con:
systemctl list-timers --all
# oppure
systemctl status backup-db.timer
Il OnCalendar supporta sintassi molto flessibile: *-*-* 03:00:00 per tutti i giorni, Mon..Fri 10:00 per giorni feriali o anche hourly. Guarda man systemd.time per tutte le opzioni.
Gestire il fallimento con i timer
A differenza di cron, systemd può riprovare un job fallito. Aggiungi al servizio:
[Service]
Restart=on-failure
RestartSec=5min
Così se lo script esce con codice != 0, systemd tenta di nuovo dopo 5 minuti. Per limitare i tentativi:
StartLimitInterval=1h
StartLimitBurst=3
RandomDelay per scaglionare i job
Se hai più timer che partono allo stesso minuto, il server potrebbe impennarsi. Aggiungi RandomizedDelaySec=30m e systemd ritarda ogni esecuzione di un valore casuale tra 0 e 30 minuti. Perfetto per job di backup o sincronizzazioni da più server.
Quando usare cron e quando systemd timer
Non c'è una risposta unica. Noi, in Meteora Web, seguiamo questa regola pratica:
- Cron per job semplici su server singoli, quando non servono dipendenze e la tolleranza agli errori è bassa (es. pulizia log, email automatiche).
- Systemd timer per task critici: backup, generazione di report, sincronizzazioni, job che dipendono da altri servizi (es. dopo il mount di un volume).
Un caso reale: un nostro cliente aveva uno script che importava file CSV ogni ora. Con cron, a volte lo script partiva mentre il file era ancora in scrittura da un altro processo. Con systemd timer abbiamo aggiunto un Requires= sul servizio che genera il file, e After=. Zero problemi.
Sponsored Protocol
Monitoraggio: non fidarti, controlla
Configurare un timer o un cron è solo l'inizio. Devi sapere quando non funziona. Ecco una checklist minima:
- Controlla i log con
journalctl -u backup-db.service -n 20per systemd, o il file di log dello script per cron. - Imposta un healthcheck esterno: puoi usare un servizio come healthchecks.io o anche un semplice cron che ogni ora scrive un timestamp su un file statico. Poi un monitor esterno (UptimeRobot, Better Uptime) controlla che il timestamp sia recente.
- Per i cron, aggiungi al crontab una riga che invia un report giornaliero via email:
0 8 * * * echo "Riassunto job ieri" | mail -s "Cron report" admin@tuoDominio.com
Esempio completo: backup con sistema di notifica
Mettiamo tutto insieme. Creiamo un servizio che fa un backup del database e invia notifica su Telegram se fallisce.
Servizio: /etc/systemd/system/backup-mysql.service
[Unit]
Description=Backup MySQL
After=mysql.service
Requires=mysql.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup_mysql.sh
User=backup
Restart=on-failure
RestartSec=5min
StartLimitInterval=1h
StartLimitBurst=3
Timer: /etc/systemd/system/backup-mysql.timer
[Unit]
Description=Backup MySQL alle 2 di notte
[Timer]
OnCalendar=*-*-* 02:00:00
RandomizedDelaySec=15m
Persistent=true
[Install]
WantedBy=timers.target
Script di backup: /usr/local/bin/backup_mysql.sh
#!/bin/bash
set -e
BACKUP_DIR="/var/backups/mysql"
LOGFILE="$BACKUP_DIR/backup_$(date +%Y%m%d_%H%M).log"
OUTFILE="$BACKUP_DIR/db_$(date +%Y%m%d_%H%M).sql.gz"
mkdir -p "$BACKUP_DIR"
exec > "$LOGFILE" 2>&1
echo "$(date) - Inizio backup"
mysqldump --single-transaction --routines --events \
--user backup --password=XXXXXXXX \
--all-databases | gzip > "$OUTFILE"
echo "$(date) - Backup completato: $OUTFILE"
# Notifica su Telegram se tutto ok (opzionale)
curl -s -X POST https://api.telegram.org/botTOKEN/sendMessage \
-d chat_id=ID \
-d text="Backup MySQL riuscito: $(du -sh $OUTFILE | cut -f1)"
Non dimenticare di rendere eseguibile lo script e testarlo manualmente prima di attivare il timer.
Sponsored Protocol
In sintesi — cosa fare adesso
- Rivedi i tuoi cron esistenti: aggiungi lock, logging e notifiche su fallimento. Parti dai più critici.
- Scegli un job da migrare a systemd timer: inizia con un backup o un task che dipende da un servizio. Segui la struttura servizio + timer.
- Imposta il monitoraggio: se non hai già un healthcheck esterno, configura un semplice ping periodico su un URL statico del tuo server. Noi usiamo UptimeRobot per questo.
- Documenta: tieni un file markdown nel repository del progetto con l'elenco di tutti i job schedulati, i loro log e come verificare che funzionano. La prossima volta che il server cade, ringrazierai te stesso.
Se gestisci server per clienti o progetti interni, queste pratiche non sono opzionali. Sono ciò che separa un sistema che “funziona quasi sempre” da uno su cui puoi dormire tranquillo. Noi le abbiamo imparate sulla pelle, ma tu puoi saltare la parte dolorosa.
Need a hand con la tua automazione? Noi di Meteora Web lavoriamo su stack Linux da otto anni. Se il tuo cron job non è all'altezza, parliamone.