Linux concept: cron
Bijna elke server draait werk waar niemand naar kijkt. Een backup om drie uur 's nachts, een certificaat dat twee keer per maand vernieuwd wordt, een cache die elk uur geleegd wordt. Het programma dat dat regelt doet hetzelfde werk sinds 1979, op vrijwel dezelfde manier, en het heet cron. Het is ook een van de makkelijkste onderdelen van een Linux-systeem om subtiel fout te doen, want een kapotte cron-taak en een werkende cron-taak zien er precies hetzelfde uit: stil.
1. De basis
cron is een daemon. Hij start als de machine opstart en doet dan iets bijna komisch eenvoudigs: een keer per minuut wordt hij wakker, kijkt op de klok, en vergelijkt dat met een lijst regels. Elke regel die op de huidige minuut past wordt uitgevoerd. Daarna gaat hij weer slapen tot de minuut om is.
Dat is het hele ontwerp. Er is geen wachtrij, geen planner in de slimme zin van het woord, en niets wordt ingehaald. Stond de machine om 03:00 uit, dan is de taak van 03:00 simpelweg niet gebeurd.
1.1 Controleren of hij draait
Bevestig voor alles eerst dat de daemon er is. Op Debian en Ubuntu heten zowel het pakket als de service cron:
$ systemctl is-active cron
active
$ systemctl is-enabled cron
enabled
Op systemen uit de Red Hat-familie heet het pakket cronie en de service crond. De regels in dit artikel gelden voor allebei, omdat beide van hetzelfde oorspronkelijke programma afstammen.
1.2 De drie plaatsen waar taken staan
Dit is het deel dat de meeste verwarring geeft. Er is geen enkele centrale lijst met cron-taken; er zijn drie soorten plaatsen, en ze hebben niet hetzelfde formaat.
| Waar | Van wie | Formaat |
|---|---|---|
/var/spool/cron/crontabs/<gebruiker> |
Een gebruiker | Vijf tijdvelden, dan het commando. Bewerk je met crontab -e, nooit met de hand. |
/etc/crontab en /etc/cron.d/* |
Het systeem, meestal pakketten | Vijf tijdvelden, dan een gebruikersnaam, dan het commando. Bewerk je rechtstreeks. |
/etc/cron.{hourly,daily,weekly,monthly}/ |
Het systeem | Helemaal geen tijdvelden. Alleen uitvoerbare scripts, gedraaid door run-parts. |
Het zesde veld in de middelste groep is verreweg de meest voorkomende oorzaak van "mijn cron-taak draait niet". Een regel die je uit een gebruikerscrontab naar /etc/cron.d kopieert, wordt gelezen met het eerste woord van het commando als gebruikersnaam, en faalt.
Je ziet alle drie op een normale machine:
$ crontab -l # your own jobs
$ cat /etc/crontab # the system table
$ ls /etc/cron.d/ # one file per package
anacron e2scrub_all php sysstat
$ ls /etc/cron.daily/ # scripts, no schedule inside them
0anacron apt-compat dpkg logrotate man-db plocate
Naar bovenHet juiste mentale model: cron is een wekker, geen takenlijst. Hij gaat af op een moment in de tijd. Hij weet niet of de taak gelukt is, of hij nog van de vorige keer draait, of dat hij gemist werd terwijl de machine uit stond.
2. Waar komt de naam vandaan?
De naam wordt algemeen herleid tot het Griekse chronos, tijd. De auteur van het oorspronkelijke programma heeft de afleiding nooit opgeschreven, dus zie dat als de aanvaarde verklaring en niet als een gedocumenteerd feit. Over de namen eromheen bestaat geen twijfel:
cron the daemon that watches the clock
crontab cron TABle - both the file and the command that edits it
cron.d a drop-in directory of cron fragments
anacron "anachronistic" cron: for machines that are not always on
De d aan het eind van crond op Red Hat-systemen is het gebruikelijke Unix-achtervoegsel voor een daemon, hetzelfde als in sshd, httpd en systemd. Debian en Ubuntu hebben hem gewoon weggelaten.
Een naamgevingsdetail doet er in de praktijk toe. Omdat zowel het bestand als het commando crontab heet, heeft het naslagwerk er twee verschillende pagina's voor, in twee verschillende secties:
$ man 1 crontab # the command: -l, -e, -r
$ man 5 crontab # the file format: the five fields
Bijna alles waar mensen het web voor afzoeken staat in crontab(5), en bijna niemand opent hem.
3. Een korte geschiedenis
cron verscheen in Version 7 Unix in 1979. Die eerste versie was nog eenvoudiger dan die van vandaag: een systeembrede tabel, gelezen door een daemon die elke minuut wakker werd. Er waren geen crontabs per gebruiker en geen crontab-commando, omdat er slechts een enkel bestand was en alleen de beheerder het mocht bewerken.
De versie die jij vrijwel zeker draait is in 1987 geschreven door Paul Vixie. Het is nog steeds degene die vandaag meegeleverd wordt, en het versienummer is verfrissend eerlijk over zijn leeftijd:
$ dpkg -l cron | tail -1
ii cron 3.0pl1-184ubuntu2 amd64 process scheduling daemon
Die "3.0pl1" is Vixie cron 3.0, patch level 1. De 184 erachter is Debians aantal patches, en dat vertelt je waar het meeste werk van de afgelopen dertig jaar werkelijk in ging zitten.
| Periode | Mijlpaal |
|---|---|
| 1979 | Version 7 Unix levert cron: een tabel, een daemon, een minuut per keer |
| Jaren 80 | System V voegt crontabs per gebruiker toe, het commando crontab, en cron.allow / cron.deny |
| 1987 | Paul Vixie brengt zijn cron uit, met reeksen, stappen, namen en de @daily-afkortingen |
| Jaren 90-2000 | Debian voegt /etc/cron.d toe, PAM-ondersteuning, afhandeling van zomertijd, en maakt crontab setgid in plaats van setuid root |
| Onderweg | anacron dekt het geval dat cron niet kan: machines die uit staan |
| Jaren 2010 | systemd-timers verschijnen; pakketten gaan allebei meeleveren, met de cron-versie uitgeschakeld |
De Debian-wijzigingen verdienen nog een zin, want ze verklaren een permissie die er alarmerend uitziet tot je weet waarom:
$ ls -l /usr/bin/crontab
-rwxr-sr-x 1 root crontab 39664 Mar 31 2024 /usr/bin/crontab
Die s in de groepskolom is setgid. Het commando draait als groep crontab, en dat is de enige groep die in de spool-map mag schrijven. In de oorspronkelijke Vixie cron was dit programma setuid root; Debian versmalde het tot een groep die precies een ding kan. Daarom moet je crontab -e gebruiken in plaats van zelf het spool-bestand te bewerken: het bestand is niet van jou om in te schrijven.
4. Eenvoudige toepassingen
4.1 De vijf velden
Een regel in een gebruikerscrontab bestaat uit vijf tijdvelden en daarna al het overige, en dat is het commando. Het commentaarblok dat Debian in /etc/crontab meelevert is er het beste schema van, en het staat al op je machine:
# .---------------- minute (0 - 59)
# | .------------- hour (0 - 23)
# | | .---------- day of month (1 - 31)
# | | | .------- month (1 - 12) OR jan,feb,mar,apr ...
# | | | | .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,...
# | | | | |
# * * * * * command to be executed
Lees een regel van rechts naar links en hij zegt zichzelf hardop. Deze draait elke dag om 03:30:
30 3 * * * /usr/local/bin/backup.sh
Een sterretje betekent "elke waarde van dit veld". De regel hierboven is dus: minuut 30, uur 3, elke dag van de maand, elke maand, elke dag van de week.
Zowel 0 als 7 betekent zondag in het veld voor de dag van de week, een historisch compromis tussen twee Unix-families die het oneens waren. Je mag ook Engelse afkortingen van drie letters schrijven, in willekeurige schrijfwijze:
0 4 * * 1 # Monday at 04:00
0 4 * * mon # exactly the same
0 4 * * MON # also the same, case does not matter
Namen mogen niet in reeksen of lijsten, dus mon-fri werkt wel maar een gemengde lijst van namen en getallen niet. Bij twijfel: gebruik getallen.
4.2 Je eigen crontab bewerken
Drie commando's dekken bijna alles:
| Commando | Doet |
|---|---|
crontab -l |
Toon je crontab (kort voor list, lijst) |
crontab -e |
Bewerk hem in $EDITOR (kort voor edit) |
crontab -r |
Verwijder hem volledig (kort voor remove) |
crontab -u gebruiker -l |
Werk aan de crontab van iemand anders (alleen root) |
crontab bestand |
Vervang je crontab door de inhoud van een bestand |
Kijk eens naar -e en -r op een toetsenbord. Ze liggen naast elkaar, ze doen heel verschillende dingen, en er is geen ongedaan maken. Dit is een oprecht beroemde manier om een middag kwijt te raken. Twee gewoontes beschermen je:
$ crontab -l > ~/crontab.backup # before you touch anything
$ crontab -i -r # -i asks for confirmation first
De laatste rij van de tabel is om een andere reden het kennen waard: crontab bestand betekent dat je taken in versiebeheer kunnen staan. Houd het echte bestand in een repository, en installeer het met een enkel commando:
$ crontab -n deploy.cron # dry run: check the syntax, install nothing
$ crontab deploy.cron # replace the live crontab with this file
De optie -n (kort voor no action, niets doen) is weinig bekend en is precies wat je wilt in een deployscript: hij ontleedt het bestand, meldt elke fout, en stopt zonder iets te wijzigen.
4.3 Waar de uitvoer heen gaat
Dit is het belangrijkste dat je over cron moet begrijpen, en het heeft niets met de planning te maken.
Alles wat een cron-taak afdrukt wordt behandeld als een foutmelding. Als een taak naar standaarduitvoer of standaardfout schrijft, mailt cron die tekst naar de eigenaar van de crontab. Drukt de taak niets af, dan zegt cron niets.
Op een desktop of een moderne server staat meestal geen mailsysteem geïnstalleerd, dus die mail gaat helemaal nergens heen. Je taak draait, drukt een fout af, en die fout wordt weggegooid. Daarom zijn kapotte cron-taken stil.
De oplossing is dat je zelf bepaalt waar de uitvoer heen gaat:
30 3 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
Lees het staartje van die regel goed, want de volgorde doet ertoe:
| Deel | Betekent |
|---|---|
>> |
Toevoegen aan het logbestand in plaats van het elke nacht overschrijven |
/var/log/backup.log |
Hier gaat standaarduitvoer heen |
2>&1 |
Stuur standaardfout naar dezelfde plek waar standaarduitvoer al heen gaat |
In plaats daarvan 2>&1 >> bestand schrijven is een klassieke fout: dat richt de foutuitvoer op de terminal die cron je gaf (en dat is niets), en pas daarna wordt standaarduitvoer omgeleid. De omleiding moet eerst.
Je ziet ook op veel regels op internet > /dev/null 2>&1 staan. Dat legt de taak volledig het zwijgen op. Dat is de juiste keuze voor een taak die zelf logt, en de verkeerde keuze voor al het andere, want je gooit de enige waarschuwing weg die je zou krijgen.
4.4 De afkortingen
Vixie cron voegde acht bijzondere woorden toe die alle vijf de velden vervangen:
| Woord | Hetzelfde als | Betekenis |
|---|---|---|
@hourly |
0 * * * * |
Aan het begin van elk uur |
@daily / @midnight |
0 0 * * * |
Elke dag om middernacht |
@weekly |
0 0 * * 0 |
Zondag om middernacht |
@monthly |
0 0 1 * * |
De eerste van de maand om middernacht |
@yearly / @annually |
0 0 1 1 * |
1 januari om middernacht |
@reboot |
- | Een keer, als cron zelf start |
Ze lezen prettig, maar er zit een addertje onder het gras dat je moet kennen voordat je ze op een drukke server gebruikt: elke @daily-taak op de machine gaat af op precies 00:00, en elke @hourly-taak op precies minuut 0. Belasting spreiden is een goede reden om de velden uit te schrijven en een oneven minuut te kiezen.
@reboot heeft een waarschuwing in zijn eigen man-pagina. Hij draait als de cron-daemon start, en dat kan tijdens het opstarten voor het netwerk, de database of het bestandssysteem zijn dat je taak nodig heeft. Voor alles met echte afhankelijkheden is een systemd-unit het juiste gereedschap, en paragraaf 6.10 laat zien waarom.
5. Gemiddelde toepassingen
5.1 Reeksen, lijsten en stappen
Elk van de vijf velden accepteert meer dan een getal of een sterretje:
| Schrijfwijze | Naam | Voorbeeld | Betekent |
|---|---|---|---|
a-b |
Reeks, inclusief | 8-11 |
Uur 8, 9, 10 en 11 |
a,b,c |
Lijst | 0,15,30,45 |
Vier keer per uur |
*/n |
Stap over het hele veld | */5 |
Elke vijfde waarde: 0, 5, 10, ... |
a-b/n |
Stap binnen een reeks | 9-17/2 |
9, 11, 13, 15, 17 |
Reeksen en lijsten mag je vrij combineren, wat oudere Unix-crons niet toestonden:
*/10 9-17 * * 1-5 /usr/local/bin/check.sh # every 10 min, office hours, weekdays
0 2 1,15 * * /usr/local/bin/report.sh # 02:00 on the 1st and the 15th
15 */6 * * * /usr/local/bin/sync.sh # 00:15, 06:15, 12:15, 18:15
In stapwaarden schuilt een valkuil die paragraaf 7.4 uit elkaar haalt. Kort gezegd: een stap telt binnen de reeks van zijn eigen veld en begint dan opnieuw, dus */7 in het minuutveld geeft je geen gelijke tussenpozen van zeven minuten over de uurgrens heen.
5.2 Systeemtaken: het zesde veld
/etc/crontab en elk bestand in /etc/cron.d gebruiken dezelfde vijf tijdvelden, en daarna een gebruikersnaam voor het commando. Daarmee kan een bestand werk als verschillende gebruikers plannen:
# /etc/cron.d/mysite
# m h dom mon dow user command
17 * * * * www-data /usr/local/bin/cache-warm.sh
30 3 * * * root /usr/local/bin/backup.sh
Bestanden in /etc/cron.d zijn om drie redenen de juiste plek voor alles wat een server nodig heeft. Het zijn gewone bestanden, dus configuratiebeheer en versiebeheer kunnen ze uitrollen. Ze overleven het verwijderen van een gebruiker. En cron merkt wijzigingen vanzelf op, dus er valt niets te herladen.
Er gelden vier regels voor deze bestanden, en elke regel die je breekt betekent stil falen:
- Ze moeten eigendom zijn van
rooten niet schrijfbaar voor groep of anderen. - Ze moeten het gebruikersveld bevatten. Dat vergeten is de meest gemaakte fout in deze map.
- Ze erven geen omgevingsinstellingen uit
/etc/crontab. Elk bestand staat op zichzelf. - De bestandsnaam mag geen punt bevatten. Paragraaf 7.3 laat zien wat er gebeurt als dat wel zo is.
Hier is een echt voorbeeld van deze machine, meegeleverd door het PHP-pakket:
$ cat /etc/cron.d/php
# Look for and purge old sessions every 30 minutes
09,39 * * * * root [ -x /usr/lib/php/sessionclean ] && if [ ! -d /run/systemd/system ]; then /usr/lib/php/sessionclean; fi
Let op de test in het midden. Op een machine met systemd doet deze cron-taak bewust niets, omdat een systemd-timer hetzelfde werk doet. Dat is het onthouden waard wanneer je probeert uit te zoeken waarom een taak die er duidelijk staat nooit lijkt te draaien.
5.3 De run-parts-mappen
De vier mappen /etc/cron.hourly, cron.daily, cron.weekly en cron.monthly bevatten helemaal geen planning. Ze bevatten uitvoerbare scripts. /etc/crontab plant een programma genaamd run-parts dat alles erin uitvoert:
$ cat /etc/crontab
17 * * * * root cd / && run-parts --report /etc/cron.hourly
25 6 * * * root test -x /usr/sbin/anacron || { cd / && run-parts --report /etc/cron.daily; }
47 6 * * 7 root test -x /usr/sbin/anacron || { cd / && run-parts --report /etc/cron.weekly; }
52 6 1 * * root test -x /usr/sbin/anacron || { cd / && run-parts --report /etc/cron.monthly; }
In die vier regels zijn drie dingen te zien. De dagelijkse, wekelijkse en maandelijkse runs worden volledig overgeslagen als anacron geïnstalleerd is, want dan is anacron de eigenaar. Alles draait standaard tussen 06:00 en 07:00, dus een machine die op dat moment uit staat draait zijn dagelijkse taken nooit. En de scripts draaien vanuit /, niet vanuit je thuismap.
Om er je eigen script in te zetten, maak je het uitvoerbaar en geef je het een naam zonder extensie:
$ sudo cp cleanup /etc/cron.daily/cleanup # note: no .sh
$ sudo chmod +x /etc/cron.daily/cleanup
Bevestig het daarna. run-parts --test toont precies de lijst scripts die uitgevoerd zouden worden, in volgorde, en voert er geen enkele uit:
$ run-parts --test /etc/cron.daily
/etc/cron.daily/0anacron
/etc/cron.daily/apport
/etc/cron.daily/apt-compat
/etc/cron.daily/dpkg
/etc/cron.daily/google-chrome
/etc/cron.daily/logrotate
/etc/cron.daily/man-db
/etc/cron.daily/plocate
/etc/cron.daily/slack
/etc/cron.daily/sysstat
Dit is de controle die je redt. Staat je script niet in die lijst, dan zal het nooit draaien, en paragraaf 7.3 legt de gebruikelijke reden uit. Let ook op dat 0anacron bewust vooraan sorteert: run-parts voert uit op naamvolgorde, dus een cijfer vooraan is hoe een pakket zorgt dat het als eerste gaat.
5.4 Uitvoer ergens nuttigs heen sturen
Twee omgevingsvariabelen bovenaan een crontab veranderen waar de uitvoer heen gaat. Ze gelden voor elke regel eronder:
MAILTO=Dit e-mailadres wordt beveiligd tegen spambots. JavaScript dient ingeschakeld te zijn om het te bekijken. # mail job output here instead of to the crontab owner
MAILTO="" # send no mail at all, for every job in this file
0 3 * * * /usr/local/bin/backup.sh
MAILTO helpt alleen als de machine ook echt mail kan versturen. Op een server zonder mailserver wordt de mail aangemaakt en daarna weggegooid. Wil je weten of een taak faalde, vertrouw er dan niet op. Log naar een bestand dat je kunt lezen, of laat de taak zich melden bij een monitoringdienst als hij klaar is.
Een logbestand waar je elke vijf minuten aan toevoegt groeit eeuwig door, en een volle schijf door een loggende cron-taak is een ongewoon vervelende manier om een server neer te halen. Waar je ook naartoe omleidt, het moet geroteerd worden. Zet een bestand in /etc/logrotate.d, naast die van de pakketten:
$ cat /etc/logrotate.d/myjob
/var/log/myjob.log {
rotate 14
daily
compress
missingok
notifempty
}
Het alternatief is helemaal geen bestand bezitten. Stuur de uitvoer door logger en het wordt gewone syslog, van een label voorzien zodat je het terugvindt, geroteerd door wat de systeemlogs toch al roteert:
0 3 * * * /usr/local/bin/backup.sh 2>&1 | /usr/bin/logger -t backup
$ journalctl -t backup --since today
De optie -t (kort voor tag, label) maakt dit de moeite waard: hij labelt elke regel zodat journalctl -t backup je die taak geeft en niets anders. Let wel op dat doorsturen naar logger de afsluitcode van het script achter de pipe verbergt, dus gebruik dit samen met het markeerbestand uit paragraaf 6.9 en niet in plaats daarvan.
5.5 Controleren of hij gedraaid heeft
cron logt naar syslog onder de faciliteit cron, en zijn kindprocessen hernoemen zichzelf naar hoofdletters CRON, en dat is waar je op zoekt:
$ journalctl -t CRON --since '-1 hour'
Aug 23 15:35:01 xps CRON[408129]: (root) CMD (command -v debian-sa1 > /dev/null && debian-sa1 1 1)
$ journalctl -u cron --since today # the daemon's own messages
$ grep CRON /var/log/syslog # on systems with rsyslog
Standaard logt cron alleen dat een taak gestart is. Hij logt de afsluitcode niet, en daarom is een log vol CMD-regels geen bewijs dat er iets gewerkt heeft. Dat kun je veranderen. De optie -L neemt een bitmasker, en 4 voegt gefaalde taken toe:
| Waarde | Logt |
|---|---|
| 1 | De start van elke taak (de standaard) |
| 2 | Het einde van elke taak |
| 4 | Elke taak die met een waarde ongelijk aan nul eindigt |
| 8 | Het procesnummer van elke taak |
| 15 | Alles hierboven |
De meeste handleidingen zeggen dat je EXTRA_OPTS in /etc/default/cron moet zetten. Op het huidige Ubuntu is dat advies achterhaald, en het bestand zegt dat zelf:
$ cat /etc/default/cron
# This file has been deprecated. Please add custom options for cron using
# $ systemctl edit cron.service
# or
# $ systemctl edit --full cron.service
De manier van nu is een systemd-drop-in. Draai sudo systemctl edit cron.service en voeg de twee regels hieronder toe. De lege ExecStart= is verplicht: die wist het bestaande commando voordat je een nieuw commando geeft, en zonder die regel weigert systemd de unit.
[Service]
ExecStart=
ExecStart=/usr/sbin/cron -f -P -L 15
Daarna sudo systemctl restart cron. Dit een dag aanzetten is vaak de snelste manier om een discussie te beslechten over de vraag of een taak gedraaid heeft.
6. Gevorderde toepassingen
6.1 De omgeving die een cron-taak echt krijgt
"Het werkt als ik het met de hand draai maar niet vanuit cron" is de meest gehoorde cron-klacht die er is, en de omgeving is bijna altijd de reden. In plaats van de bekende verhalen te herhalen, hier een meting. Deze crontab draaide op de machine waarop dit artikel geschreven is:
$ crontab -l
* * * * * /usr/bin/env > /tmp/cron-env.txt 2>&1
$ cat /tmp/cron-env.txt
HOME=/home/peter
LOGNAME=peter
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
LANG=en_US.UTF-8
SHELL=/bin/sh
PWD=/home/peter
Zes variabelen. Dat is de hele wereld waarin je taak draait. Drie details uit die lijst doen er meer toe dan de rest.
Het PATH-advies dat je gelezen hebt klopt waarschijnlijk niet meer. Elke cron-handleiding zegt dat cron je een uitgeklede PATH van /usr/bin:/bin geeft. Dat is hier niet gebeurd, en de reden is te zien in de servicedefinitie:
$ systemctl cat cron.service | grep ExecStart
ExecStart=/usr/sbin/cron -f -P $EXTRA_OPTS
De optie -P betekent "stel PATH niet in voor kindprocessen, laat het erven". Op dit systeem komt de waarde dus uit /etc/environment via PAM, en is het een volkomen normaal systeem-PATH.
De bruikbare les overleeft de correctie, en is scherper dan het bekende verhaal: cron geeft je een systeem-PATH, nooit jouw PATH. Vergelijk de twee op dezelfde machine:
$ echo $PATH # interactive shell
/bin:/home/peter/.local/bin:/usr/local/cuda/bin:/usr/local/sbin:/usr/local/bin:...
# plus a version manager, plus node_modules/.bin, plus more
Alles wat je .bashrc of .profile toevoegde is weg: ~/.local/bin, alles wat via nvm, rbenv of pyenv geïnstalleerd is, en elke map per project. Een taak die node of composer bij de kale naam aanroept werkt in je terminal en faalt onder cron, precies hierom.
SHELL is /bin/sh, niet bash. Op Debian en Ubuntu is /bin/sh dash, een kleinere, snellere en striktere shell. Alles wat bash-specifiek is faalt: [[ ... ]], arrays, source in plaats van ., ${var,,}. Het falen is vaak een enkele verwarrende regel in een log dat je niet leest.
Wat ontbreekt telt ook. Er is geen SSH_AUTH_SOCK, dus een taak die via SSH naar git pusht kan je agent niet gebruiken. Er is geen XDG_RUNTIME_DIR, dus systemctl --user en rootless containergereedschap gedragen zich vreemd. Er is geen DISPLAY, dus niets grafisch werkt.
Er zijn drie manieren om hiermee om te gaan, in oplopende mate van robuustheid:
# 1. Set what you need at the top of the crontab
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin:/home/peter/.local/bin
# 2. Use absolute paths everywhere, always
30 3 * * * /usr/bin/php /var/www/site/cli/joomla.php scheduler:run --all
# 3. Best: call a script, and let the script set up its own world
30 3 * * * /usr/local/bin/nightly.sh
De derde is degene om naar te grijpen. Een crontabregel van een regel die een script aanroept houdt de planning en het werk gescheiden, zet de logica in versiebeheer, en laat je precies hetzelfde met de hand draaien terwijl je aan het testen bent.
6.2 De werkmap is niet waar je script staat
In de meting hierboven staat een regel die meer storingen veroorzaakt dan je zou denken:
PWD=/home/peter
cron start je taak in de thuismap van de gebruiker als wie hij draait. Niet in de map waar het script staat, en niet in de map waar jij toevallig stond toen je de crontabregel schreef. Voor taken in /etc/cron.d is het de thuismap van de gebruiker uit het zesde veld, wat voor www-data op Debian /var/www is. Voor de run-parts-mappen is het /, omdat /etc/crontab voor elk van die regels cd / && zegt.
Elk script dat een bestand ten opzichte van zichzelf zoekt breekt:
require 'config.php'; # PHP, resolved against the working directory
. ./settings.sh # shell
source venv/bin/activate # python virtualenv
cat ../data/input.csv # anything at all
Alle vier werken als je het script vanuit zijn eigen map draait, en falen vanuit cron, met een foutmelding die naar een mail gaat die niemand leest. Er zijn twee goede oplossingen en een slechte:
# Acceptable: change directory in the crontab entry
0 3 * * * cd /var/www/site && /usr/bin/php cli/import.php
# Better: let the script find itself, so it works from anywhere
#!/bin/bash
cd "$(dirname "$0")" || exit 1
# Best: never use relative paths inside a scheduled script at all
De middelste is het waard om als gewoonte aan te nemen. Twee regels bovenaan elk script dat je plant halen deze hele categorie problemen weg, en het betekent ook dat je het script tijdens het testen vanuit je eigen thuismap kunt draaien.
6.3 Een taak opsporen die niet draait
"Mijn cron-taak werkt niet" zijn eigenlijk zes verschillende vragen, en die op volgorde beantwoorden vindt het probleem veel sneller dan gokken. Werk deze lijst af en stop bij de eerste stap die faalt.
1. Draait cron je crontab uberhaupt? Bewijs eerst dat het leidingwerk klopt. Installeer een taak die niet kan mislukken:
* * * * * /usr/bin/date >> /tmp/cron-test.log 2>&1
Wacht een minuut. Verschijnt /tmp/cron-test.log niet, dan zit het probleem niet in je script: het zit in de daemon, de crontab, of de toestemming om cron te gebruiken. Controleer systemctl is-active cron en paragraaf 6.6.
2. Heeft cron het geprobeerd? Het log vertelt je wat cron gestart heeft, en het toont het commando precies zoals cron het ontleed heeft. Hier zie je een procentteken opduiken als een afgekapt commando:
$ journalctl -t CRON --since '-10 min'
3. Werkt het commando met de hand? Draai de exacte tekst uit de crontab in je eigen shell. Faalt het hier, dan was cron nooit het probleem.
4. Werkt het als de juiste gebruiker? Een taak onder www-data heeft andere permissies, een andere thuismap, en mogelijk geen shell. Test als die gebruiker:
$ sudo -u www-data /usr/bin/php /var/www/site/cli/joomla.php scheduler:run --all
Deze ene stap vindt de meeste permissieproblemen meteen, en het is de stap die mensen overslaan.
5. Werkt het in de omgeving van cron? Hier komen de PATH- en shellverschillen uit 6.1 boven water. Kleed je omgeving uit en probeer opnieuw:
$ env -i /bin/sh -c '/usr/local/bin/job.sh'
Voor de exacte waarheid in plaats van een benadering: plan /usr/bin/env voor een minuut in en lees wat eruit komt, zoals in 6.1.
6. Wordt de uitvoer weggegooid? Draait de taak wel maar gebeurt er niets, voeg dan een omleiding toe en kijk wat hij zegt. Een omhulsel dat de omstandigheden naast de uitvoer vastlegt maakt van een onzichtbare storing een duidelijke:
#!/bin/bash
LOG=/var/log/myjob.log
{
echo "===== $(date --iso-8601=seconds) ====="
echo "user: $(id -un) pwd: $(pwd)"
echo "PATH=$PATH"
/usr/bin/php /var/www/site/cli/import.php
echo "exit: $?"
} >> "$LOG" 2>&1
Nog een controle geldt alleen voor de cron.daily-familie: staat het script in een van die mappen, draai er dan eerst run-parts --test op. Een probleem met de bestandsnaam laat elke stap hierboven er goed uitzien terwijl er nooit iets uitgevoerd wordt. Paragraaf 7.3 heeft de details.
6.4 Voorkomen dat taken overlappen
cron start vrolijk een taak terwijl de vorige nog bezig is. Een backup die normaal vier minuten duurt en elke vijf minuten gepland staat, draait op een slechte dag twee keer tegelijk. Twee kopieën die hetzelfde bestand schrijven is hoe backups corrupt raken.
cron heeft hier geen optie voor, maar flock lost het op met een enkel woord:
*/5 * * * * flock -n /tmp/backup.lock /usr/local/bin/backup.sh
De optie -n (kort voor nonblock) betekent: is het slot al bezet, geef dan meteen op in plaats van te wachten. De tweede run stopt stilletjes en de eerste gaat door. Twee varianten zijn het kennen waard:
| Optie | Gedrag als het slot bezet is |
|---|---|
-n |
Direct stoppen. Juist voor frequente taken waarbij een run overslaan geen probleem is. |
-w 30 |
Wacht tot 30 seconden op het slot, en geef dan op. |
-E 0 |
Gebruik afsluitcode 0 als het slot bezet is, zodat monitoring geen storing meldt. |
Zet het slotbestand ergens dat blijft bestaan, zoals /var/lock, als de taak ertoe doet. Vergrendelen kost een regel, en het voorkomt een soort storing die achteraf oprecht lastig te onderzoeken is.
Overlap is niet de enige manier waarop een geplande taak de machine pijn doet. Een nachtelijk rapport dat elke kern volledig belast, of een backup die de schijf verzadigt, vertraagt de website die de reden is dat de server bestaat. Twee omhulsels kosten niets:
0 2 * * * nice -n 10 ionice -c 3 /usr/local/bin/report-generator
nice -n 10 verlaagt de CPU-prioriteit, zodat alles wat interactief is voorgaat. ionice -c 3 zet de taak in de idle-klasse voor schijftoegang, waar hij alleen aan de beurt komt als niets anders iets wil. Geen van beide maakt de taak trager op een rustige machine; allebei maken ze hem onzichtbaar op een drukke.
6.5 anacron, voor machines die niet altijd aan staan
cron heeft geen geheugen. Een laptop die om 06:25 dicht is draait de dagelijkse taken nooit, en haalt ze ook nooit in. anacron bestaat precies hiervoor, en zijn eigen man-pagina zegt het verschil onomwonden: hij gaat er niet van uit dat de machine doorlopend draait, en hij meet periodes in dagen in plaats van in kloktijden.
Zijn tabel heeft vier kolommen in plaats van vijf velden:
$ cat /etc/anacrontab
SHELL=/bin/sh
HOME=/root
LOGNAME=root
# period(days) delay(min) job-identifier command
1 5 cron.daily run-parts --report /etc/cron.daily
7 10 cron.weekly run-parts --report /etc/cron.weekly
@monthly 15 cron.monthly run-parts --report /etc/cron.monthly
Lees de eerste regel als: "draai dit hooguit een keer per dag; en als je hem start, wacht dan eerst vijf minuten". anacron legt na elke geslaagde run een tijdstempel per taaknaam vast, en vergelijkt bij de volgende start de datum. Was de laatste run gisteren of eerder, dan is de taak aan de beurt. De vertraging spreidt taken zodat een machine die wakker wordt niet alles tegelijk draait.
Alleen de datum wordt vergeleken, nooit het uur. anacron is het juiste gereedschap voor "ongeveer dagelijks" werk en het verkeerde voor alles dat op een bepaald tijdstip moet gebeuren.
Dit verklaart ook die test -x /usr/sbin/anacron ||-bewakingen in /etc/crontab. Is anacron geïnstalleerd, dan doet cron een stap terug uit de dagelijkse, wekelijkse en maandelijkse mappen en laat hij anacron de eigenaar zijn, zodat ze nooit allebei hetzelfde script draaien.
6.6 Wie cron mag gebruiken
Twee bestanden regelen de toegang, en de logica heeft een verrassing:
| Situatie | Wie crontab mag gebruiken |
|---|---|
/etc/cron.allow bestaat |
Alleen de gebruikers die erin staan. cron.deny wordt volledig genegeerd. |
Alleen /etc/cron.deny bestaat |
Iedereen behalve de gebruikers die erin staan. |
| Geen van beide bestaat | Hangt van het systeem af. Op standaard Debian- en Ubuntu-systemen mag iedereen. |
root mag altijd een crontab installeren, ongeacht beide bestanden. Op een gedeelde server of hostingserver is een /etc/cron.allow met alleen de accounts die het nodig hebben een goedkope en effectieve beperking, want het weigert iedereen standaard in plaats van dat jij ze allemaal moet opnoemen.
Een permissiedetail bijt mensen: allebei de bestanden moeten voor iedereen leesbaar zijn, of leesbaar voor de groep crontab. Zijn ze dat niet, dan weigert cron elke gebruiker de toegang totdat de permissies gerepareerd zijn.
6.7 Een geplande taak veilig houden
Een cron-taak is een onbeheerd commando dat als iemand draait, vaak als root, en dat eeuwig. Dat verdient dezelfde zorg als elke andere geprivilegieerde automatisering, en twee specifieke fouten komen keer op keer terug.
Een commandoregel is openbaar. Alles wat je in een crontab zet is tijdens het draaien zichtbaar in de procestabel, voor elke gebruiker op de machine:
$ ps -eo user,args | grep curl
root curl -u admin:supersecret https://api.example.com/backup
Tenzij het systeem met hidepid is ingesteld, en dat is niet de standaard, kan elk lokaal account daarop wachten. Datzelfde geheim staat ook in het crontab-bestand, in je backups van dat bestand, en in elk log dat het commando vastlegt. Bewaar inloggegevens in een bestand dat de taak leest:
$ sudo install -o root -g root -m 600 /dev/null /etc/myapp/backup.env
$ sudoedit /etc/myapp/backup.env # API_TOKEN=...
# and in the script, not in the crontab:
. /etc/myapp/backup.env
curl -H "Authorization: Bearer $API_TOKEN" https://api.example.com/backup
Een root-taak is niet veiliger dan het bestand dat hij draait. Deze regel ziet er prima uit:
0 3 * * * root /usr/local/sbin/backup.sh
Kan een willekeurige gebruiker zonder rechten in backup.sh schrijven, dan kan die gebruiker erin zetten wat hij wil en draait root dat om drie uur 's nachts. Dat is geen backupscript meer, dat is een root-shell met een tijdschakelaar. Hetzelfde geldt voor de map waarin het staat, want een bestand kunnen vervangen is net zo goed als het kunnen bewerken.
$ ls -ld /usr/local/sbin /usr/local/sbin/backup.sh
drwxr-xr-x 2 root root 4096 Aug 23 12:00 /usr/local/sbin
-rwxr-xr-x 1 root root 842 Aug 23 12:00 /usr/local/sbin/backup.sh
Beide horen eigendom van root te zijn en door niemand anders schrijfbaar. cron dwingt dit al af voor de crontab-bestanden zelf, en daarom worden /etc/cron.d-regels genegeerd als ze niet van root zijn of wel schrijfbaar voor groep of anderen, maar niets controleert het script aan de andere kant van de regel. Die controle is aan jou.
De derde regel heeft geen voorbeeld nodig: draai een taak niet als root omdat het makkelijker is. Het meeste geplande werk heeft een map en een database nodig, dus geef het een account met precies dat.
6.8 De planner van een webapplicatie aansturen
De meeste moderne webapplicaties hebben hun eigen takenplanner en verwachten een enkele cron-regel die hem aandrijft. Joomla is een goed voorbeeld, omdat het daar een consoleprogramma voor meelevert. De commandonamen komen rechtstreeks uit de geïnstalleerde broncode:
$ php /var/www/site/cli/joomla.php scheduler:list
$ php /var/www/site/cli/joomla.php scheduler:run --all
$ php /var/www/site/cli/joomla.php scheduler:run --id 3
De crontabregel die hem aanstuurt heeft drie dingen nodig die makkelijk fout gaan:
*/5 * * * * www-data /usr/bin/php /var/www/site/cli/joomla.php scheduler:run --all >> /var/log/joomla-cron.log 2>&1
- Draai als de gebruiker van de webserver. Draai je het als root, dan is elk cachebestand en elk log dat de applicatie schrijft eigendom van root, en kan de website er zelf niet meer in schrijven. Dit is een veelvoorkomende manier om een site te breken met een cron-taak die "werkt".
- Gebruik het absolute pad naar PHP. Er staat vaak meer dan een PHP op een server, en de CLI-versie is niet altijd degene die de webserver gebruikt.
which phpin jouw shell hoeft niet te zijn wat cron vindt. - Log de uitvoer. De planner rapporteert wat hij gedraaid heeft. Zonder omleiding wordt dat rapport de leegte in gemaild.
Hetzelfde patroon geldt voor elke applicatie met een planner: draai als de gebruiker van de applicatie, met een absoluut pad naar de interpreter, en stuur de uitvoer naar een bestand. De applicatie bepaalt welke taken aan de beurt zijn; cron levert alleen een hartslag.
Het tweede punt verdient meer dan een regel, want "PHP is PHP" klopt niet. De PHP die cron draait is de CLI-versie, en op Debian en Ubuntu heeft die zijn eigen configuratie, volledig los van die van je website:
$ php --ini
Configuration File (php.ini) Path: /etc/php/8.3/cli
Loaded Configuration File: /etc/php/8.3/cli/php.ini
Scan for additional .ini files in: /etc/php/8.3/cli/conf.d
Lees het pad. Dat deel cli is een mapnaam, en ernaast staat een map fpm met een andere php.ini en een andere set ingeschakelde extensies. Daar volgen vier dingen uit, en elk daarvan heeft weleens een supportmelding opgeleverd:
- Andere limieten. CLI komt meestal met
memory_limit = -1en geen maximale uitvoertijd, terwijl FPM echte limieten heeft. Een taak die in de browser sneuvelt kan vanuit cron probleemloos draaien, en een taak die je vanuit cron testte kan in de browser sneuvelen. - Andere extensies. Een extensie die voor FPM aan staat, staat niet automatisch aan voor CLI. De taak faalt dan met "class not found" voor iets dat de website duidelijk wel heeft.
- Andere versies. Op een server met meerdere PHP-versies is
/usr/bin/phpwelke versie ook het standaardalternatief is, en de site kan onder FPM een andere draaien. - Alles anders, in een container. Draait de site in Docker, dan is de
/usr/bin/phpvan de host helemaal niet de PHP van de site, en moet de cron-regel via de container.
Drie commando's beslechten het voordat je iets inplant:
$ /usr/bin/php -v # which version cron will actually use
$ /usr/bin/php --ini # which configuration it loads
$ /usr/bin/php -m # which extensions are enabled for CLI
Noem op een server met meer dan een versie de versie expliciet in de crontab (/usr/bin/php8.3) in plaats van erop te vertrouwen dat de standaard bij de volgende upgrade blijft waar hij is.
6.9 Weten dat het echt gelukt is
cron start een commando en vergeet het. Hij kijkt nooit naar de afsluitcode, dus vanuit cron gezien zijn een backup die niets schreef en een backup die vijftig gigabyte schreef identieke gebeurtenissen. Wil je weten welke van de twee het was, dan moet de taak dat zelf zeggen.
Begin met het script eerlijk maken. Een shellscript geeft de status van zijn laatste commando terug, en dat is zelden wat je bedoelt:
#!/bin/bash
set -euo pipefail # stop on the first error, and on a failure inside a pipe
Zonder pipefail meldt mysqldump ... | gzip > out.gz succes zodra gzip slaagt, en dat doet die zelfs als hij helemaal niets aangereikt krijgt.
Laat daarna een spoor achter dat iets van buiten kan controleren. Het patroon is een regel, en de && is de hele truc:
30 3 * * * /usr/local/bin/backup && touch /var/lib/myapp/backup.last-success
Het tijdstempel verschuift alleen als de taak geslaagd is. Nu wordt "heeft de backup vannacht gedraaid?" een vraag die iedereen kan beantwoorden, ook een monitoringsysteem dat geen idee heeft wat een backup is:
$ find /var/lib/myapp/backup.last-success -mmin +1500
/var/lib/myapp/backup.last-success # printed = older than 25 hours = alert
Dit is sterker dan controleren of cron draait, en veel sterker dan controleren of de taak in het log staat, want het rapporteert over het werk in plaats van over de poging. Nuttige signalen, ongeveer op volgorde van hoeveel ze je vertellen:
| Signaal | Beantwoordt |
|---|---|
Het cron-log heeft een CMD-regel |
cron heeft het geprobeerd. Meer niet. |
| Afsluitcode was 0 | Het commando denkt dat het gelukt is |
| Leeftijd van een succesmarkering | Het is voor het laatst gelukt op een bekend moment |
| Grootte en leeftijd van het uitvoerbestand | Het heeft iets geloofwaardigs geproduceerd |
| Een aantal dat de taak meldt (rijen, bestanden, bytes) | Het heeft de hoeveelheid werk gedaan die je verwacht |
Duw voor alles wat ertoe doet in plaats van te trekken: laat de taak bij succes een monitoringadres aanroepen, en laat die dienst alarm slaan als de aanroep uitblijft. Een taak die stil faalt en een taak die zes weken geleden verwijderd is zien er vanaf de server hetzelfde uit. Voor iets dat op een hartslag wacht zien ze er niet hetzelfde uit.
6.10 systemd-timers, het moderne alternatief
Op elke machine met systemd draait al een tweede planner, en een verrassende hoeveelheid werk waarvan je aanneemt dat cron het doet is er stilletjes naartoe verhuisd:
$ systemctl list-timers --all
NEXT LEFT LAST UNIT
Sun 2026-08-23 15:39:00 CEST 4min 38s Sun 2026-08-23 15:09:04 CEST phpsessionclean.timer
Sun 2026-08-23 16:34:21 CEST 59min Sun 2026-08-23 15:31:08 CEST anacron.timer
Mon 2026-08-24 00:00:00 CEST 8h Sun 2026-08-23 00:00:05 CEST logrotate.timer
Mon 2026-08-24 01:47:50 CEST 10h Sun 2026-08-23 05:28:44 CEST man-db.timer
Een timer bestaat uit twee bestanden: een .timer die zegt wanneer, en een .service die zegt wat. Hier is degene die het opruimen van PHP-sessies uit paragraaf 5.2 overnam, en dat is de reden dat die cron-regel een systemd-test om zich heen heeft:
$ systemctl cat phpsessionclean.timer
[Unit]
Description=Clean PHP session files every 30 mins
[Timer]
OnCalendar=*-*-* *:09,39:00
Persistent=true
[Install]
WantedBy=timers.target
Die OnCalendar-regel is het directe equivalent van 09,39 * * * *. De schrijfwijze leest als jaar-maand-dag uur:minuut:seconde, en hij ondersteunt dezelfde lijsten en stappen als cron. Anders dan bij cron kun je vragen of je het goed hebt voordat je erop vertrouwt:
$ systemd-analyze calendar "*-*-* *:09,39:00"
Normalized form: *-*-* *:09,39:00
Next elapse: Sun 2026-08-23 15:39:00 CEST
(in UTC): Sun 2026-08-23 13:39:00 UTC
From now: 2min 15s left
Er is geen cron-commando dat dit doet, en dat alleen al is een reden om naar een timer te grijpen als de planning ingewikkeld is.
| Nodig | cron | systemd-timer |
|---|---|---|
| Snel opzetten | Een regel, een commando | Twee bestanden plus systemctl enable --now |
| Inhalen na uitval | Nee. Daarvoor anacron, apart. | Persistent=true |
| Voorkomen dat alles tegelijk start | Met de hand oneven minuten kiezen | RandomizedDelaySec=5m |
| Wachten op het netwerk of een database | Nee. @reboot gokt. |
After=, Requires= |
| Waar de uitvoer heen gaat | Mail, meestal nergens | Het journaal, altijd |
| Overlappende runs voorkomen | flock |
Ingebouwd: een service draait of draait niet |
| Het nu testen | Wachten, of de tijd aanpassen | systemctl start unit.service |
| CPU, geheugen of IO begrenzen | Nee | CPUQuota=, MemoryMax= |
| Werkt overal | Ja, ook in containers en op BSD | Alleen waar systemd draait |
De eerlijke samenvatting: voor "draai dit script elke nacht" is cron een regel en is er geen reden om iets te veranderen. Voor alles dat uitval moet overleven, op een andere service moet wachten, begrensd moet worden in verbruik, of getest moet worden voordat het live gaat, is een timer het betere gereedschap en verdient het extra bestand zich alleen al terug met de logging.
Naar boven7. Iets wat de meeste gebruikers niet weten
7.1 Een procentteken is een regeleinde
Dit is de vreemdste regel in het hele formaat, en degene die de meest onbegrijpelijke storingen oplevert. In een crontab is een niet-ontsnapt % geen procentteken. De eerste beeindigt het commando, en alles erna wordt standaardinvoer voor dat commando. Verdere procenttekens worden regeleindes.
Hier is het bewijs. Deze crontabregel is op een draaiende machine geïnstalleerd:
* * * * * cat > /tmp/cron-pct.txt%hello from stdin%second line
Kijk wat cron logde toen hij hem draaide. Het commando dat hij uitvoerde is niet het commando dat er stond:
$ journalctl -t CRON
Aug 23 15:35:01 xps CRON[408132]: (peter) CMD (cat > /tmp/cron-pct.txt)
$ cat /tmp/cron-pct.txt
hello from stdin
second line
Het commando werd afgekapt bij de eerste %, en de rest werd hem als twee regels invoer gevoerd. Dat is gedocumenteerd gedrag, geen bug, en daarom doet deze regel niet wat hij lijkt te doen:
0 9 * * * /usr/local/bin/report.sh --format=csv --sample=50% # BROKEN
0 9 * * * /usr/local/bin/report.sh --format=csv --sample=50\% # correct
Het bijt het hardst bij date, want datumnotaties bestaan uit procenttekens. Een dagelijks logbestand met de datum in de naam is iets natuurlijks om te willen, en de naieve versie faalt:
0 3 * * * tar czf /backup/$(date +%Y-%m-%d).tgz /var/www # BROKEN
0 3 * * * tar czf /backup/$(date +\%Y-\%m-\%d).tgz /var/www # correct
Ze allemaal ontsnappen gaat makkelijk mis. Het commando in een script zetten betekent dat je er nooit meer over hoeft na te denken, en dat is het echte argument voor de derde optie uit paragraaf 6.1.
De eigenschap heeft wel een nut. Omdat alles na de eerste % standaardinvoer is, kun je een commando gegevens voeren zonder here-document:
0 9 * * 1 mail -s "Monday reminder" Dit e-mailadres wordt beveiligd tegen spambots. JavaScript dient ingeschakeld te zijn om het te bekijken. %Stand-up at 10:00.
7.2 Dag van de maand en dag van de week zijn OF, niet EN
Elk ander veld in een crontab beperkt de planning. Deze twee doen het omgekeerde. Uit crontab(5):
If both fields are restricted (i.e., aren't *), the command will be run when either field matches the current time.
Deze regel betekent dus niet "de 1e en de 15e, maar alleen als het een vrijdag is":
30 4 1,15 * 5 /usr/local/bin/job.sh
Hij draait om 04:30 op de 1e, op de 15e, en op elke vrijdag. In een normale maand zijn dat ongeveer zes runs in plaats van de een of twee die je verwachtte.
De regel geldt alleen als beide velden beperkt zijn. Is een van beide *, dan gedraagt het andere zich normaal. De veilige gewoonte is dus eenvoudig: beperk nooit beide dagvelden in dezelfde regel. Heb je de doorsnede echt nodig, laat dan een veld op * staan en zet de test in het commando. De man-pagina geeft het patroon voor "de tweede zaterdag van de maand":
0 4 8-14 * * test $(date +\%u) -eq 6 && /usr/local/bin/job.sh
Dag 8 tot en met 14 bevat elke weekdag precies een keer, dus de datumreeks kiest de tweede week en date +%u (dag van de week, 1 tot 7) kiest zaterdag. Let op de ontsnapte \%, om de reden uit 7.1.
7.3 Een punt in de bestandsnaam betekent dat hij nooit draait
Zet een script met de naam backup.sh in /etc/cron.daily, maak het uitvoerbaar, en het zal nooit draaien. Geen foutmelding, geen logregel, niets.
De reden is run-parts, en het staat gedocumenteerd: tenzij hij --lsbsysinit of --regex meekrijgt, mogen de namen uitsluitend bestaan uit ASCII-letters, ASCII-cijfers, liggende streepjes en koppeltekens. Een punt staat niet in dat rijtje, dus het bestand wordt stilzwijgend genegeerd, net als elke map.
Het kost tien seconden om het te laten zien:
$ mkdir /tmp/rptest
$ touch /tmp/rptest/good /tmp/rptest/bad.sh
$ chmod +x /tmp/rptest/*
$ run-parts --test /tmp/rptest
/tmp/rptest/good
Beide bestanden bestaan, beide zijn uitvoerbaar, en toch zou alleen good ooit draaien. Deze regel is een bewuste eigenschap: hij zorgt dat logrotate.dpkg-old en reservekopieën van je editor niet uitgevoerd worden tijdens een pakketupgrade. Maar hij pakt iedereen een keer, want een shellscript .sh noemen is het natuurlijkste ter wereld.
Dezelfde naamgevingsregel geldt voor /etc/cron.d. Maak er een gewoonte van om run-parts --test op de map te draaien nadat je er iets in zet.
7.4 Stappen beginnen opnieuw bovenaan de reeks
*/5 werkt perfect omdat 5 een deler van 60 is. */7 niet, en het resultaat is niet wat de meeste mensen verwachten. Een stap genereert waarden binnen de reeks van zijn eigen veld en daarna begint het veld opnieuw:
*/7 * * * * runs at minute 0, 7, 14, 21, 28, 35, 42, 49, 56
then the hour ends and the next run is at minute 0
Het gat tussen 56 en de volgende 0 is vier minuten, geen zeven. Over een dag krijg je 9 runs per uur in plaats van een run elke zeven minuten. Hetzelfde geldt in elk veld: */9 in het uurveld draait om 0, 9 en 18, en dan begint de dag opnieuw, dus het gat van 18:00 tot de volgende 00:00 is zes uur.
Heeft een taak echt een gelijke tussenpoos nodig, kies dan een stap die het veld deelt (2, 3, 4, 5, 6, 10, 12, 15, 20, 30 voor minuten), of gebruik een systemd-timer met OnUnitActiveSec=7m, die meet vanaf het einde van de vorige run en zich niets van klokgrenzen aantrekt.
7.5 Een klok voor iedereen
cron draait in een enkele tijdzone, die van het systeem, en dat is niet per gebruiker in te stellen. Het naslagwerk is er onomwonden over, en het is een beperking die mensen op de harde manier ontdekken na een servermigratie:
It currently does not support per-user timezones. Even if a user specifies the TZ environment variable in his crontab this will affect only the commands executed in the crontab, not the execution of the crontab tasks themselves.
TZ=Europe/Amsterdam bovenaan je crontab verandert dus wat date binnen je taak afdrukt. Het verplaatst de taak niet. Moet een rapport om 09:00 Amsterdamse tijd de deur uit op een server die op UTC staat, dan schrijf je de planning in UTC en pas je hem twee keer per jaar aan, of je laat de taak vroeg starten en zelf de lokale tijd controleren.
Zomertijd wordt afgehandeld, en de regels zijn specifiek. Bij een klokverzetting van minder dan drie uur:
- Gaat de klok vooruit, dan draaien taken die in het overgeslagen uur zouden vallen kort na de verzetting alsnog. Er gaat niets verloren.
- Gaat de klok achteruit, dan draaien taken in het herhaalde uur geen tweede keer. Er wordt niets verdubbeld.
- Dit geldt alleen voor taken op een vast tijdstip. Taken met een sterretje in het uur- of minuutveld, en
@hourly, volgen gewoon de nieuwe tijd. - Een verzetting van meer dan drie uur wordt gezien als iemand die de klok corrigeert, en de nieuwe tijd geldt onmiddellijk.
Dat is beter gedrag dan de meeste mensen aannemen, maar het dekt alleen de planning van cron zelf. Een taak die zelf uitrekent wat "gisteren" is, moet de dagen van 23 en 25 uur nog steeds zelf aankunnen.
7.6 Weten waar cron ophoudt
Een deel van vakmanschap is weten wanneer een gereedschap het verkeerde is. cron doet maar een enkel ding: hij start een commando op een tijdstip. Alles hieronder is iets dat hij bewust niet doet.
| Nodig | Gebruik | Waarom |
|---|---|---|
| Een keer draaien, op een tijdstip in de toekomst | at |
cron is voor herhaling. at is voor "morgen om 6 uur". |
| Inhalen na uitval | anacron of Persistent=true |
cron heeft geen geheugen van gemiste runs |
| Wachten tot een service klaar is | Een systemd-unit | cron kent geen afhankelijkheden |
| Een gefaalde taak opnieuw proberen | Het script, of een echte planner | cron kijkt nooit naar de afsluitcode |
| Taken op volgorde aan elkaar knopen | Een script, of een workflowtool | Twee taken tien minuten uit elkaar plannen en hopen is geen afhankelijkheid |
| Weten dat een taak gestopt is met draaien | Externe monitoring | Stilte is de normale uitvoer van cron, dus stilte kan niet je alarm zijn |
De laatste rij is degene om serieus te nemen. Elke andere storing in deze lijst meldt zich uiteindelijk. Een cron-taak die zes weken geleden stil gestopt is ziet er van buiten precies zo uit als een cron-taak die werkt, tot de dag dat je nodig hebt wat hij produceerde.
Naar boven8. Best practices
- Zet het werk in een script, niet in de crontab. Een regel die
/usr/local/bin/nightly.shaanroept houdt de planning gescheiden van de logica, zet de logica in versiebeheer, laat je testen door het zelf te draaien, en betekent dat je nooit een procentteken hoeft te ontsnappen. - Leid de uitvoer altijd om. Sluit elke regel af met
>> /var/log/iets.log 2>&1, in die volgorde. Zonder dat is de enige melding van een storing een mail die waarschijnlijk nergens heen gaat. - Gebruik overal absolute paden. Zowel voor de interpreter als voor het script. Het PATH van cron is een systeem-PATH, niet het jouwe, en niets wat je shell-profiel toevoegde is aanwezig.
- Test het commando in de omgeving van cron, niet in die van jou.
env -i /bin/sh -c '/usr/local/bin/job.sh'brengt je dichtbij, en een tijdelijke* * * * *-regel dieenvdraait geeft je de exacte waarheid. - Maak een backup voordat je bewerkt.
crontab -l > ~/crontab.backupkost een seconde, en-rligt naast-eop het toetsenbord, zonder bevestiging en zonder ongedaan maken. - Controleer de syntaxis voordat je installeert.
crontab -n bestandontleedt zonder iets te wijzigen, en dat is wat je in een deployscript wilt. - Beperk nooit beide dagvelden op dezelfde regel. Ze worden gecombineerd met OF, niet met EN. Laat er een op
*staan en zet de extra voorwaarde in het commando. - Laat elk gepland script zijn eigen map opzoeken. cron start je in de thuismap van de gebruiker, niet naast het script.
cd "$(dirname "$0")"bovenaan haalt een hele categorie "werkt met de hand, faalt vanuit cron" weg. - Houd geheimen uit de crontab. Een commandoregel is via
pszichtbaar voor elke gebruiker op de machine. Zet inloggegevens in een bestand met modus 0600 en lees dat vanuit het script. - Controleer wie het script van een root-taak mag schrijven. Kan een gebruiker zonder rechten het bewerken, of de map eromheen, dan is die gebruiker root. cron controleert de permissies van het crontab-bestand en verder niets.
- Roteer waar je naartoe logt. Een bestand waar elke vijf minuten iets bij komt vult uiteindelijk een schijf. Een blokje in
/etc/logrotate.d, of een pipe naarlogger, kost een minuut. - Laat een succesmarkering achter, en waarschuw op de leeftijd ervan.
taak && touch /var/lib/app/last-successmaakt van "is het gelukt?" een vraag die een monitoringsysteem kan beantwoorden zonder iets van de taak te weten. - Wees een goede buur op een drukke machine.
nice -n 10 ionice -c 3voor een zware taak zorgt dat hij niet concurreert met de website waarvoor de server bestaat. - Zet
flock -nvoor alles wat lang kan duren. Dat ene woord voorkomt dat twee kopieën van een backup hetzelfde bestand schrijven. - Geef de voorkeur aan
/etc/cron.dvoor servertaken. Gewone bestanden die configuratiebeheer kan uitrollen, met een expliciet gebruikersveld, en zonder punt in de bestandsnaam. - Draai applicatietaken als de gebruiker van de applicatie. Een planner die als root draait laat bestanden van root achter en breekt de site die hij moest onderhouden.
- Monitor van buitenaf. Laat de taak zich bij een monitoringdienst melden als hij klaar is. De stilte van cron is identiek of de taak werkte, faalde, of sinds maart niet meer gedraaid heeft.
$ man 5 crontab # the file format: fields, steps, the OR rule, the % rule
$ man 1 crontab # the command: -l, -e, -r, -n, -i
$ man 8 cron # the daemon: logging, DST, Debian specifics
$ man 8 anacron # for machines that are not always on
$ man 8 run-parts # the filename rules for cron.daily and friends
$ man 7 systemd.time # OnCalendar syntax, if you move to timers
Naar boven9. Veelgemaakte fouten
9.1 Mythe versus werkelijkheid
| Mythe | Werkelijkheid |
|---|---|
"Het PATH van cron is /usr/bin:/bin." |
Niet op het huidige Ubuntu, dat cron -P draait en een volledig systeem-PATH erft. De echte regel is dat het nooit jouw PATH is. |
"30 4 1,15 * 5 draait op de 1e en de 15e als het een vrijdag is." |
Hij draait op de 1e, de 15e, en elke vrijdag. Beide dagvelden zijn OF. |
"Mijn script is uitvoerbaar, dus /etc/cron.daily draait het." |
Niet als er een punt in de naam staat. run-parts negeert het stilzwijgend. |
"*/7 betekent elke zeven minuten." |
Het betekent minuut 0, 7, ... 56, en dan begint het uur opnieuw. Het laatste gat is vier minuten. |
| "De taak drukte niets af, dus hij werkte." | Hij drukte niets af dat jou bereikte. Zonder mailsysteem gooit cron de fout weg. |
"TZ= in mijn crontab verplaatst de planning." |
Het verandert alleen de omgeving van het commando. cron plant altijd in de tijdzone van het systeem. |
| "Ik moet cron herstarten na het bewerken van een crontab." | Nee. cron controleert de spool en /etc/cron.d elke minuut op wijzigingen. |
"Een CMD-regel in het log bewijst dat de taak werkte." |
Standaard logt cron alleen dat een taak gestart is. Voeg -L 15 toe om eindes en storingen te loggen. |
"@reboot draait nadat het systeem klaar is." |
Hij draait als de cron-daemon start, en dat kan voor het netwerk of de database zijn. |
| "cron draait mijn script vanuit de map waar het staat." | Hij draait vanuit de thuismap van de gebruiker. Relatieve paden in het script breken. |
| "De PHP die cron gebruikt is die van mijn website." | Het is de CLI-versie, met een eigen php.ini, eigen extensies, en mogelijk een andere versie. |
| "Een wachtwoord in een crontab is privé omdat het bestand modus 600 heeft." | Het bestand wel. De commandoregel niet: ps toont hem aan elke gebruiker terwijl de taak draait. |
"Zet opties in /etc/default/cron." |
Achterhaald op het huidige Ubuntu. Het bestand zegt zelf dat je systemctl edit cron.service moet gebruiken. |
9.2 Andere valkuilen om te vermijden
- Het gebruikersveld vergeten in
/etc/cron.d. Het eerste woord van je commando wordt als gebruikersnaam gelezen en de regel faalt. Gebruikerscrontabs hebben vijf velden voor het commando; systeembestanden zes. - Rechtstreeks in
/var/spool/cron/crontabsbewerken. De map is niet voor niets alleen schrijfbaar voor de groepcrontab, en een bestand dat je er met de hand neerzet wordt mogelijk nooit geladen. Gebruikcrontab -eofcrontab bestand. - Het laatste regeleinde weglaten. cron beschouwt een crontab waarvan de laatste regel geen regeleinde heeft als kapot, en
crontabweigert hem te installeren. Elke fatsoenlijke editor zet er een; een heredoc in de shell misschien niet. 2>&1 >> bestandschrijven. De volgorde klopt niet, en de foutuitvoer gaat nog steeds nergens heen. Leid eerst standaarduitvoer om, en richt daarna standaardfout daarop.- Bash aannemen. cron geeft je
/bin/sh, en dat is op Debian en Ubuntu dash.[[ ]], arrays ensourcefalen allemaal. ZetSHELL=/bin/bashbovenaan de crontab, of schrijf draagbare code. - Alles om middernacht plannen.
@dailyzet elke taak op de machine samen op 00:00. Spreid ze, en laatRandomizedDelaySecvan een timer het voor je doen als de machine er een van vele is. - Testen door te wachten. Zet de planning niet op "over twee minuten" om vervolgens naar je terminal te staren. Draai het commando eerst met de hand, installeer daarna de echte planning, en controleer met het log.
- Een gebruikersaccount met een crontab hernoemen. Spool-bestanden zijn naar het account genoemd. Hernoem de gebruiker en de crontab stopt met draaien, zonder dat iets aangeeft waarom.
- Een logbestand ongelimiteerd laten groeien. Een taak die elke vijf minuten toevoegt en nooit geroteerd wordt vult de schijf, en een volle schijf breekt alles op de server voordat iemand naar de cron-taak kijkt.
- Een afsluitcode door een pipe vertrouwen.
dump | gzip > out.gzmeldt succes zodragzipslaagt, ook bij lege invoer. Gebruikset -o pipefailin het script. - Een taak geloven die uitgeschakeld is. Cron-regels van pakketten bevatten steeds vaker
if [ ! -d /run/systemd/system ], waardoor ze op een systemd-machine niets doen. Kijk of er een timer is voordat je de cron-regel gaat onderzoeken.
10. Samenvatting
cron is een klein, oud, buitengewoon betrouwbaar programma dat precies een ding doet, en de meeste problemen die mensen ermee hebben komen doordat ze meer verwachten.
- cron wordt een keer per minuut wakker, vergelijkt de klok met een lijst regels, en draait wat past. Hij heeft geen wachtrij, geen geheugen, en geen belangstelling voor de vraag of een taak gelukt is.
- Taken staan op drie soorten plaatsen: gebruikerscrontabs (vijf velden),
/etc/crontaben/etc/cron.d(vijf velden plus een gebruiker), en de mappen in de stijl vancron.daily(scripts, geen planning). - De vijf velden zijn minuut, uur, dag van de maand, maand, dag van de week. Zowel 0 als 7 betekent zondag.
- Reeksen, lijsten en stappen zijn vrij te combineren, maar een stap begint bovenaan zijn eigen veld opnieuw, dus
*/7is geen gelijke tussenpoos. - Zijn beide dagvelden beperkt, dan worden ze gecombineerd met OF. Beperk ze nooit allebei.
- Een niet-ontsnapt
%beeindigt het commando en wordt standaardinvoer. Ontsnap hem als\%, of zet het commando in een script. - Uitvoer wordt gemaild, en op de meeste servers gaat die mail nergens heen. Leid om met
>> bestand 2>&1of je hoort nooit dat een taak faalde. - De omgeving bestaat uit zes variabelen. Het PATH is een systeem-PATH, niet het jouwe, en de shell is
/bin/sh, geen bash. - Taken starten in de thuismap van de gebruiker, niet naast het script, dus relatieve paden falen.
- In
cron.dailyencron.dwordt een bestandsnaam met een punt stilzwijgend genegeerd. Controleer metrun-parts --test. - cron kijkt nooit naar de afsluitcode. Wil je weten dat een taak werkte, laat dan een succesmarkering achter en waarschuw op de leeftijd daarvan.
- Een crontab-commandoregel is via
pszichtbaar voor elke gebruiker, en een root-taak die een door gebruikers schrijfbaar script draait is een root-shell met een tijdschakelaar. - cron gebruikt een tijdzone voor iedereen, en gaat verstandig om met zomertijd bij taken op een vast tijdstip.
flock -nvoorkomt dat runs overlappen.anacrondekt machines die uit staan.- systemd-timers doen inhalen, afhankelijkheden, willekeurige vertraging, verbruikslimieten en logging in het journaal. Voor alles voorbij "draai dit elke nacht" zijn ze het betere gereedschap.
Dit is het spiekbriefje dat je wilt bewaren:
# .---------------- minute (0 - 59)
# | .------------- hour (0 - 23)
# | | .---------- day of month (1 - 31)
# | | | .------- month (1 - 12) or jan,feb,...
# | | | | .---- day of week (0 - 7) (0 and 7 are Sunday) or sun,mon,...
# * * * * * command
crontab -l list your jobs
crontab -e edit them
crontab -l > backup.txt back them up FIRST
crontab -n file check syntax, install nothing
crontab file install from a file (version control)
crontab -i -r delete, with a confirmation prompt
crontab -u www-data -l another user's jobs (root only)
*/5 * * * * every 5 minutes
15 */6 * * * 00:15, 06:15, 12:15, 18:15
30 3 * * * 03:30 every day
0 4 * * 1 Mondays at 04:00
0 2 1 * * 1st of the month at 02:00
@reboot when the cron daemon starts
@daily @weekly @monthly midnight shorthands (all at 00:00)
... >> /var/log/job.log 2>&1 keep the output (order matters)
... > /dev/null 2>&1 throw it away on purpose
MAILTO="" no mail for any job in this file
flock -n /var/lock/j.lock ... do not overlap with the previous run
nice -n 10 ionice -c 3 ... stay out of the way on a busy machine
... 2>&1 | logger -t myjob log to syslog instead of a file you must rotate
job && touch /var/lib/ok success marker, for monitoring to check the age of
\% a literal percent sign
journalctl -t CRON what cron started
run-parts --test /etc/cron.d what would run from a directory
sudo -u www-data CMD test as the user the job will run as
env -i /bin/sh -c 'CMD' test in something close to cron's environment
php --ini which php.ini the CLI build loads
ps -eo user,args proof that command lines are not private
systemctl edit cron.service add -L 15 to log endings and failures
systemd-analyze calendar "..." test a systemd timer schedule
Een geplande taak is een van de weinige onderdelen van een server waar niemand naar kijkt tot de dag dat het ertoe doet. Wil je de backups, vernieuwingen en opruimtaken op je server zo ingericht hebben dat ze zichtbaar zijn als ze werken en luidruchtig als ze falen, in plaats van in beide gevallen stil, dan is dat precies het soort rustige werk waar ik graag bij help.
Naar boven

Peter is Joomla specialist en Linux admin voor snelle, veilige en schaalbare websites.












