Tworzenie własnej usługi systemd i automatyczny start przy rozruchu

Opublikowano 14 min czytania

Plik jednostki wiersz po wierszu: Type, User, WorkingDirectory, Restart i network-online.target. Z komunikatami błędów w oryginalnym brzmieniu i z dowodem, że usługa naprawdę przetrwa restart serwera.

Program, który po restarcie nie wraca sam z siebie, nie jest na serwerze usługą, tylko ryzykiem. nohup, screentmux utrzymują proces przy życiu tylko tak długo, jak długo nic się nie dzieje. Ten artykuł pisze własny plik jednostki wiersz po wierszu, pokazuje komunikaty błędów w oryginalnym brzmieniu i opisuje, jak się z tego wydostać, gdy usługa utknie w karuzeli startów.

Wszystkie polecenia wykonujesz jako root. Sprawdzone na Debianie 13 (systemd 257), Debianie 12 (252), Ubuntu 24.04 LTS (255) oraz Ubuntu 22.04 LTS (249). Tam, gdzie te cztery systemy się różnią, jest o tym mowa.

Co potrafi usługa systemd, a czego nie potrafią nohup i screen

Różnica nie polega na wygodzie, tylko na odpowiedzialności. systemd uruchamia proces przy starcie systemu w ustalonej kolejności, umieszcza go we własnej cgroup, zbiera stdout i stderr w journalu, restartuje go po awarii i kończy czysto przy wyłączaniu, sygnałem SIGTERM. Najbardziej brakuje właśnie cgroup: skrypt uruchomiony przez nohup zostawia po sobie osierocone procesy potomne, a jednostka sprząta całą swoją grupę.

Przygotowanie: program, użytkownik i katalog

Zanim powstanie jednostka, musi działać to, co ma ona uruchamiać. Za przykład posłuży skrypt, który pisze na stdout. I o to właśnie chodzi: usługa pod systemd nie loguje sama do pliku, tylko pisze na stdout, a systemd odkłada to do journala.

mkdir -p /opt/kh-demo
cat > /opt/kh-demo/run.sh <<'EOF'
#!/bin/bash
set -euo pipefail
while true; do
  echo "kh-demo działa, $(date --iso-8601=seconds), PWD=$PWD, GREETING=${GREETING:-nie ustawiono}"
  sleep 10
done
EOF
chmod +x /opt/kh-demo/run.sh

Następnie własny użytkownik systemowy: --system przydziela UID poniżej 1000, a /usr/sbin/nologin uniemożliwia logowanie interaktywne.

useradd --system --no-create-home --home-dir /opt/kh-demo --shell /usr/sbin/nologin khdemo
id khdemo
chown -R root:khdemo /opt/kh-demo
chmod 750 /opt/kh-demo

Kluczowy test przed napisaniem pliku jednostki: czy program działa na koncie tego użytkownika? Kto go pominie, będzie później debugować systemd, choć problem siedzi w samym programie.

timeout 3 runuser -u khdemo -- /opt/kh-demo/run.sh; echo "Kod wyjścia $?"

Kod wyjścia 124 to tutaj wynik pożądany: timeout przerwał działający program. Każda inna wartość oznacza, że skrypt zakończył się sam z siebie, a wtedy winy nie ponosi systemd.

Plik jednostki wiersz po wierszu

Własne jednostki trafiają do /etc/systemd/system/, a nie do /lib/systemd/system/ ani /usr/lib/systemd/system/: te dwa katalogi należą do menedżera pakietów i zostaną nadpisane przy najbliższym apt upgrade.

cat > /etc/systemd/system/kh-demo.service <<'EOF'
[Unit]
Description=KernelHost Demo Worker
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=khdemo
Group=khdemo
WorkingDirectory=/opt/kh-demo
EnvironmentFile=-/etc/kh-demo.env
ExecStart=/opt/kh-demo/run.sh
Restart=on-failure
RestartSec=5s
TimeoutStopSec=20s
SyslogIdentifier=kh-demo
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full

[Install]
WantedBy=multi-user.target
EOF

Sekcja [Unit]

Description= to tekst widoczny w systemctl status. Nie ma żadnego znaczenia technicznego, ale decyduje o tym, czy o trzeciej w nocy zrozumiesz, co właściwie jest w stanie failed. Całe zdanie, nie jedno słowo.

