Przejdź do treści
Lekcja 87 z 117

Kurs › Moduł 10 › Lekcja 87

Zadania o wyznaczonej godzinie i powiadomienia

⏱ ok. 14 min czytania· moduł 10: Usługi i serwery

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 crond

Na 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 cron87

Przy 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)
│ │ │ │ │
* * * * * komenda

Gwiazdka znaczy „każdy”. Czyli * * * * * to „co minutę, każdej godziny, każdego dnia”. Budzik, który dzwoni bez przerwy. Jak sąsiad z wiertarką.

WpisKiedy
30 3 * * *codziennie o 3:30
0 8 * * 1w poniedziałki o 8:00
*/5 * * * *co 5 minut
0 9-17 * * 1-5o 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
@rebootraz, po starcie systemu
@dailyraz 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.log

I wpis w crontabie:

* * * * * /home/cron87/bin/test.sh

Czekamy do pełnej minuty. I proszę:

2026-11-03 15:30:01 PATH=/usr/bin:/bin SHELL=/bin/sh HOME=/home/cron87 PWD=/home/cron87

Zwróć 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.txt

Ta 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.weekly

Fragment 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>&1

Plik 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ęcznie
raport gotowy

A z crona przyszło takie zgłoszenie:

/bin/sh: 1: raport87: not found

Bo 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 ok

Po 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/nieistniejacy

MAILTO="" 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 postfix

Na 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
mailq
Mail queue is empty

Pusta 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:       cron87

Po każdej zmianie:

sudo newaliases

Bez 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 postfix

Co 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 atd

Zadanie podajesz na wejście:

echo 'echo z at: $(date +%T) >> ~/at.log' | at now + 2 minutes
echo 'touch ~/nigdy' | at 23:59 tomorrow
atq
warning: 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 cron87

atq to kolejka, atrm 2 usuwa zadanie numer 2. Po dwóch minutach:

cat ~/at.log
atrm 2
atq
z at: 15:31:00

Kolejka 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.target

Sam skrypt:

#!/bin/bash
tar czf "/root/etc-$(date +%F).tar.gz" /etc

Włączasz timer, nie usługę:

sudo systemctl daemon-reload
sudo systemctl enable --now kopia.timer
systemctl list-timers

Persistent=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 CET

I pliki też sprawdzisz, zanim cokolwiek włączysz. Literówka OnCalender:

systemd-analyze verify kopia.timer
kopia.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

KomendaFlagaCo robi
crontab-eedytuj swój crontab
crontab-lpokaż swój crontab
crontab-rusuń cały crontab (uwaga!)
crontab-u NAZWAdziałaj na crontabie innego użytkownika (root)
crontabPLIKwgraj crontab z pliku
atnow + 2 minutesjednorazowe zadanie za 2 minuty
atqkolejka zadań at
atrmNUMERusuń zadanie at
systemctllist-timerslista timerów i kiedy ruszą
systemd-analyzecalendar 'WYRAŻENIE'sprawdź, kiedy odpali OnCalendar
systemd-analyzeverify PLIKsprawdź plik jednostki
postconf-e 'klucz = wartość'zmień ustawienie Postfixa
postconf-npokaż ustawienia inne niż domyślne
postmapPLIKzbuduj tablicę (np. z hasłami)
newaliasesprzebuduj /etc/aliases
mail-s "Temat" adreswyślij maila z wejścia
mailqkolejka 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.log

Mija 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.log
15:30

A 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

  1. W crontabie swojego użytkownika ustaw zadanie, które co 5 minut dopisuje do pliku ~/obciazenie.log datę i wynik komendy uptime. Zrób to przez skrypt z pełną ścieżką. Po kwadransie sprawdź, czy w pliku są trzy wpisy.
  2. Przez at zaplanuj za 3 minuty utworzenie pliku ~/at-dziala. Pokaż kolejkę, a po 3 minutach sprawdź, że plik powstał.
  3. Napisz timer systemd, który co 15 minut uruchamia ten sam skrypt. Sprawdź go systemd-analyze verify, włącz i pokaż w systemctl 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.31

Godzina 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
atq

Po 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.target
sudo systemd-analyze verify /etc/systemd/system/obciazenie.timer
sudo systemctl daemon-reload
sudo systemctl enable --now obciazenie.timer
systemctl list-timers obciazenie.timer

verify 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ę.