Configurare un cron job: pianificazione, permessi ed errori frequenti

Pubblicato il 20 min di lettura

Il job gira nella shell, ma non in cron. Questa guida spiega i cinque campi temporali, la differenza fra crontab utente e /etc/cron.d, la trappola del PATH e come dimostrare che un job è andato davvero a buon fine.

Un cron job si configura in cinque minuti e poi costa spesso ore. Il comando gira senza problemi nella shell, da cron invece non succede nulla, e nel log, nel migliore dei casi, si legge che cron ha avviato qualcosa. Questo articolo affronta esattamente i punti in cui le guide abituali si fermano: i messaggi di errore reali, le differenze fra le distribuzioni e la domanda di come capire se un job è arrivato davvero in fondo e non si è soltanto avviato.

Tutte le indicazioni si riferiscono a Debian 13 (trixie), Debian 12 (bookworm), Ubuntu 24.04 LTS e Ubuntu 22.04 LTS. Queste quattro distribuzioni usano lo stesso cron (la variante Debian di Vixie Cron), non cronie come Red Hat e Fedora. Questa differenza diventa decisiva più avanti, quando si parla di fuso orario.

Il servizio cron è davvero attivo?

Nelle installazioni complete cron è presente. Nelle immagini cloud minimali, nelle immagini base per container e nelle installazioni netinst ridotte all'essenziale il pacchetto manca di frequente. È la prima cosa da chiarire, prima di mettersi a fare il debug di qualsiasi altra cosa.

command -v crontab
systemctl status cron

Volutamente command -v crontab e non command -v cron: il servizio vero e proprio si trova in /usr/sbin, e su Debian quella directory compare solo nel percorso di ricerca di root. Come utente normale non ottieni semplicemente nessun output, anche se cron è installato. Su Ubuntu 24.04 la verifica funziona anche da utente senza privilegi. crontab, invece, si trova ovunque in /usr/bin ed è quindi visibile a tutti, e systemctl status cron risponde comunque alla domanda più importante, cioè se il servizio è anche in esecuzione.

Se il servizio manca, lo installi:

apt-get update
apt-get install -y cron
systemctl enable --now cron

Su Debian e Ubuntu il servizio si chiama cron, non crond. Chi è abituato a Red Hat ottiene qui un Unit crond.service could not be found. e cerca nel posto sbagliato.

In esercizio normale non esiste alcun obbligo di riavvio: il cron di Debian sorveglia le directory delle crontab tramite inotify e rilegge da solo le modifiche. Dopo aver modificato una crontab non devi quindi riavviare niente. Fanno eccezione il cambio del fuso orario di sistema e le modifiche a /etc/default/cron.

I cinque campi, e la trappola nel quinto

Ogni riga inizia con cinque campi temporali, poi segue il comando.

CampoIntervalloNota
Minuto0 a 59
Ora0 a 23formato 24 ore, nessun fuso orario
Giorno del mese1 a 31
Mese1 a 12anche da jan a dec
Giorno della settimana0 a 70 e 7 sono entrambi domenica, anche da sun a sat
30 4 * * *      /usr/local/bin/backup.sh      # ogni giorno alle 04:30
*/10 * * * *    /usr/local/bin/check.sh       # ogni 10 minuti
0 2 * * 0       /usr/local/bin/weekly.sh      # la domenica alle 02:00
15 3 1 * *      /usr/local/bin/monthly.sh     # il primo del mese alle 03:15
0 9-17 * * 1-5  /usr/local/bin/business.sh    # nei giorni feriali, ogni ora dalle 9 alle 17

Due dettagli vengono fraintesi quasi sempre.

Giorno del mese e giorno della settimana sono un OR, non un AND. Non appena entrambi i campi sono ristretti, cioè nessuno dei due contiene un asterisco, il job parte quando uno dei due corrisponde. 0 3 13 * 5 non significa "venerdì 13", ma "ogni giorno 13 e in più ogni venerdì". Se vuoi davvero venerdì 13, quel controllo lo fai dentro allo script.

Gli intervalli non dividono in modo uniforme. */7 * * * * parte ai minuti 0, 7, 14, 21, 28, 35, 42, 49 e 56, poi il contatore salta all'ora successiva. Fra il minuto 56 e il minuto 0 passano quindi solo quattro minuti. Vale per ogni intervallo che non divide esattamente 60 oppure 24. Per "ogni 90 minuti" in cron non esiste una notazione pulita, qui un timer systemd è la scelta migliore.

