Zadania o wyznaczonej godzinie i powiadomienia
Po tej lekcji: kursant umie zaplanować zadanie cronem, at i timerem systemd, wie, czemu skrypt „działa ręcznie, a z crona nie”, i dostaje powiadomienia o błędach na maila.
Wstęp
Trzecia w nocy. Ruch na stronie zerowy, baza danych się nudzi. Idealny moment na kopię zapasową. Tylko kto ją zrobi? Ty? O trzeciej? Z kotem na twarzy?
Nie. Admin nie wstaje w nocy, żeby wpisać komendę. Admin raz ustawia budzik i serwer sam o trzeciej robi kopię, czyści stare logi, sprawdza certyfikaty, a rano w skrzynce czeka mail „wszystko poszło” albo „coś się wysypało”.
Dzisiaj poznasz trzy budziki Linuksa: cron do rzeczy powtarzalnych, at do rzeczy jednorazowych i timery systemd, czyli nowocześniejszego kuzyna crona. A na koniec podłączymy pocztę, żeby serwer umiał na Ciebie naskarżyć.
Cron, czyli budzik z listą
Wyobraź sobie budzik, który nie tylko dzwoni, ale ma przyklejoną karteczkę: „o 3:00 zrób kopię, w poniedziałki o 8:00 wyślij raport, co 5 minut sprawdź dysk”. Co minutę budzik zerka na karteczkę i jeśli coś pasuje do aktualnej godziny, odpala to.
Ten budzik to cron, program, który działa w tle przez cały czas. Karteczka to crontab, od „cron table”, tabela crona. Każdy użytkownik ma swoją.
[Ubuntu] Pakiet cron, usługa cron. [RHEL] Pakiet cronie, usługa crond.
# Ubuntu
sudo apt install cron
systemctl status cron
# RHEL
sudo dnf install cronie
systemctl status crondNa obu jest zwykle zainstalowany od razu. Swoją karteczkę oglądasz i edytujesz tak:
crontab -l # pokaż moją listę
crontab -e # edytuj moją listę
sudo crontab -u eryk -l # admin podgląda cudzą listęJak nie masz jeszcze żadnych zadań:
no crontab for cron87Przy pierwszym crontab -e na Ubuntu system zapyta, jakim edytorem chcesz pracować. Wybierz nano, jeśli vim jeszcze Cię straszy. Nikt nie ocenia. Prawie nikt.
Pięć gwiazdek
Każda linijka w crontabie to jedno zadanie. Pięć pól czasu, potem komenda:
┌──────── minuta (0-59)
│ ┌────── godzina (0-23)
│ │ ┌──── dzień miesiąca (1-31)
│ │ │ ┌── miesiąc (1-12)
│ │ │ │ ┌ dzień tygodnia (0-7, 0 i 7 to niedziela)
│ │ │ │ │
* * * * * komendaGwiazdka znaczy „każdy”. Czyli * * * * * to „co minutę, każdej godziny, każdego dnia”. Budzik, który dzwoni bez przerwy. Jak sąsiad z wiertarką.
| Wpis | Kiedy |
|---|---|
30 3 * * * | codziennie o 3:30 |
0 8 * * 1 | w poniedziałki o 8:00 |
*/5 * * * * | co 5 minut |
0 9-17 * * 1-5 | o pełnej godzinie od 9 do 17, w dni robocze |
0 0 1 * * | pierwszego dnia miesiąca o północy |
15 6,18 * * * | o 6:15 i 18:15 |
@reboot | raz, po starcie systemu |
@daily | raz dziennie o północy |
Gwiazdka z ukośnikiem to krok: */5 to „co piąta minuta”. Myślnik to zakres: 1-5 to poniedziałek–piątek. Przecinek to lista: 6,18.
Zróbmy to naprawdę. Mały skrypt, który zapisuje, w jakich warunkach go uruchomiono:
#!/bin/bash
echo "$(date '+%F %T') PATH=$PATH SHELL=$SHELL HOME=$HOME PWD=$PWD" >> /home/cron87/cron-env.logI wpis w crontabie:
* * * * * /home/cron87/bin/test.shCzekamy do pełnej minuty. I proszę:
2026-11-03 15:30:01 PATH=/usr/bin:/bin SHELL=/bin/sh HOME=/home/cron87 PWD=/home/cron87Zwróć uwagę na dwie rzeczy. Sekunda 01, czyli cron odpala zadania na początku minuty, nie co do sekundy. I PATH=/usr/bin:/bin. Zapamiętaj to, bo za chwilę ten krótki PATH zepsuje komuś noc.
Ostrożnie z literką r
crontab -e edytuje. crontab -r usuwa CAŁĄ Twoją listę. Bez pytania, bez kosza.
A teraz spójrz na klawiaturę. E i R leżą obok siebie. [na ekranie: zbliżenie na klawisze E i R, dramatyczna muzyka]
Dlatego zanim zaczniesz grzebać, zrób kopię:
crontab -l > ~/crontab-kopia.txt
# a w razie wypadku:
crontab ~/crontab-kopia.txtTa druga komenda wgrywa listę z pliku. Przydaje się też w skryptach: przygotowujesz plik i wgrywasz go jednym ruchem.
Budziki systemowe: /etc/crontab i /etc/cron.d
Twoja prywatna karteczka to jedno. Ale system ma też swoje, wspólne dla całego budynku.
cat /etc/crontab
ls /etc/cron.d /etc/cron.daily /etc/cron.weeklyFragment z Ubuntu:
17 * * * * root cd / && run-parts --report /etc/cron.hourly
25 6 * * * root test -x /usr/sbin/anacron || { cd / && run-parts --report /etc/cron.daily; }Widzisz różnicę? Po pięciu polach czasu jest szóste: nazwa użytkownika. W /etc/crontab i w plikach w /etc/cron.d/ mówisz nie tylko kiedy, ale też jako kto. W prywatnym crontabie tego pola nie ma, bo wiadomo, czyja to karteczka.
Pakiety chętnie wrzucają tu swoje pliki. Po instalacji certbota w /etc/cron.d/certbot pojawia się zadanie odnawiania certyfikatów. Własne zadania systemowe też najlepiej wrzucać jako osobny plik w /etc/cron.d/:
# /etc/cron.d/test87
MAILTO=""
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
* * * * * cron87 raport87 >> /home/cron87/z-crond.log 2>&1Plik ma mieć prawa 644 i właściciela root. Nie używaj w nazwie kropki, bo na Debianie i Ubuntu cron po cichu pomija pliki w /etc/cron.d, których nazwy mają kropki. Sprawdziłem: plik bezkropki ruszył po minucie, a identyczny kropka.test nie zrobił nic. Żadnego błędu, żadnego ostrzeżenia. Plik kopia.sh w /etc/cron.d po prostu nie zadziała, a Ty będziesz szukać błędu w skrypcie.
Foldery /etc/cron.daily, /etc/cron.weekly, /etc/cron.monthly to jeszcze prostsza wersja: wrzucasz tam wykonywalny skrypt i on leci raz dziennie, raz w tygodniu, raz w miesiącu. Uruchamia je run-parts, który też nie lubi kropek w nazwach.
„Działa ręcznie, a z crona nie”
Najczęstsze zdanie w historii administracji, zaraz po „u mnie działa”.
Cron to nie Ty. Cron uruchamia zadanie w bardzo ubogim środowisku. To jak wysłanie kucharza do obcej kuchni bez noży, bez przypraw i z kartką „zrób to samo co w domu”. Trzy główne powody:
1. Krótki PATH. Widziałeś: /usr/bin:/bin. Nie ma /usr/local/bin, nie ma /usr/sbin. Jeśli Twój program leży gdzie indziej, cron go nie znajdzie.
raport87 # ręcznieraport gotowyA z crona przyszło takie zgłoszenie:
/bin/sh: 1: raport87: not foundBo raport87 leży w /usr/local/bin. Naprawa: pisz pełne ścieżki (/usr/local/bin/raport87) albo ustaw PATH= na górze crontaba, tak jak w pliku z /etc/cron.d wyżej.
2. Inna powłoka i inny folder. SHELL=/bin/sh, czyli na Ubuntu dash, nie Bash. Bashowe wynalazki wpisane prosto w crontab mogą się wyłożyć. Folder startowy to Twój domowy, a nie ten, w którym akurat stałeś. Ścieżki względne typu ./backup.sh to proszenie się o kłopoty.
3. Znak procentu. O nim za chwilę w typowym błędzie, bo zasługuje na osobną scenę.
MAILTO, czyli cron, który donosi
Skąd wiedziałem, że z crona przyszedł błąd raport87: not found? Bo cron wysyła mailem wszystko, co zadanie wypisało na ekran, i normalny wynik, i błędy. Komu? Właścicielowi crontaba albo temu, kogo wskażesz zmienną MAILTO:
MAILTO=admin87
* * * * * raport87
* * * * * cd ~/nieistniejacy && echo okPo minucie w skrzynce:
--- Cron <cron87@vm> raport87 | root@vm (Cron Daemon) | admin87@vm
/bin/sh: 1: raport87: not found
--- Cron <cron87@vm> cd ~/nieistniejacy && echo ok | root@vm (Cron Daemon) | admin87@vm
/bin/sh: 1: cd: can't cd to /home/cron87/nieistniejacyMAILTO="" wyłącza maile. Ale uwaga: jak wyłączysz maile i nie przekierujesz wyniku do pliku, błędy znikają w próżni. Zadanie nie działa od pół roku, a Ty nic nie wiesz. Zasada: albo mail, albo log, nigdy „nic”.
Żeby to działało, serwer musi umieć wysłać maila. I tu wchodzi Postfix.
Postfix: poczta na serwerze
Postfix to poczta w budynku. Na serwerze nie stawiamy pełnego serwera pocztowego dla firmy, tylko skrzynkę nadawczą: przyjmij list od crona i oddaj go dalej.
# Ubuntu
sudo apt install postfix bsd-mailx
# RHEL
sudo dnf install postfix s-nail
sudo systemctl enable --now postfixNa Ubuntu instalator zapyta o typ konfiguracji. Na początek wybierz „Local only”, czyli poczta tylko w obrębie serwera. bsd-mailx na Ubuntu i s-nail na RHEL dają komendę mail.
Test:
echo "Test z serwera" | mail -s "Test powiadomienia" admin87
mailqMail queue is emptyPusta kolejka to dobra wiadomość: list już doszedł. Lokalne skrzynki leżą w /var/mail/NAZWA.
/etc/aliases to książka adresowa budynku. „Listy do kierownika oddawaj Ani”:
postmaster: root
root: eryk
admin87: cron87Po każdej zmianie:
sudo newaliasesBez newaliases Postfix dalej czyta starą wersję, bo tak naprawdę korzysta z przetworzonej kopii tego pliku, a nie z tekstu.
Postfix jako klient wysyłający
Poczta do /var/mail jest fajna, ale nikt tam nie zagląda. Chcesz, żeby powiadomienia trafiały na prawdziwy adres, na przykład w Google Workspace albo Microsoft 365. Wtedy Postfix nie dostarcza listów sam, tylko oddaje je do prawdziwego serwera pocztowego, jak list wrzucony do skrzynki na poczcie. Taki serwer pośredni to relayhost.
sudo postconf -e 'relayhost = [smtp.example.com]:587'
sudo postconf -e 'smtp_sasl_auth_enable = yes'
sudo postconf -e 'smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd'
sudo postconf -e 'smtp_sasl_security_options = noanonymous'
sudo postconf -e 'smtp_tls_security_level = encrypt'
sudo postconf -e 'inet_interfaces = loopback-only'Login i hasło do skrzynki:
echo '[smtp.example.com]:587 serwer@example.com:TajneHaslo' | sudo tee /etc/postfix/sasl_passwd
sudo chmod 600 /etc/postfix/sasl_passwd
sudo postmap /etc/postfix/sasl_passwd
sudo systemctl reload postfixCo tu się dzieje: relayhost mówi „wszystko oddawaj temu serwerowi na porcie 587”. Nawiasy kwadratowe znaczą „to dokładnie ten serwer, nie szukaj w DNS rekordów MX”. encrypt wymusza szyfrowanie, bo hasło po sieci otwartym tekstem to jak krzyczenie PIN-u przy bankomacie. loopback-only sprawia, że Postfix przyjmuje listy tylko od programów z tego serwera, a nie od całego internetu. I bardzo ważne: chmod 600 na pliku z hasłem.
[RHEL] RHEL 10 nie ma już biblioteki Berkeley DB, więc typ hash: nie działa. Domyślny jest lmdb:. Na RHEL 10 piszesz smtp_sasl_password_maps = lmdb:/etc/postfix/sasl_passwd. Jak skopiujesz stary poradnik z hash:, Postfix poskarży się na nieobsługiwany typ słownika.
at, czyli jednorazowe przypomnienie
Cron to budzik dzwoniący codziennie. A czasem potrzebujesz karteczki: „za dwie godziny zrestartuj usługę” albo „jutro o 23:59 wyłącz konto stażysty”. Do tego jest at.
sudo apt install at # Ubuntu
sudo dnf install at # RHEL
sudo systemctl enable --now atdZadanie podajesz na wejście:
echo 'echo z at: $(date +%T) >> ~/at.log' | at now + 2 minutes
echo 'touch ~/nigdy' | at 23:59 tomorrow
atqwarning: commands will be executed using /bin/sh
job 1 at Tue Nov 3 15:31:00 2026
warning: commands will be executed using /bin/sh
job 2 at Wed Nov 4 23:59:00 2026
1 Tue Nov 3 15:31:00 2026 a cron87
2 Wed Nov 4 23:59:00 2026 a cron87atq to kolejka, atrm 2 usuwa zadanie numer 2. Po dwóch minutach:
cat ~/at.log
atrm 2
atqz at: 15:31:00Kolejka pusta, zadanie numer 2 nigdy się nie wykona. Plik nigdy dotrzymał słowa.
at rozumie sporo zapisów: now + 1 hour, 16:00, noon, midnight, tomorrow, a nawet teatime, czyli 16:00. Brytyjskie korzenie. Jest też batch: odpal zadanie, gdy serwer nie będzie zbyt zajęty.
Timery systemd, czyli budzik z kierownikiem zmiany
Pamiętasz systemd, kierownika zmiany? On też umie budzić. Timer systemd to dwa pliki: usługa, czyli co zrobić, i timer, czyli kiedy.
/etc/systemd/system/kopia.service:
[Unit]
Description=Nocna kopia folderu /etc
[Service]
Type=oneshot
ExecStart=/usr/local/bin/kopia-etc.sh/etc/systemd/system/kopia.timer:
[Unit]
Description=Codziennie o 2:30 uruchom kopia.service
[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
RandomizedDelaySec=10min
[Install]
WantedBy=timers.targetSam skrypt:
#!/bin/bash
tar czf "/root/etc-$(date +%F).tar.gz" /etcWłączasz timer, nie usługę:
sudo systemctl daemon-reload
sudo systemctl enable --now kopia.timer
systemctl list-timersPersistent=true to anacron w wersji systemd: jeśli serwer był wyłączony o 2:30, zadanie odpali się zaraz po starcie. RandomizedDelaySec rozrzuca start w losowym oknie, żeby sto serwerów nie waliło do jednego magazynu kopii w tej samej sekundzie.
Zapis czasu w OnCalendar jest inny niż w cronie. Najlepsze jest to, że możesz go sprawdzić przed użyciem:
systemd-analyze calendar 'Mon..Fri 08:00' --iterations=3 Original form: Mon..Fri 08:00
Normalized form: Mon..Fri *-*-* 08:00:00
Next elapse: Wed 2026-11-04 08:00:00 CET
(in UTC): Wed 2026-11-04 07:00:00 UTC
From now: 15h left
Iteration #2: Thu 2026-11-05 08:00:00 CET
(in UTC): Thu 2026-11-05 07:00:00 UTC
From now: 1 day 15h left
Iteration #3: Fri 2026-11-06 08:00:00 CETI pliki też sprawdzisz, zanim cokolwiek włączysz. Literówka OnCalender:
systemd-analyze verify kopia.timerkopia.timer:5: Unknown key name 'OnCalender' in section 'Timer', ignoring.
kopia.timer: Timer unit lacks value setting. Refusing.Cron by tego nie powiedział. Cron by po prostu milczał.
Cron czy timer? Cron: jedna linijka, znany wszędzie, prosto. Timer: dwa pliki, ale logi w journalctl -u kopia.service, zależności, losowe opóźnienie, nadrabianie. Na RHCSA trzeba umieć jedno i drugie.
Jak to działa pod spodem
Demon crona budzi się co minutę, sprawdza, czy ktoś zmienił crontaby, i porównuje aktualny czas z każdą linijką. Prywatne crontaby leżą w [Ubuntu] /var/spool/cron/crontabs/NAZWA, [RHEL] /var/spool/cron/NAZWA. Nie edytuj ich ręcznie, od tego jest crontab -e, który przed zapisem sprawdza składnię. Uruchomione zadanie dostaje środowisko zbudowane od zera, a jego wyjście cron zbiera i oddaje do programu sendmail, który dostarcza Postfix.
Komenda crontab na Ubuntu ma ustawiony bit setgid na grupę crontab (-rwxr-sr-x root crontab). Dzięki temu zwykły użytkownik może zapisać swoją listę w folderze, do którego sam nie ma wstępu. Kto w ogóle może używać crona, decydują pliki /etc/cron.allow i /etc/cron.deny, a dla at: /etc/at.allow i /etc/at.deny.
Ślady zostają w logach: [Ubuntu] journalctl -u cron albo /var/log/syslog, [RHEL] /var/log/cron. Tam widać każdą linijkę CMD (...), czyli co cron faktycznie uruchomił. To bardzo się przyda za chwilę.
Timery systemd działają zupełnie inaczej: nie ma minutowego sprawdzania, systemd wylicza najbliższy termin i czeka dokładnie do niego. A poczta z Postfixa: [Ubuntu] journalctl -u postfix lub /var/log/mail.log, [RHEL] /var/log/maillog.
Ściąga flag
| Komenda | Flaga | Co robi |
|---|---|---|
crontab | -e | edytuj swój crontab |
crontab | -l | pokaż swój crontab |
crontab | -r | usuń cały crontab (uwaga!) |
crontab | -u NAZWA | działaj na crontabie innego użytkownika (root) |
crontab | PLIK | wgraj crontab z pliku |
at | now + 2 minutes | jednorazowe zadanie za 2 minuty |
atq | kolejka zadań at | |
atrm | NUMER | usuń zadanie at |
systemctl | list-timers | lista timerów i kiedy ruszą |
systemd-analyze | calendar 'WYRAŻENIE' | sprawdź, kiedy odpali OnCalendar |
systemd-analyze | verify PLIK | sprawdź plik jednostki |
postconf | -e 'klucz = wartość' | zmień ustawienie Postfixa |
postconf | -n | pokaż ustawienia inne niż domyślne |
postmap | PLIK | zbuduj tablicę (np. z hasłami) |
newaliases | przebuduj /etc/aliases | |
mail | -s "Temat" adres | wyślij maila z wejścia |
mailq | kolejka poczty do wysłania |
⚠ Typowy błąd
Chcesz, żeby cron zapisywał godzinę do pliku. Wpisujesz w crontab dokładnie to, co działa w terminalu:
* * * * * date +%H:%M >> /home/cron87/zly.logMija minuta. Pliku zly.log nie ma. Zaglądasz do logu crona:
CRON[11824]: (cron87) CMD (date +)date +. Reszta zniknęła. Dlaczego? W crontabie znak % jest specjalny: oznacza koniec komendy i początek tekstu, który cron poda komendzie na wejście. Wszystko po pierwszym % poleciało w kosmos, a razem z tym przekierowanie do pliku.
Naprawa: ucieknij procentom backslashem:
* * * * * date +\%H:\%M >> /home/cron87/godzina.log15:30A najlepsza naprawa to nie trzymać logiki w crontabie. Wszystko, co dłuższe niż jedno słowo, wrzucasz do skryptu, a w crontabie zostaje tylko pełna ścieżka do niego. W skrypcie % jest zwykłym znakiem.
✎ Zadanie
- W crontabie swojego użytkownika ustaw zadanie, które co 5 minut dopisuje do pliku
~/obciazenie.logdatę i wynik komendyuptime. Zrób to przez skrypt z pełną ścieżką. Po kwadransie sprawdź, czy w pliku są trzy wpisy. - Przez
atzaplanuj za 3 minuty utworzenie pliku~/at-dziala. Pokaż kolejkę, a po 3 minutach sprawdź, że plik powstał. - Napisz timer systemd, który co 15 minut uruchamia ten sam skrypt. Sprawdź go
systemd-analyze verify, włącz i pokaż wsystemctl list-timers.
Rozwiązanie znajdziesz pod spodem.
Pokaż rozwiązanie
Skrypt, który dopisuje obciążenie do pliku, ~/bin/obciazenie.sh:
#!/bin/bash
# Dopisuje date i obciazenie do dziennika
echo "$(date '+%F %T') $(uptime)" >> "$HOME/obciazenie.log"ShellCheck nie ma uwag. Wpis w crontabie, przez crontab -e:
*/5 * * * * $HOME/bin/obciazenie.sh$HOME zadziała, bo cron ustawia tę zmienną, a linijkę i tak wykonuje powłoka. Po chwili:
2026-11-03 15:40:01 15:40:01 up 22 min, 0 user, load average: 1.45, 1.63, 1.31Godzina 15:40:01, a nie żadna 15:43. */5 to nie „co 5 minut od teraz”, tylko „w minutach podzielnych przez 5”: :00, :05, :10. Cron nie liczy od chwili, kiedy go ustawiłeś. Cron patrzy na zegar ścienny jak pracownik, który czeka na równą godzinę, żeby iść na przerwę.
Druga część, at:
echo "touch ~/at-dziala" | at now + 3 minutes
atqPo trzech minutach ls -l ~/at-dziala pokazuje pusty plik. Gotowe.
Trzecia część, timer. /etc/systemd/system/obciazenie.service:
[Unit]
Description=Zapis obciazenia do dziennika
[Service]
Type=oneshot
User=eryk
ExecStart=/home/eryk/bin/obciazenie.sh/etc/systemd/system/obciazenie.timer:
[Unit]
Description=Co 15 minut zapisz obciazenie
[Timer]
OnCalendar=*:0/15
Persistent=true
[Install]
WantedBy=timers.targetsudo systemd-analyze verify /etc/systemd/system/obciazenie.timer
sudo systemctl daemon-reload
sudo systemctl enable --now obciazenie.timer
systemctl list-timers obciazenie.timerverify milczy, czyli jest dobrze. W systemd cisza to pochwała. *:0/15 znaczy: każda godzina, minuty od zera co piętnaście. Jak nie wierzysz, systemd-analyze calendar '*:0/15' pokaże *-*-* *:00/15:00 i najbliższy termin. Bez User= skrypt poleciałby jako root i $HOME wskazywałoby na /root. Plik z obciążeniem lądowałby w mieszkaniu dyrektora, a Ty szukałbyś go u siebie.
Co dalej
Mamy budziki, mamy pocztę. Czas na pierwszą prawdziwą usługę, którą zobaczy świat: serwer WWW. W następnej lekcji instalujemy Apache, otwieramy port 80 i wrzucamy pierwszą stronę.