Cron a časování

cron, systemd timers, opakované úlohy.

Spousta věcí na serveru se musí dít pravidelně a sama — zálohovat databázi ve tři ráno, každou noc smazat staré logy, jednou týdně poslat report. Nechtěl bys to klikat ručně a hlavně bys na to zapomněl. Od toho je plánovač úloh: řekneš mu co spustit a kdy, a on to hlídá za tebe. V Linuxu jsou dva hlavní: klasický cron (starý, jednoduchý, všude) a moderní systemd timers (víc práce na nastavení, ale logy a závislosti navíc). Tahle kapitola tě provede oběma.


Pár pojmů na úvod

  • Cron = démon (služba na pozadí), který každou minutu kontroluje seznam úloh a spouští ty, na které přišel čas. Běží pořád, tiše, od bootu.
  • Crontab = tabulka úloh pro cron (zkratka z „cron table"). Každý uživatel má svou vlastní; existuje i systémová. Edituje se příkazem crontab -e.
  • Job (úloha) = jeden řádek v crontabu — kdy se má spustit + jaký příkaz.
  • systemd timer = novější způsob časování. Dvojice souborů: .timer (kdy) + .service (co). Součást systemd, takže výstup jde do journalu a umí závislosti mezi úlohami.
  • OnCalendar = zápis času v systemd timeru (obdoba cronového formátu, ale čitelnější).
  • Prostředí (env) = proměnné jako PATH, HOME, které má proces k dispozici. Klíčové: cron ti dá skoro prázdné prostředí — o tom je půlka téhle kapitoly.

Proč vůbec časované úlohy

Zamysli se, co na serveru musí běžet, i když se zrovna nedíváš:

  • Zálohy — databáze i soubory se zálohují typicky v noci, kdy je nízký provoz (0 3 * * * = 3:00).
  • Čištění — mazání dočasných souborů, rotace a mazání starých logů, prázdný koš, staré cache.
  • Reporty — každé ráno spočítat návštěvnost a poslat mailem, každý týden souhrn prodejů.
  • Údržba — obnova SSL certifikátů (Let's Encrypt), synchronizace dat, kontrola volného místa.
  • Monitoring — každých pár minut zkontrolovat, že služba běží, a případně upozornit.

Společné je, že to musí jet spolehlivě a bez tebe. Ruční spouštění = dřív nebo později zapomeneš, a zjistíš to až když dojde místo na disku nebo chybí týdenní záloha.


Cron: základ

Cron má tři příkazy, které potřebuješ na začátku znát:

crontab -e   # otevře tvůj crontab v editoru (přidáš/upravíš úlohy)
crontab -l   # vypíše tvůj crontab (co tam máš)
crontab -r   # smaže celý crontab — POZOR, bez ptaní

crontab -e otevře textový editor. Každý řádek je jedna úloha. Prázdné řádky a řádky začínající # (komentáře) se ignorují. Po uložení a zavření editoru cron změny sám načte — nic se nerestartuje.

💡 Píšeš pro sebe (běžný uživatel)? Úloha poběží pod tvým účtem. Potřebuješ něco jako root (třeba zálohu systému)? Buď sudo crontab -e (rootův crontab), nebo systémové soubory níže.


Formát cronu: 5 polí + příkaz

Srdce cronu je jeden řádek. Má pět časových polí a za nimi příkaz:

┌───────────── minuta        (0–59)
│ ┌───────────── hodina      (0–23)
│ │ ┌───────────── den v měsíci (1–31)
│ │ │ ┌───────────── měsíc    (1–12)
│ │ │ │ ┌───────────── den v týdnu (0–7, 0 i 7 = neděle)
│ │ │ │ │
* * * * *  příkaz-který-se-spustí
PoleVýznamRozsah
1minuta0–59
2hodina0–23
3den v měsíci1–31
4měsíc1–12
5den v týdnu0–7 (0 i 7 = neděle)

Hvězdička * znamená „každý" (každou minutu / každou hodinu…). Dají se skládat:

  • , = výčet — 0,30 (nula a třicátá minuta)
  • - = rozsah — 1-5 (pondělí až pátek)
  • */N = každých N — */15 (každých 15 jednotek)

Příklady, které si zapamatuj

0 3 * * *        /usr/local/bin/zaloha.sh      # každý den ve 3:00
*/15 * * * *     /usr/local/bin/check.sh        # každých 15 minut
0 * * * *        /usr/local/bin/hodinovka.sh    # každou celou hodinu (v :00)
30 8 * * 1-5     /usr/local/bin/report.sh       # v 8:30 v pracovní dny (po–pá)
0 0 1 * *        /usr/local/bin/mesicni.sh       # 1. dne v měsíci o půlnoci
0 2 * * 0        /usr/local/bin/tydenni.sh       # každou neděli ve 2:00

🚩 Nejčastější chyba: čte se to zleva jako „minuta hodina", ne „hodina minuta". 0 3 = ve 3:00, ne v 0:03. Když si nejsi jistý, ověř zápis na webu jako crontab.guru — ukáže ti to lidsky.


Systémový cron: /etc/crontab a /etc/cron.d

Kromě uživatelských crontabů (crontab -e) existuje systémový cron. Používá se pro úlohy, které patří k systému nebo balíčku:

  • /etc/crontab — hlavní systémový soubor. Má o pole navíc: uživatele, pod kterým se úloha spustí (mezi časovými poli a příkazem).
  • /etc/cron.d/ — složka, do které balíčky i ty přidáváš samostatné soubory se stejným formátem. Přehlednější než cpát všechno do jednoho souboru.
  • /etc/cron.daily/, cron.weekly/, cron.monthly/ — hodíš tam skript (bez časového zápisu) a systém ho spustí jednou denně/týdně/měsíčně. Jednoduché, když ti nezáleží na přesné hodině.

Formát řádku v /etc/crontab a /etc/cron.d/ má tedy uživatele navíc:

# minuta hodina den měsíc den-v-týdnu  UŽIVATEL  příkaz
0 3 * * *   root   /usr/local/bin/zaloha.sh

💡 Pravidlo: osobní věcicrontab -e. Systémové/servisní → soubor v /etc/cron.d/ (dá se verzovat v gitu, nasadit deploymentem, snadno smazat).


⚠️ Cron má skoro prázdné prostředí

Tohle je největší past cronu a připraví o nervy skoro každého. Cron nespouští tvůj příkaz ve stejném prostředí jako tvůj terminál. Když se přihlásíš, načte se .bashrc, .profile, nastaví se plný PATH, proměnné… Cron nic z toho nedělá. Dostaneš holé, minimální prostředí:

  • PATH je oříznutý (často jen /usr/bin:/bin) — příkazy z /usr/local/bin nebo ~/bin nemusí jít.
  • Žádný .bashrc, žádné aliasy, žádné proměnné, které sis nastavil v shellu.
  • HOME může být jiné, než čekáš.

Výsledek: skript ti funguje ručně, ale z cronu záhadně ne. Řešení:

# 1) Používej ABSOLUTNÍ cesty ke všemu
0 3 * * *   /usr/bin/pg_dump mydb > /var/backups/db.sql   # ne jen "pg_dump"

# 2) Nebo si PATH nastav přímo v crontabu (nahoře, platí pro řádky pod ním)
PATH=/usr/local/bin:/usr/bin:/bin
0 3 * * *   zaloha.sh

# 3) Nebo si prostředí načti na začátku skriptu
#    (v skriptu: source /home/user/.profile, absolutní cesty, apod.)

🚩 Zlaté pravidlo cronu: absolutní cesty, absolutní cesty, absolutní cesty. K binárkám (/usr/bin/php), ke skriptům (/home/user/skript.sh) i k souborům (/var/log/...). Nespoléhej, že cron ví, kde co je.


Kam jde výstup

Když příkaz z cronu něco vypíše (na stdout nebo stderr), cron to pošle mailem lokálnímu uživateli. Na většině serverů ale mail nastavený není, takže výstup zmizí — a s ním i chybové hlášky. Ty úlohu ladíš naslepo. Proto výstup vždycky někam přesměruj:

# všechno (stdout i stderr) do logu, přepisuje se
0 3 * * *   /usr/local/bin/zaloha.sh > /var/log/zaloha.log 2>&1

# přidávat na konec (>>), ať máš historii
0 3 * * *   /usr/local/bin/zaloha.sh >> /var/log/zaloha.log 2>&1

# nezajímá mě výstup, jen chyby chci mailem → zahodím stdout, nechám stderr
0 3 * * *   /usr/local/bin/zaloha.sh > /dev/null
  • > soubor přesměruje stdout (normální výstup).
  • 2>&1 znamená „a stderr (chyby) pošli tam, kam jde stdout" — tedy taky do logu.
  • > /dev/null = zahodit („černá díra").

💡 Bez 2>&1 bys do logu dostal jen normální výstup a chyby by ti utekly mailem (= do prázdna). Pro ladění cronu je > log 2>&1 tvůj nejlepší kamarád.


systemd timers: moderní alternativa

Na moderních distrech (Fedora, Debian, Ubuntu…) je vedle cronu systemd, který umí časovat taky — přes timery. Je to víc psaní, ale dostaneš věci navíc, které cron nemá. Timer je vždy dvojice:

1) Serviceco se má spustit (/etc/systemd/system/zaloha.service):

[Unit]
Description=Nocni zaloha databaze

[Service]
Type=oneshot
ExecStart=/usr/local/bin/zaloha.sh

2) Timerkdy (/etc/systemd/system/zaloha.timer):

[Unit]
Description=Spousti zalohu kazdy den ve 3:00

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

[Install]
WantedBy=timers.target

Zapneš to a zkontroluješ takhle:

sudo systemctl daemon-reload
sudo systemctl enable --now zaloha.timer   # zapnout + hned aktivovat
systemctl list-timers                       # PŘEHLED: co, kdy naposled, kdy příště
systemctl status zaloha.timer               # stav jednoho timeru
journalctl -u zaloha.service                # LOGY z běhů úlohy

OnCalendar= — zápis času

Čitelnější než cron. Formát je den-v-týdnu rok-měsíc-den hodina:minuta:sekunda:

OnCalendar=*-*-* 03:00:00        # každý den ve 3:00
OnCalendar=Mon..Fri 08:30        # pracovní dny v 8:30
OnCalendar=*-*-01 00:00:00       # 1. den v měsíci o půlnoci
OnCalendar=hourly                 # každou hodinu (existují i daily, weekly…)
OnCalendar=*:0/15                 # každých 15 minut

💡 Ověř si zápis příkazem systemd-analyze calendar "Mon..Fri 08:30" — vypíše ti, kdy přesně to příště padne. Skvělé proti překlepům.

Proč vůbec timers místo cronu

  • Logy zdarma — každý běh jde do journalu (journalctl -u ...). U cronu si logování musíš zařídit ručně přesměrováním. Tohle je největší praktický rozdíl.
  • Persistent=true dožene zmeškané — když byl server v čase úlohy vypnutý, cron ten běh prostě zahodí. Timer s Persistent=true úlohu spustí hned po startu. Zlaté pro notebooky a stroje, co neběží 24/7.
  • Závislosti — service umí After=, Requires= atd. Úloha počká, až naběhne databáze nebo síť.
  • Přesnost a náhodaRandomizedDelaySec= rozhodí čas (aby ti sto serverů nespustilo zálohu v přesně stejnou vteřinu), AccuracySec= řídí přesnost.
  • Jednotné ovládánísystemctl, journalctl — stejné nástroje jako pro všechny ostatní služby.

🚩 Cron není zastaralý ani špatný — pro „spusť jednoduchý skript každou noc" je pořád nejrychlejší. Timers ber tam, kde chceš logy, dohnání zmeškaných běhů nebo závislosti.


Failure modes — časté průšvihy s cronem

  • „Skript jede ručně, z cronu ne." Klasika číslo jedna — cron nezná tvůj PATH ani prostředí. Použij absolutní cesty a nastav PATH v crontabu. (Viz sekce o prostředí výše.)
  • Zapomenuté přesměrování výstupu. Bez > log 2>&1 chyby zmizí (mail nikam nechodí) a ladíš naslepo. Vždycky loguj.
  • Překrývající se běhy. Úloha běží každých 5 minut, ale trvá 8 minut → naskočí druhá, než skončí první, a perou se o zdroje (nebo o zámek databáze). Řešení: flock (zámek souboru) nebo u systemd je to defaultně ošetřené (oneshot service se nespustí dvakrát).
  • Špatná timezone. Cron jede v systémovém čase serveru. Když server běží v UTC a ty čekáš „ve 3 ráno našeho času", spustí se to jindy. Zkontroluj timedatectl a počítej s posunem.
  • Nezálohovaný crontab. crontab -r (blízko -e na klávesnici!) smaže celý crontab bez ptaní. A obecně: crontab žije jen na tom stroji. Zálohuj si ho (crontab -l > crontab-backup.txt) nebo, líp, měj úlohy v /etc/cron.d/ verzované v gitu.
  • Chybějící % escaping. V cronu má % speciální význam (znak nového řádku). Když ho potřebuješ v příkazu (třeba date +%Y), musíš ho uvést jako \% — jinak se řádek utne.

🛠️ Cvičení

  1. Přelož si zápisy. Co dělají tyhle řádky? 30 2 * * *, */10 * * * *, 0 0 * * 0, 15 14 1 * *, 0 9-17 * * 1-5. Řekni každý lidsky (kdy se spustí).
  2. Napiš crontab řádek. Sestav úlohu, která každou noc ve 4:15 spustí /usr/local/bin/zaloha.sh a všechen výstup i chyby ukládá na konec /var/log/zaloha.log. Vysvětli každou část řádku.
  3. Odhal past prostředí. Máš skript, který ručně funguje, ale z cronu ne — vypisuje „command not found". Vyjmenuj aspoň dvě možné příčiny a ke každé konkrétní opravu.
  4. Cron vs systemd timer. Server je přes noc vypnutý a ráno ho zapneš. Noční záloha měla jet ve 3:00. Co udělá cron a co systemd timer s Persistent=true? Který si vybereš a proč?
  5. (navíc) Postav timer. Napiš dvojici .service + .timer, která každých 15 minut spustí /usr/local/bin/check.sh. Jaké příkazy použiješ k zapnutí a ke kontrole, kdy poběží příště?
Náčrt řešení — rozbal, až si cvičení zkusíš sám
  1. Překlad zápisů30 2 * * * = každý den ve 2:30. */10 * * * * = každých 10 minut. 0 0 * * 0 = každou neděli o půlnoci (den v týdnu 0 = neděle). 15 14 1 * * = 1. dne v měsíci ve 14:15. 0 9-17 * * 1-5 = každou celou hodinu od 9 do 17 v pracovní dny (po–pá). Pozor: čte se „minuta hodina", ne naopak.
  2. Crontab řádek15 4 * * * /usr/local/bin/zaloha.sh >> /var/log/zaloha.log 2>&1. Části: 15 4 * * * = ve 4:15 každý den; /usr/local/bin/... = absolutní cesta ke skriptu; >> = přidávej na konec logu (ne přepiš); 2>&1 = i chyby (stderr) pošli do stejného logu. Pozor: bez 2>&1 by chyby utekly „mailem" do prázdna.
  3. Past prostředí — (a) PATH je v cronu oříznutý, takže binárka mimo /usr/bin, /bin se nenajde → oprava: absolutní cesta k příkazu (/usr/local/bin/php) nebo nastav PATH=... nahoře v crontabu. (b) Skript spoléhá na proměnnou nebo alias z .bashrc, který cron nenačítá → oprava: nastav proměnné přímo ve skriptu nebo si na jeho začátku načti prostředí (source ...). Pozor: skoro vždy to je jedna z těchhle dvou věcí.
  4. Cron vs timer — cron zmeškaný běh (server byl ve 3:00 vypnutý) prostě zahodí, záloha se ten den neudělá a spustí se až další noc. systemd timer s Persistent=true si pamatuje, že běh chyběl, a spustí ho hned po startu systému ráno. Pro stroj, co neběží 24/7 (notebook, večer vypínaný server), si vyber timer s Persistent=true. Pozor: čistý cron pro dohánění zmeškaných běhů nástroj nemá.
  5. Timer — service (check.service): [Service] s Type=oneshot a ExecStart=/usr/local/bin/check.sh. Timer (check.timer): [Timer] s OnCalendar=*:0/15 (každých 15 minut) a [Install] WantedBy=timers.target. Zapnutí: sudo systemctl daemon-reload a sudo systemctl enable --now check.timer. Kontrola: systemctl list-timers ukáže, kdy poběží příště; journalctl -u check.service ukáže logy z běhů. Pozor: obě jména (.service i .timer) musí sedět k sobě.

🧠 Otázky & odpovědi

Co znamená pět polí v crontabu a v jakém jsou pořadí?

Zleva doprava: minuta (0–59), hodina (0–23), den v měsíci (1–31), měsíc (1–12) a den v týdnu (0–7, kde 0 i 7 je neděle). Za pěti poli následuje příkaz. Hvězdička * znamená „každý". Takže 0 3 * * * je 3:00 každý den. Nejčastější omyl je záměna prvních dvou polí — čte se minuta pak hodina, tedy 0 3 je ve tři ráno, ne v 0:03.

Proč mi skript funguje ručně, ale z cronu ne?

Skoro vždycky proto, že cron má jiné (skoro prázdné) prostředí než tvůj terminál. Nenačítá .bashrc ani .profile, PATH je oříznutý na pár základních adresářů a chybí proměnné, na které skript spoléhá. Výsledek je typicky „command not found" nebo že skript nenajde soubor. Oprava: používej absolutní cesty ke všem příkazům i souborům, nastav si PATH= nahoře v crontabu, nebo si na začátku skriptu prostředí načti sám. A vždycky loguj výstup, ať vidíš, na čem to padá.

Kam se poděl výstup a chyby z mojí cron úlohy?

Cron posílá výstup příkazu mailem lokálnímu uživateli. Na většině serverů ale žádný mail nastavený není, takže výstup i chybové hlášky prostě zmizí. Proto výstup vždy přesměruj do souboru: ... >> /var/log/uloha.log 2>&1. >> přidává na konec logu, 2>&1 zajistí, že tam skončí i chyby (stderr), ne jen normální výstup. Bez tohohle laděníš cron naslepo.

Kdy použít systemd timer místo cronu?

Cron je super na jednoduché „spusť skript každou noc". systemd timer sáhni tam, kde chceš něco navíc: logy zdarma (každý běh jde do journalu, journalctl -u ...), dohnání zmeškaných běhů (Persistent=true spustí úlohu po startu, když byl stroj v daný čas vypnutý — cron to neumí), závislosti (úloha počká na síť nebo databázi) a jednotné ovládání přes systemctl. Cena je víc psaní — potřebuješ dvojici souborů .service (co) a .timer (kdy).

Co se stane, když úloha trvá déle než její interval?

Dojde k překrývání běhů. Když máš úlohu každých 5 minut, ale ta trvá 8, cron ti v páté minutě spustí druhou kopii, přestože první ještě běží. Dvě kopie se pak perou o stejné zdroje — třeba obě sahají na stejnou databázi nebo soubor a poškodí si data. Řešení u cronu je zámek přes flock, který druhý běh nepustí, dokud první neskončí. U systemd oneshot service tohle řeší systemd sám — nespustí službu podruhé, dokud první instance běží.