After= ustala wyłącznie kolejność, a nie zależność. After=network-online.target znaczy tyle: jeśli ten target i tak wystartuje w tym rozruchu, zaczekaj na niego. Nie znaczy, że zostanie uruchomiony.

Wants= faktycznie go zamawia, i to jako zależność słabą. Jeśli target zawiedzie, usługa mimo wszystko wystartuje. Odpowiednik Requires= pociągnąłby usługę za sobą w błąd. Dla większości zastosowań właściwe jest Wants=, bo usługa, która przez timeout sieci w ogóle nie ruszy, jest gorsza od takiej, która przez chwilę działa w próżnię i dzięki Restart= wraca.

Sekcja [Service]

User= oraz Group= ustalają tożsamość procesu. Bez tych wierszy usługa działa jako root. Na Debianie i Ubuntu useradd zakłada domyślnie grupę o tej samej nazwie, dlatego Group=khdemo pasuje.

WorkingDirectory= to katalog roboczy procesu. Bez tego wpisu usługa startuje w /. Każdy program, który szuka plików konfiguracyjnych, szablonów albo wtyczek względem bieżącego katalogu, od razu wtedy tonie. Jeśli podany katalog nie istnieje, start kończy się kodem 200/CHDIR.

EnvironmentFile= wczytuje zmienne z pliku. Wiodący minus przed ścieżką sprawia, że plik jest opcjonalny. Bez tego minusa start zawodzi, gdy pliku zabraknie. Zawartość to zwykłe wiersze KEY=VALUE, a nie shell. export na początku jest błędem, a PORT=$BASE_PORT nie zostanie rozwinięte.

ExecStart= potrzebuje bezwzględnej ścieżki do pliku wykonywalnego. To reguła, na której potyka się najwięcej osób, dlatego niżej ma własną sekcję.

TimeoutStopSec= ogranicza, jak długo systemd czeka po SIGTERM, zanim wyśle SIGKILL. Domyślnie jest to 90 sekund.

SyslogIdentifier= ustawia nazwę, pod którą wiersze pojawiają się w journalu. Bez tego wiersza nadawca nazywa się run.sh, co przy filtrowaniu jest bezużyteczne.

NoNewPrivileges=true zabrania procesowi i wszystkim procesom potomnym zdobywania uprawnień przez binaria z setuid. PrivateTmp=true daje usłudze własny katalog /tmp. ProtectSystem=full montuje /usr, /boot/etc tylko do odczytu. Poziom strict idzie dalej i wymaga wtedy StateDirectory= albo ReadWritePaths= dla wszystkiego, co ma być zapisywalne.

Sekcja [Install]

WantedBy=multi-user.target odpowiada na pytanie, kiedy usługa ma startować automatycznie. multi-user.target to normalna praca wielodostępowa bez interfejsu graficznego, czyli dokładnie ten stan, do którego dochodzi serwer. Dopiero systemctl enable analizuje tę sekcję i zakłada dowiązanie symboliczne. Gdy zabraknie [Install], enable przerywa działanie komunikatem The unit files have no installation config (WantedBy=, RequiredBy=, Also=, Alias= settings in the [Install] section, and DefaultInstance= for template units). Usługę da się wtedy uruchomić ręcznie, ale po restarcie nigdy już nie wróci.

Type=simple, Type=exec i Type=forking

Type= odpowiada dokładnie na jedno pytanie: po czym systemd pozna, że usługa wystartowała?

Przy Type=simple usługa uchodzi za uruchomioną, gdy tylko proces zostanie utworzony, czyli po fork(), a jeszcze przed wykonaniem samego programu. To wartość domyślna i właściwa dla niemal wszystkich nowoczesnych programów, które pozostają na pierwszym planie.

Przy Type=exec systemd czeka dodatkowo, aż powiedzie się execve(). Różnica w praktyce jest spora: przy Type=simple polecenie systemctl start zgłasza sukces nawet wtedy, gdy pliku binarnego w ogóle nie ma, a błąd wypływa dopiero potem w journalu. Przy Type=exec start w dokładnie takim przypadku zawodzi natychmiast. Dostępne od systemd 240, czyli na wszystkich czterech dystrybucjach.