Usare crontab -e nel modo giusto

La crontab utente non la modifichi mai direttamente in /var/spool/cron/crontabs/, ma sempre tramite lo strumento apposito:

crontab -e

Alla prima chiamata il programma risponde con no crontab for root - using an empty one. Su sistemi appena installati segue poi la scelta dell'editor. Se manca qualsiasi editor, per esempio in un'immagine minimale, la chiamata si interrompe con un messaggio del tipo /usr/bin/sensible-editor: 25: editor: not found. Rimedio: installare un editor oppure indicare esplicitamente quello desiderato.

apt-get install -y nano
EDITOR=nano crontab -e

Il grande vantaggio di crontab -e rispetto alla scrittura diretta del file è il controllo al salvataggio. Se una riga è rotta, vedi:

"/tmp/crontab.7hK2mn/crontab":3: bad minute
errors in crontab file, can't install.
Do you want to retry the same edit? (y/n)

Il numero di riga è corretto, il campo indicato nel messaggio non sempre: bad minute compare anche quando manca semplicemente un campo, perché in quel caso il parser legge tutto spostato verso sinistra. In caso di successo l'operazione termina con crontab: installing new crontab. Solo questa riga significa che la modifica è stata applicata.

Altri comandi che dovresti conoscere:

crontab -l                         # visualizzare
crontab -l > /root/crontab.bak     # salvare
crontab -u www-data -l             # leggere la crontab di un altro utente (come root)
crontab -i -r                      # eliminare, con conferma (solo al terminale)

Una nota sul backup: crontab -l > /root/crontab.bak segnala no crontab for root e restituisce il codice di ritorno 1 finché per quell'utente non esiste ancora nessuna crontab. Il file di destinazione viene creato lo stesso, in quel caso con 0 byte. Nell'ordine di questo articolo la cosa non salta all'occhio, ma in uno script che controlla il codice di ritorno o che sovrascrive un backup più vecchio sì, eccome.

crontab -r senza -i cancella subito l'intera crontab, senza chiedere nulla. Dato che sulla tastiera r ed e non sono lontane, è un caso concreto di perdita di dati. Al terminale abituati quindi a crontab -i -r e fai prima un backup con crontab -l.

La parola importante qui è terminale. crontab -i -r è un comando puramente interattivo. Se lo standard input non è collegato a un terminale, quindi in uno script, in un cron job o in una riga singola come ssh host "crontab -i -r", la richiesta di conferma gira all'infinito: crontab: really delete root's crontab? (y/n) Please enter Y or N: Please enter Y or N: ... si ripete senza limite e in pochi secondi scrive centinaia di kilobyte di output, e la chiamata si chiude soltanto con un'interruzione dall'esterno. Per tutto ciò che è automatizzato usi quindi la variante non interattiva e fai prima un backup:

crontab -l > /root/crontab.bak     # salvare prima di eliminare
crontab -r                         # eliminare senza conferma
printf "y\n" | crontab -i -r       # mantenere la domanda, fornire la risposta

Se esistono /etc/cron.allow o /etc/cron.deny, sono questi file a decidere chi può creare crontab. Gli utenti interessati ricevono: You (username) are not allowed to use this program (crontab). Se /etc/cron.allow è presente, vale in modo esclusivo: ogni utente non elencato è bloccato.

Crontab utente, /etc/crontab e /etc/cron.d

Ci sono quattro posti in cui possono trovarsi le pianificazioni, e hanno formati diversi. È esattamente qui che nascono gli errori più difficili da individuare.

Crontab utente: cinque campi

Si gestisce con crontab -e, gira sotto l'utente a cui appartiene e non ha un campo utente. Se ce ne scrivi comunque uno, cron prova a eseguire il nome utente come comando e nella mail o nel log trovi:

/bin/sh: 1: root: not found

/etc/cron.d: sei campi

I file in /etc/cron.d/ sono crontab di sistema e hanno un campo utente fra i campi temporali e il comando. È il posto giusto per i job che appartengono a un'applicazione o a un sistema di gestione della configurazione, perché ogni file può essere sostituito singolarmente.

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=""

0 3 * * * root /usr/local/bin/backup.sh >> /var/log/kh-backup.log 2>&1

Se dimentichi il campo utente, cron interpreta la prima parola del comando come nome utente e la riga fallisce in silenzio, perché quell'utente non esiste.