Przy Type=forking systemd zakłada, że program sam przechodzi w tło: uruchomiony proces kończy się, proces potomny działa dalej, a za sygnał startu uchodzi dopiero to zakończenie. To klasyczne zachowanie uniksowego demona. Kto korzysta z tego typu, potrzebuje z reguły dodatkowo PIDFile= z bezwzględną ścieżką, inaczej systemd zgaduje, który z pozostałych procesów jest procesem głównym.

Zalecenie jest jednoznaczne: Type=forking tylko wtedy, gdy programu absolutnie nie da się od tego odwieść. Prawie każde oprogramowanie ma na to przełącznik, często --foreground, -D FOREGROUND, --no-daemon albo daemon off; w konfiguracji. Praca na pierwszym planie plus Type=simple daje krótszą jednostkę, logi trafiają do journala, a Restart= działa niezawodnie.

Typowe błędne połączenie: program, który sam przechodzi w tło, pod Type=simple. systemd widzi koniec procesu startowego, uznaje usługę za zakończoną i zabija razem z nią procesy potomne. Objawem jest Active: inactive (dead) zaraz po systemctl start, które nie zgłosiło żadnego błędu. Przypadek odwrotny, czyli program pierwszoplanowy pod Type=forking, każe systemd czekać na koniec, który nigdy nie nadchodzi: Job for foo.service failed because a timeout was exceeded, po 90 sekundach.

Poprawne podłączenie After=network-online.target

W tym miejscu prawie każdy poradnik urywa się za wcześnie. network.target oznacza jedynie, że wystartowało zarządzanie siecią, a nie że skonfigurowany jest adres IP. Kto potrzebuje osiągalnego adresu, na przykład dlatego, że program dowiązuje się do stałego IP, chce network-online.target.

Ten target nie jest jednak osiągany sam z siebie. Wymaga włączonej odpowiedniej usługi oczekującej, a ta różni się w zależności od zarządcy sieci:

systemctl list-unit-files 'systemd-networkd-wait-online.service' 'NetworkManager-wait-online.service' 'ifupdown-wait-online.service' --no-pager

Na Ubuntu Server 22.04 i 24.04 siecią zarządza netplan z systemd-networkd, tam systemd-networkd-wait-online.service jest aktywna, a network-online.target ma realne znaczenie. W klasycznej instalacji Debiana z ifupdown usługa ifupdown-wait-online.service wprawdzie istnieje, ale nie jest włączona. Target uchodzi wtedy natychmiast za osiągnięty, a oczekiwanie, na którym polegasz, w ogóle nie następuje.

Na Debianie 12 i 13 z ifupdown usługę oczekującą włączasz w razie potrzeby jednorazowo:

systemctl enable ifupdown-wait-online.service

Włączanie dwóch usług oczekujących naraz to zły pomysł, bo obie czekają wtedy na swojego zarządcę sieci, a jedna z nich nieuchronnie wpada w 90-sekundowy timeout. To najczęstsza przyczyna tego, że serwer nagle uruchamia się półtorej minuty dłużej. Od każdej kolejności i tak solidniejszy jest program, który znosi brak połączenia przy starcie, w parze z Restart=on-failure.

Restart, RestartSec i hamulec startowy

Restart=on-failure restartuje usługę przy kodzie wyjścia różnym od zera, przy sygnale takim jak SIGSEGV oraz wtedy, gdy upłynie timeout watchdoga, ale nie po wyjściu z kodem 0 i nie po ręcznym systemctl stop. Restart=always restartuje dodatkowo po czystym zakończeniu, a tym najszybciej buduje się nieskończoną pętlę.

systemd ma na to wbudowany hamulec: domyślnie dozwolonych jest pięć prób startu w ciągu dziesięciu sekund (StartLimitBurst=5, StartLimitIntervalSec=10s). Potem systemd się poddaje i zgłasza:

kh-demo.service: Start request repeated too quickly.
kh-demo.service: Failed with result 'exit-code'.
Failed to start kh-demo.service - KernelHost Demo Worker.

Usługa zostaje wtedy w stanie failed i nie reaguje już nawet na systemctl start, dopóki licznik nie zostanie wyzerowany:

systemctl reset-failed kh-demo.service

Dwa szczegóły regularnie kosztują tu czas. Po pierwsze, StartLimitBurst= oraz StartLimitIntervalSec= należą do sekcji [Unit], a nie do [Service]. W niewłaściwej sekcji zostają zignorowane, z ostrzeżeniem w journalu, ale bez błędu. Po drugie, ręczne wywołania systemctl restart też się liczą: kto podczas debugowania zrestartuje usługę pięć razy, sam zaciągnie ten hamulec.

Reguła kciuka: StartLimitIntervalSec większy niż RestartSec razy StartLimitBurst.

Na Debianie 13 i Ubuntu 24.04 dochodzą jeszcze RestartSteps= oraz RestartMaxDelaySec= (od systemd 254) dla wykładniczo rosnących czasów oczekiwania. Na Debianie 12 i Ubuntu 22.04 tych dyrektyw nie ma i są ignorowane.

Włączanie, uruchamianie i dowód, że usługa działa

Przed pierwszym startem warto sprawdzić składnię. Kontrola znajdzie literówki w nazwach dyrektyw, źle zapisane sekcje i brakujące programy, niczego przy tym nie uruchamiając:

systemd-analyze verify /etc/systemd/system/kh-demo.service

Brak jakiegokolwiek wyjścia oznacza: wszystko w porządku. Literówka w rodzaju WorkingDirectiry= wywołuje, zależnie od wersji systemd, Unknown key name 'WorkingDirectiry' in section 'Service', ignoring albo Unknown key 'WorkingDirectiry' in section [Service], ignoring. Tu rodzą się ciche błędy: systemd ignoruje nieznane klucze, usługa startuje, ale zachowuje się inaczej, niż oczekujesz.

systemctl daemon-reload

daemon-reload wczytuje pliki jednostek na nowo, ale niczego nie restartuje. Kto o przeładowaniu zapomni, przy najbliższym systemctl status zobaczy: Warning: The unit file, source configuration file or drop-ins of kh-demo.service changed on disk. Run 'systemctl daemon-reload' to reload units. Zapamiętaj kolejność: najpierw daemon-reload, potem restart.

systemctl enable kh-demo.service
systemctl start kh-demo.service

Oba kroki naraz załatwia systemctl enable --now kh-demo.service.

Teraz część, którą większość poradników pomija: dowód, że naprawdę zadziałało, a nie tylko że nie pojawił się błąd. Cztery niezależne potwierdzenia.

Po pierwsze, istnieje dowiązanie symboliczne. To cały mechanizm, jaki kryje się za enable. Jeśli go brakuje, usługa po restarcie nie wystartuje, niezależnie od tego, co akurat mówi status.

ls -l /etc/systemd/system/multi-user.target.wants/kh-demo.service
systemctl is-enabled kh-demo.service

Po drugie, stan. Rozstrzygający jest dopisek w nawiasie w wierszu Active:. active (running) znaczy, że proces działa. active (exited) znaczy, że program się zakończył i nikogo już nie ma. Dla usługi, która ma działać stale, to błąd, nawet jeśli obok świeci się na zielono.

systemctl status kh-demo.service --no-pager
systemctl is-active kh-demo.service

Po trzecie, journal. Nie tylko czy wiersze docierają, ale czy docierają właściwe.

journalctl -u kh-demo.service -n 20 --no-pager

Podgląd na żywo: journalctl -u kh-demo.service -f. Tylko ostatni rozruch: -b. Zakres czasu: --since "-1h". Więcej na ten temat w artykule journalctl: analiza logów pod systemd.

Po czwarte, konfiguracja obowiązująca. Nie plik, tylko to, co systemd z niego zrobił. Różnica ma znaczenie, gdy tylko w grę wchodzą pliki drop-in.

systemctl show kh-demo.service -p ExecStart -p User -p WorkingDirectory -p Restart -p MainPID
systemctl cat kh-demo.service

Ostatecznym dowodem pozostaje prawdziwy restart serwera. Po nim to polecenie pokaże, czy coś zostało po drodze:

systemctl list-units --type=service --state=failed --no-pager

Najczęstsze błędy w oryginalnym brzmieniu

systemd zgłasza błędy startu przez własne kody wyjścia powyżej 200. Sama liczba w status mówi już, gdzie szukać.