Tre regole per /etc/cron.d/ vengono violate di continuo:

  • Il nome del file non può contenere un punto. Sono ammessi soltanto lettere, cifre, trattini bassi e trattini. backup.cron o kh-backup.sh vengono ignorati senza alcun avviso. Il motivo è la gestione dei pacchetti: in questo modo residui come .dpkg-dist o .dpkg-old non vengono mai eseguiti. Chiama il file semplicemente kh-backup.
  • Permessi e proprietario devono essere corretti. Ci si aspetta root:root e modo 0644. Altrimenti nel log compaiono messaggi come (*system*) WRONG FILE OWNER, (*system*) BAD FILE MODE oppure un avviso su un modo insicuro, scrivibile dal gruppo o da altri. In quel caso il job non parte.
  • Il file deve terminare con un'interruzione di riga. cron pretende che ogni voce si chiuda con un newline. Se l'ultima riga finisce senza newline, viene saltata, e per giunta in silenzio: nella controprova con un file per il resto identico ma privo di newline finale il job non è partito neanche una volta, senza messaggio di errore e senza riga di log. Chi genera il file con echo -n, con un printf senza \n finale o da un template senza riga vuota in fondo perde esattamente l'ultimo job.
chown root:root /etc/cron.d/kh-backup
chmod 0644 /etc/cron.d/kh-backup

/etc/crontab e le directory cron.*

Anche /etc/crontab ha sei campi e appartiene alla distribuzione. Modificalo solo se sai perché lo stai facendo. Tramite run-parts richiama le directory /etc/cron.hourly, cron.daily, cron.weekly e cron.monthly. Gli script che stanno lì hanno bisogno del bit di esecuzione e allo stesso modo non possono avere un punto nel nome. Un backup.sh in /etc/cron.daily/ non viene mai eseguito, un backup sì. Puoi verificarlo senza eseguire niente:

run-parts --test /etc/cron.daily

Vengono elencati solo gli script che run-parts avvierebbe davvero. Se il tuo manca dalla lista, la causa è il nome oppure il bit di esecuzione.

Perché in cron il PATH è diverso

È di gran lunga la causa più frequente del classico "gira nella shell, ma non in cron". cron non avvia una login shell. Non vengono letti né ~/.bashrc~/.profile/etc/profile. Per le crontab utente il cron di Debian imposta un percorso di ricerca minimo:

PATH=/usr/bin:/bin

Mancano quindi /usr/local/bin e /usr/sbin. Su Debian 12, Debian 13, Ubuntu 22.04 e Ubuntu 24.04 /usr/bin e /usr/sbin restano directory separate, il merge di /usr riguarda solo /bin e /sbin. Tutto quello che hai messo tu in /usr/local/bin, tutto ciò che arriva da pip install, tutto ciò che viene da un Node installato con nvm e gli strumenti di sistema come ufw o iptables per cron semplicemente non esistono. Il messaggio di errore è allora:

/bin/sh: 1: backup.sh: not found

La seconda parte della trappola: cron imposta SHELL=/bin/sh. Su Debian e Ubuntu /bin/sh è un collegamento a dash, non a bash. Ogni costrutto tipico di bash nell'intestazione dello script o direttamente nella riga della crontab fallisce:

/bin/sh: 1: [[: not found
/bin/sh: 1: source: not found
/bin/sh: 1: Syntax error: "(" unexpected

Tre contromisure, in quest'ordine:

  1. Usare percorsi assoluti. /usr/local/bin/backup.sh invece di backup.sh, /usr/bin/php invece di php. È la variante più robusta, perché funziona indipendentemente da qualsiasi variabile d'ambiente. Il percorso però leggilo, invece di scriverlo a memoria: command -v date su Debian 13, Debian 12, Ubuntu 24.04 e Ubuntu 22.04 restituisce /usr/bin/date, mentre su sistemi più vecchi come Debian 11 restituisce /bin/date. Una riga con un percorso sbagliato viene accettata da crontab senza commenti e senza avvisi, fallisce solo a runtime e per giunta in silenzio. Per controllare, una volta crontab -l e una volta env -i /bin/sh -c "/usr/bin/date", che segnala subito un percorso sbagliato con not found.
  2. Impostare PATH e SHELL all'inizio della crontab. Le assegnazioni valgono per tutte le righe successive. Importante: cron non espande le variabili, PATH=$PATH:/opt/bin non funziona, scrivi il percorso per intero.
  3. Usare uno script wrapper. La riga della crontab richiama solo lo script, e lo script imposta il proprio ambiente.
#!/bin/bash
set -euo pipefail
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
cd /srv/app
exec ./do-the-work.sh

Se vuoi sapere che cosa cron passa davvero al tuo job, fattelo mostrare. Inserisci per un minuto:

* * * * * /usr/bin/env > /tmp/cron-env.txt 2>&1

Solo questa strada mostra l'ambiente che cron imposta realmente. Una shell ricostruita con env -i ci va soltanto vicino, perché porta con sé il proprio percorso predefinito. Poi confronti /tmp/cron-env.txt con il tuo ambiente interattivo. Di solito saltano all'occhio non solo PATH e SHELL, ma anche le variabili di locale mancanti. Senza LANG tutto gira nella locale C, il che cambia l'ordinamento, i formati di data e la resa dei caratteri accentati. Gli script che contano su LANG=de_DE.UTF-8 in cron si comportano in modo diverso. Manca anche l'ssh-agent, per cui i job che accedono via SSH hanno bisogno di una chiave senza passphrase e di un -i esplicito.

L'ultima piccola cattiveria di questa categoria: il segno di percentuale. In una riga di crontab un % non mascherato viene trasformato in un'interruzione di riga, e tutto ciò che segue arriva al comando come standard input. I formati di data vanno quindi mascherati.

0 2 * * * /usr/bin/tar -czf /backup/web-$(date +\%F).tar.gz /var/www

Reindirizzare l'output, mail e "No MTA installed"

Per impostazione predefinita cron invia per mail al proprietario della crontab tutto ciò che un job scrive su stdout o stderr. Su un server senza sistema di posta questo finisce nel log:

(CRON) info (No MTA installed, discarding output)

Non è un errore del job. Significa solo che è stato prodotto dell'output e che nessuno ha potuto riceverlo. Un job silenzioso non genera questa riga. È quindi persino un segnale utile: se compare all'improvviso, il tuo job ha iniziato a scrivere qualcosa, di solito un messaggio di errore.

Per il reindirizzamento ci sono tre schemi sensati:

# tutto in un file di log, errori compresi
0 3 * * * /usr/local/bin/backup.sh >> /var/log/kh-backup.log 2>&1

# output normale scartato, errori ancora per mail
0 3 * * * /usr/local/bin/backup.sh > /dev/null

# tutto nel journal, con etichetta chiara
0 3 * * * /usr/local/bin/backup.sh 2>&1 | /usr/bin/logger -t kh-backup

La variante > /dev/null 2>&1 è molto diffusa e altrettanto pericolosa: butta via anche tutti i messaggi di errore. Un job che fallisce da quattro mesi ha allora esattamente lo stesso aspetto di uno che funziona. Se vuoi rinunciare alle mail, imposta piuttosto MAILTO="" all'inizio della crontab e scrivi l'output in un file. Se invece vuoi le mail a un indirizzo preciso, imposti MAILTO=alerts@example.org, ma in tal caso serve un sistema di posta installato.

I file di log personali in /var/log/ crescono senza limiti. Predisponi una piccola regola in /etc/logrotate.d/, altrimenti prima o poi è proprio il cron job di maggior successo a riempirti il disco.

Come capire che è andato davvero a buon fine

Qui la guida veloce si separa dalla verifica solida. cron registra l'avvio di un job. Non registra né la fine né il codice di uscita. Una riga CMD nel log dimostra quindi solo che cron ha avviato la shell, non che il tuo script abbia funzionato.

I log si leggono in modo diverso a seconda del sistema:

journalctl -u cron --since "30 min ago"
journalctl -t CRON --since today

Su Debian 12 e Debian 13 è l'unica via, perché da bookworm rsyslog non viene più installato di default. Un file /var/log/syslog lì di norma non esiste più. Chi lo cerca senza trovarlo conclude a torto che il job non sia partito.

Su Ubuntu 22.04 e 24.04 rsyslog di solito è presente, lì funziona anche:

grep CRON /var/log/syslog

Un file /var/log/cron.log dedicato non esiste di serie in nessuna delle quattro versioni. La regola corrispondente in /etc/rsyslog.d/50-default.conf è commentata. Non cercare quel file, esiste solo se qualcuno lo ha attivato.

Una prova pulita arriva quindi dal job stesso. Fai in modo che lo script scriva alla fine un timestamp e il codice di uscita:

#!/bin/bash
set -euo pipefail
trap 'echo "$(date -Is) kh-backup terminato, exit $?" >> /var/log/kh-backup.log' EXIT
# il lavoro vero e proprio

Così hai tre prove invece di una: la riga CRON nel journal (cron ha avviato), la riga finale nel tuo file di log (lo script è arrivato in fondo) e il codice di uscita (si è chiuso in modo pulito). Il job funziona solo quando tutte e tre coincidono.

Se un job può durare più del suo intervallo, proteggilo anche dalla sovrapposizione. Altrimenti prima o poi partono dieci istanze in parallelo e mettono in ginocchio il server:

*/5 * * * * /usr/bin/flock -n /var/lock/kh-sync.lock /usr/local/bin/sync.sh

flock -n termina subito se un'istanza è già in esecuzione. Lo strumento si trova in util-linux ed è presente su tutti e quattro i sistemi.

@reboot e perché systemd è quasi sempre meglio

Con @reboot si può eseguire un comando all'avvio. Esistono inoltre @daily, @hourly, @weekly, @monthly e @yearly, che sostituiscono ciascuno i cinque campi temporali.

@reboot /usr/local/bin/start-app.sh

Sembra comodo e ha tre difetti seri:

  • Il momento non è "dopo il boot", ma "quando parte cron". Che in quell'istante rete, database o un mount siano pronti è pura fortuna. Il rimedio abituale è uno sleep 30 davanti, che però sposta soltanto il problema.
  • Un riavvio del servizio cron fa scattare di nuovo @reboot. Un systemctl restart cron, per esempio dopo un aggiornamento dei pacchetti, avvia la tua applicazione una seconda volta, anche se la prima è ancora in esecuzione.
  • Non esiste alcun controllo. Nessun codice di uscita, nessun riavvio in caso di crash, nessuno stato.

Per tutto ciò che deve restare in esecuzione serve un servizio systemd, non un cron job. Per le attività ricorrenti un timer è la scelta migliore. Bastano due file:

[Unit]
Description=Backup KernelHost

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
[Unit]
Description=Backup KernelHost giornaliero

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
RandomizedDelaySec=900

[Install]
WantedBy=timers.target

Ad essere attivato è il timer, non il servizio:

systemctl daemon-reload
systemctl enable --now kh-backup.timer
systemctl list-timers kh-backup.timer

I tre vantaggi decisivi rispetto a cron: Persistent=true recupera al riavvio successivo un'esecuzione mancata, mentre cron la salta senza dire niente. Il codice di uscita finisce in systemctl status kh-backup.service, quindi vedi se ha funzionato senza doverti scrivere un logging tuo. E l'output completo è disponibile con journalctl -u kh-backup.service, senza reindirizzamenti e senza mail. Il percorso di ricerca, tra l'altro, con systemd è più generoso che con cron e contiene di default anche /usr/local/bin, ma senza un intervento tuo resta comunque scarno.

Per le attività di avvio sostituisci @reboot con un servizio dalle dipendenze chiare:

[Unit]
After=network-online.target
Wants=network-online.target

Così il tuo job parte con certezza solo quando la rete è davvero attiva. Trovi altre informazioni sulle unit e sulla loro struttura in creare un servizio systemd.

Fuso orario, UTC e ora legale

cron calcola sempre nel fuso orario di sistema che deriva da /etc/localtime. Su molti server e in quasi tutte le immagini cloud quel fuso è UTC. Un job impostato su 0 3 * * * in estate parte allora alle 05:00 dell'ora legale dell'Europa centrale, non alle 03:00. Verifica per prima cosa con che cosa hai a che fare:

readlink -f /etc/localtime
timedatectl show -p Timezone --value
date
date -u

readlink -f /etc/localtime indica il percorso in /usr/share/zoneinfo e quindi il fuso realmente in vigore, per esempio /usr/share/zoneinfo/Etc/UTC oppure /usr/share/zoneinfo/Europe/Vienna. Funziona su tutte e quattro le versioni e anche dove systemd non è in esecuzione. timedatectl show -p Timezone --value restituisce la stessa informazione in forma breve e leggibile da una macchina, ma presuppone systemd.

Quello che invece dovresti toglierti dalle abitudini è guardare in /etc/timezone. Debian 13 non distribuisce più questo file, un cat /etc/timezone lì finisce con cat: /etc/timezone: No such file or directory, e nemmeno un'installazione di tzdata lo riporta indietro. Su Debian 12, Ubuntu 24.04 e Ubuntu 22.04 esiste ancora, quindi la risposta cambia a seconda della versione. In ogni caso fa fede soltanto il collegamento simbolico /etc/localtime: un valore scritto a mano in /etc/timezone non cambia niente nel fuso orario di sistema e quindi neanche nel momento in cui scattano i tuoi job.

Il fuso orario di sistema lo puoi cambiare in qualsiasi momento, dopodiché conviene riavviare cron perché il servizio recepisca la modifica con certezza:

timedatectl set-timezone Europe/Vienna
systemctl restart cron

Sui sistemi senza systemd attivo imposti invece direttamente il collegamento simbolico, il risultato è lo stesso e si controlla subito con readlink -f /etc/localtime:

ln -sf /usr/share/zoneinfo/Europe/Vienna /etc/localtime

Ora il punto che in molte guide è sbagliato: il cron di Debian e Ubuntu non conosce CRON_TZ. Questa variabile viene da cronie, il cron di Red Hat, Fedora e AlmaLinux. Su Debian 13, Debian 12, Ubuntu 24.04 e Ubuntu 22.04 non esiste un fuso orario per utente o per crontab. Se lì metti TZ=Europe/Vienna nella crontab, l'effetto riguarda esclusivamente l'ambiente dei comandi eseguiti, un date nello script mostra quindi l'ora di Vienna. Il momento di attivazione, invece, resta del tutto inalterato e continua a seguire il fuso orario di sistema. Chi non ci fa caso si ritrova con un job apparentemente configurato bene che gira comunque con due ore di scarto.

Restano tre strade pulite: impostare correttamente il fuso orario di sistema, convertire tu stesso gli orari in UTC, oppure passare a un timer systemd, che accetta un fuso orario direttamente nella pianificazione. La verifica si può fare senza rischi:

systemd-analyze calendar "Mon..Fri 03:00 Europe/Vienna"

L'output indica la prossima attivazione come data concreta. È il controllo più affidabile che puoi fare prima di attivare qualcosa.

Sull'ora legale ancora un consiglio pratico: non pianificare job fra le 02:00 e le 03:00. A marzo quell'ora non esiste, a ottobre esiste due volte. A seconda del job questo significa un'esecuzione saltata oppure un'esecuzione doppia, entrambe una volta l'anno ed entrambe difficili da riprodurre. Le 01:30 o le 03:30 sono alternative senza sorprese. Con i timer systemd aiuta in più Persistent=true, così un'esecuzione saltata viene recuperata.

Diagnosi rapida in sette passi

Se un cron job non parte, segui questa lista nell'ordine. Copre praticamente tutti i casi.

  1. Il servizio è attivo? systemctl status cron. Nessun servizio, nessun job.
  2. La voce è arrivata? crontab -l per i job utente, altrimenti guarda il file in /etc/cron.d/. Controlla il nome del file senza punto, il modo 0644, il proprietario root e l'interruzione di riga finale.
  3. cron lo ha davvero avviato? journalctl -t CRON --since today. Se manca la riga CMD, il problema è la pianificazione o il file, non lo script.
  4. Il numero di campi è giusto? Cinque campi nella crontab utente, sei in /etc/cron.d e /etc/crontab.
  5. Percorsi assoluti? Inserisci ogni comando e ogni script con il percorso completo, e ricava prima quel percorso con command -v invece di scriverlo a mano.
  6. Ambiente verificato? Inserisci una volta sola * * * * * /usr/bin/env > /tmp/cron-env.txt 2>&1, aspetta un minuto, confronta.
  7. Testare nel contesto di cron. Non eseguire lo script nella tua shell, ma con un ambiente vuoto: env -i PATH=/usr/bin:/bin /bin/sh -c '/usr/local/bin/backup.sh'. Il percorso di ricerca lo indichi volutamente in modo esplicito. env -i /bin/sh da solo svuota sì l'ambiente, ma subito dopo dash si imposta il proprio percorso predefinito /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin, che è più generoso di quello di cron. Proprio gli errori di cui parliamo qui resterebbero così invisibili. Se la chiamata fallisce, hai trovato la causa, senza dover aspettare la prossima attivazione.

L'ultimo punto è il più prezioso. Quasi ogni cron job che "inspiegabilmente" non funziona fallisce in modo riproducibile non appena lo si avvia con un ambiente vuoto. Così un enigma che si presenta solo ogni 24 ore diventa un errore che puoi risolvere in dieci secondi.

Chi esegue backup regolari tramite cron job dovrebbe mettere in sicurezza anche il sistema di destinazione. Le indicazioni utili si trovano nel nostro articolo sulla messa in sicurezza di un server Linux.

Domande frequenti

Perché il mio script gira nella shell ma non come cron job?
Nella stragrande maggioranza dei casi la causa è l'ambiente. cron non avvia una login shell, quindi non legge né ~/.bashrc né /etc/profile, e per le crontab utente imposta soltanto PATH=/usr/bin:/bin. Mancano così /usr/local/bin e /usr/sbin. Inoltre SHELL è impostata su /bin/sh, che su Debian e Ubuntu è dash e non capisce la sintassi di bash. Prova il job esattamente in questo ambiente: env -i PATH=/usr/bin:/bin /bin/sh -c, seguito dal percorso dello script fra apici singoli ('/percorso/dello/script.sh'), e fallirà subito in modo riproducibile. Il percorso di ricerca lo indichi espressamente, perché un semplice env -i /bin/sh imposta il percorso predefinito più generoso di dash e nasconde proprio questo errore.
Che cosa significa il messaggio No MTA installed, discarding output?
Il tuo job ha scritto qualcosa su stdout o stderr, cron voleva recapitarlo per mail, ma non c'è nessun sistema di posta installato. Il job in sé non ne risente e può essere comunque andato a buon fine. O reindirizzi l'output in un file di log, oppure imposti MAILTO="" all'inizio della crontab. Attenzione: il messaggio compare spesso solo quando un job fino a quel momento silenzioso inizia a produrre errori.
Debian o Ubuntu supportano la variabile CRON_TZ?
No. CRON_TZ viene da cronie, il cron di Red Hat e Fedora. Debian 13, Debian 12, Ubuntu 24.04 e Ubuntu 22.04 usano la variante Debian di Vixie Cron e non conoscono un fuso orario per crontab. Un TZ= nella crontab agisce solo sull'ambiente dei comandi, non sul momento di attivazione. Imposta il fuso orario di sistema con timedatectl, converti gli orari in UTC oppure usa un timer systemd, che accetta un fuso orario nell'espressione OnCalendar.
Perché il mio file in /etc/cron.d viene ignorato?
Quasi sempre per via del nome del file. Sono ammessi solo lettere, cifre, trattini bassi e trattini, un punto rende il file invisibile a cron. backup.cron viene quindi ignorato, backup no. Altri motivi: permessi sbagliati (servono root:root e 0644), un campo utente mancante fra i campi temporali e il comando, oppure un'ultima riga priva di interruzione di riga finale.
Dove trovo i log di cron su Debian 12 e 13?
Con journalctl, perché da Debian 12 rsyslog non viene più installato di default e un file /var/log/syslog lì di solito non esiste. Usa journalctl -u cron oppure journalctl -t CRON. Su Ubuntu 22.04 e 24.04 funziona anche grep CRON /var/log/syslog. Un file /var/log/cron.log dedicato non esiste di serie su nessuno dei quattro sistemi.
Una riga CMD nel log dimostra che il job è riuscito?
No. cron registra solo l'avvio di un job, né la fine né il codice di uscita. Uno script può essere andato in crash un secondo dopo e la riga di log avrebbe lo stesso aspetto. Per una prova reale fai scrivere allo script stesso un timestamp e il codice di uscita in un file di log, per esempio con un trap su EXIT, oppure passi a un timer systemd, dove systemctl status mostra il codice di uscita.
Meglio usare @reboot o un servizio systemd?
In quasi tutti i casi un servizio systemd. @reboot non parte dopo il boot, ma all'avvio del servizio cron, senza alcuna garanzia che rete o database siano pronti. Inoltre scatta di nuovo a ogni systemctl restart cron, per esempio dopo un aggiornamento dei pacchetti. Un servizio con After=network-online.target e Wants=network-online.target risolve entrambi i problemi e fornisce in più lo stato e il codice di uscita.

Cron Crontab systemd Amministrazione Linux Debian Ubuntu Automazione Gestione server