KodZnaczeniePrzyczyna
200/CHDIREXIT_CHDIRWorkingDirectory= nie istnieje albo użytkownik nie może do niego wejść
203/EXECEXIT_EXECExecStart= nie został znaleziony, nie jest wykonywalny albo ma zły interpreter
216/GROUPEXIT_GROUPGroup= nie istnieje
217/USEREXIT_USERUser= nie istnieje
219/CGROUPEXIT_CGROUPnie udało się utworzyć cgroup
238/STATE_DIRECTORYEXIT_STATE_DIRECTORYStateDirectory= już istnieje, ale z niewłaściwym właścicielem

203/EXEC: zła ścieżka w ExecStart

W journalu stoi wtedy wiersz w postaci kh-demo.service: Failed at step EXEC spawning /opt/kh-demo/run.sh: No such file or directory, a zaraz po nim Main process exited, code=exited, status=203/EXEC.

No such file or directory wprowadza w błąd, bo ten komunikat ma trzy przyczyny. Po pierwsze: plik naprawdę nie istnieje, zwykle przez literówkę albo dlatego, że plik binarny leży w /usr/local/bin zamiast w /usr/bin. Po drugie: brakuje bitu wykonywalności, wtedy journal zgłasza Permission denied. Po trzecie, przypadek najbardziej podstępny: plik jest wykonywalny, ale jego wiersz shebang prowadzi donikąd. Skrypt z #!/usr/bin/python zawodzi na Debianie 12 i nowszym dokładnie w ten sposób, bo istnieje tam tylko /usr/bin/python3. Kernel zgłasza brak interpretera, a systemd podaje to dalej jako brak skryptu.

Sprawdzenie zajmuje trzy sekundy:

ls -l /opt/kh-demo/run.sh
head -n 1 /opt/kh-demo/run.sh

Ścieżki względne i PATH

ExecStart=node server.js nie zadziała, nawet przy ustawionym WorkingDirectory=. Błąd przy wczytywaniu brzmi Neither a valid executable name nor an absolute path. Bezwzględny musi być tylko pierwszy element, argumenty za nim mogą pozostać względne. Poprawnie jest ExecStart=/usr/bin/node server.js, przy czym server.js zostaje rozwiązane względem WorkingDirectory. Właściwą ścieżkę poda command -v:

command -v bash

Uwaga na menedżery wersji: pod nvm polecenie zwróci ścieżkę w rodzaju /root/.nvm/versions/node/v22.14.0/bin/node, której dla użytkownika usługi w ogóle nie ma. Środowiska uruchomieniowe dla usług instaluje się systemowo, na przykład przez NodeSource albo Adoptium Temurin.

Brakujące zmienne środowiskowe

Klasyk: po zalogowaniu interaktywnym program działa, jako usługa już nie. Powód jest taki, że systemd nie uruchamia powłoki logowania. Nie są czytane ani /etc/profile, ani ~/.bashrc, ani ~/.profile, więc wszystkiego, co ustawiono tam przez export, w usłudze brakuje. PATH usługi systemowej to stała ścieżka minimalna, zwykle /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin.

Zmienne trafiają dlatego do jednostki albo do osobnego pliku:

cat > /etc/kh-demo.env <<'EOF'
GREETING=cześć
LANG=pl_PL.UTF-8
EOF
chmod 640 /etc/kh-demo.env
chown root:khdemo /etc/kh-demo.env
systemctl restart kh-demo.service
journalctl -u kh-demo.service -n 5 --no-pager

Jeśli w wyjściu widnieje teraz GREETING=cześć zamiast GREETING=nie ustawiono, plik dotarł na miejsce. Da się to sprawdzić także bezpośrednio:

systemctl show kh-demo.service -p Environment -p EnvironmentFiles

Trzy pułapki w takich plikach: podstawienie polecenia przez $(...) nie zachodzi, dosłownie rozumiany znak $ trzeba zapisać jako $$, a cudzysłowy lądują w wartości, gdzie nie mają czego szukać.

Unit not found

Failed to start kh-demo.service: Unit kh-demo.service not found. oznacza prawie zawsze jedną z trzech rzeczy: plik leży w złym katalogu, ma złe rozszerzenie (.services zamiast .service) albo brakuje daemon-reload. ls -l /etc/systemd/system/ wyjaśnia dwa pierwsze przypadki.

Różnice między Debianem 13, Debianem 12, Ubuntu 24.04 i 22.04

Podstawy, czyli [Unit], [Service], [Install], Type=simple, Restart=, User= oraz WorkingDirectory=, są na wszystkich czterech systemach identyczne. Różnice leżą na obrzeżach.

Dostępne dyrektywy. Ubuntu 22.04 przynosi systemd 249, Debian 12 systemd 252, Ubuntu 24.04 systemd 255, a Debian 13 systemd 257. Wszystkiego od wersji 253 wzwyż brakuje na obu starszych systemach: Type=notify-reload (253) oraz RestartSteps=, RestartMaxDelaySec=RestartMode=direct (254). Nie są kwitowane błędem, tylko ignorowane. Tak powstają jednostki, które na jednym systemie robią to, czego oczekujesz, a na drugim pozornie bez powodu coś zupełnie innego.

systemctl --version

Sieć. Ubuntu Server korzysta z netplana z systemd-networkd, Debian w instalacji standardowej z ifupdown. Dlatego network-online.target jest na Ubuntu miarodajny bez żadnych zabiegów, a na Debianie dopiero po włączeniu ifupdown-wait-online.service.

Ścieżki interpreterów i środowisk uruchomieniowych. Tu powstaje najwięcej błędów 203/EXEC przy przenoszeniu jednostki między systemami. Node.js występuje na Debianie 13 w wersji 20.19, na Debianie 12 w 18.20, na Ubuntu 24.04 w 18.19, a na Ubuntu 22.04 w 12.22. PHP to 8.4 na Debianie 13, 8.2 na Debianie 12, 8.3 na Ubuntu 24.04 oraz 8.1 na Ubuntu 22.04. Przy Javie skok jest największy: Debian 13 dostarcza tylko openjdk-21-jre-headless, Debian 12 tylko openjdk-17-jre-headless, a Ubuntu 24.04 i 22.04 za to 8, 11, 17 oraz 21. Kto skopiuje ExecStart=/usr/lib/jvm/java-17-openjdk-amd64/bin/java z Debiana 12 na Debiana 13, niezawodnie dostanie 203/EXEC.

Bazy danych. Debian nie dostarcza pakietu mysql-server, działa tam zawsze MariaDB. Jednostka z After=mysql.service czeka więc na Debianie na usługę, której nie ma, i startuje bez żadnego opóźnienia, po cichu i bez ostrzeżenia. Poprawnie jest tam After=mariadb.service.

Zmiana, wycofanie, sprzątanie

Własne jednostki zmienia się wprost w pliku, plus daemon-reload. Dla jednostek pochodzących z pakietu bierze się natomiast pliki drop-in, żeby najbliższa aktualizacja nie sprzątnęła twojej zmiany:

systemctl edit kh-demo.service

To zakłada plik /etc/systemd/system/kh-demo.service.d/override.conf, w którym stoją wyłącznie zmienione dyrektywy. Przypadek szczególny: dyrektywy listowe, takie jak ExecStart=, nie są zastępowane, tylko uzupełniane. Kto chce je nadpisać, musi najpierw wyczyścić listę:

ExecStart=
ExecStart=/opt/kh-demo/run.sh --verbose

systemctl revert kh-demo.service usuwa wszystkie pliki drop-in. Pełne wycofanie przykładu, dokładnie w tej kolejności:

systemctl disable --now kh-demo.service
rm -f /etc/systemd/system/kh-demo.service
rm -rf /etc/systemd/system/kh-demo.service.d
systemctl daemon-reload
systemctl reset-failed

Uwaga przy ostatnim kroku: systemctl reset-failed bez argumentu zeruje stan błędu wszystkich jednostek, nie tylko usługi z przykładu. Na systemie, na którym w stanie failed stoją jeszcze inne usługi, znikną w ten sposób również ich komunikaty z systemctl list-units --state=failed. Kto chce posprzątać wyłącznie tutaj, bierze formę celowaną systemctl reset-failed kh-demo.service z sekcji powyżej.

Kto usunie plik bez wcześniejszego disable, zostawi po sobie martwe dowiązanie symboliczne w /etc/systemd/system/multi-user.target.wants/, które przy każdym daemon-reload wypływa jako ostrzeżenie. Sprząta się je poleceniem find /etc/systemd/system -xtype l -delete. To wywołanie usuwa jednak każde martwe dowiązanie poniżej /etc/systemd/system, a więc także pozostałości po obcych usługach, które być może jeszcze się przydadzą. Bezpieczniej jest najpierw zerknąć bez -delete albo od razu sięgnąć po wariant celowany find /etc/systemd/system -xtype l -name 'kh-demo*' -delete. Opcjonalnie jeszcze użytkownik i pliki:

userdel khdemo
rm -rf /opt/kh-demo /etc/kh-demo.env

Bezwzględna ścieżka w ExecStart, właściwy Type, własny użytkownik, ustawione WorkingDirectory, zmienne przez EnvironmentFile=: kto ma to wszystko razem, ma usługę, która przetrwa restart, a w razie awarii zdradzi, co się stało.

Najczęstsze pytania

Czy po każdej zmianie w pliku jednostki muszę wykonać systemctl daemon-reload?
Tak. systemd trzyma pliki jednostek w pamięci, więc zmiana na dysku zaczyna działać dopiero po przeładowaniu. Kto o nim zapomni, zobaczy przy najbliższym systemctl status ostrzeżenie „The unit file, source configuration file or drop-ins of ... changed on disk”. Kolejność jest zawsze taka sama: najpierw systemctl daemon-reload, potem systemctl restart. Samo przeładowanie niczego nie restartuje i nie zmienia nic w działających procesach.
Co oznacza status=203/EXEC?
systemd nie mógł uruchomić programu podanego w ExecStart. W grę wchodzą trzy przyczyny: plik nie istnieje (literówka albo zły katalog), brakuje bitu wykonywalności, albo wiersz shebang skryptu wskazuje na interpreter, którego nie ma, na przykład #!/usr/bin/python na Debianie 12 i nowszym, gdzie istnieje wyłącznie /usr/bin/python3. Sprawdzisz to poleceniem ls -l na pliku oraz head -n 1 na jego pierwszym wierszu.
Dlaczego moja usługa nie startuje po restarcie, choć systemctl start działa?
Prawie zawsze brakuje sekcji [Install] z WantedBy=multi-user.target albo systemctl enable nigdy nie zostało wykonane. Dowodem jest dowiązanie symboliczne: ls -l /etc/systemd/system/multi-user.target.wants/nazwa.service musi pokazać plik, a systemctl is-enabled musi wypisać „enabled”. Gdy tej sekcji brakuje, enable przerywa działanie komunikatem „The unit files have no installation config”.
Type=simple czy Type=forking?
Type=simple, gdy program pozostaje na pierwszym planie, co dotyczy praktycznie wszystkich nowoczesnych aplikacji i jest wartością domyślną. Type=forking tylko wtedy, gdy program koniecznie sam przechodzi w tło, wtedy dodatkowo z PIDFile=. Większość demonów ma przełącznik w rodzaju --foreground albo --no-daemon, a z nim lepszym wyborem jest Type=simple. Błędne połączenie objawia się jako „inactive (dead)” zaraz po starcie albo jako timeout po 90 sekundach.
Dlaczego moja usługa nie znajduje swoich zmiennych środowiskowych?
systemd nie uruchamia powłoki logowania. Nie są czytane ani /etc/profile, ani ~/.bashrc, ani ~/.profile, więc wszystkiego, co ustawiono tam przez export, po prostu brakuje. PATH to stała ścieżka minimalna. Zmienne należą dlatego do jednostki jako Environment= albo do pliku wczytywanego przez EnvironmentFile=. Stoją w nim czyste wiersze KEY=VALUE bez export, a podstawienie polecenia nie zachodzi. Kontrola: systemctl show nazwa.service -p Environment.
Usługa nie reaguje już na systemctl start, co teraz?
Prawdopodobnie zadziałał hamulec startowy: domyślnie dozwolonych jest pięć prób startu w ciągu dziesięciu sekund, potem journal zgłasza „Start request repeated too quickly”, a usługa zostaje w stanie failed. Licznik zeruje systemctl reset-failed nazwa.service. Uwaga: ręczne wywołania systemctl restart też się liczą, więc kto podczas debugowania restartuje usługę raz za razem, sam zaciąga ten hamulec.

systemd Linux Debian Ubuntu Administracja serwerem Autostart journalctl Plik jednostki Serwer root