Aplikacja uruchomiona w terminalu zatrzymuje się, gdy tylko zamkniesz SSH lub zrestartujesz VPS. Skonfigurujemy ją jako usługę systemd: działa w tle, sama restartuje się po awarii, startuje przy uruchomieniu VPS, a jej wyjście trafia do dziennika systemowego.
Czego potrzebujesz
- VPS w StormHost z Ubuntu 24.04 lub Debianem 13
- Połączenie SSH jako root (poradnik)
- Aplikacja, którą możesz uruchomić z wiersza poleceń (na przykład bot Discord w Node.js)
- Około 20 minut
Dlaczego systemd#
systemd to menedżer usług dostarczany zarówno z Ubuntu, jak i z Debianem. Uruchamia wszystkie usługi systemowe (SSH, Nginx, bazy danych), więc nie trzeba niczego dodatkowo instalować.
| Metoda | Restart po awarii | Start po restarcie VPS | Logi | Uwaga |
|---|---|---|---|---|
Terminal, nohup, & | Nie | Nie | Tylko do pliku | Dobre tylko do szybkiego testu. |
screen, tmux | Nie | Nie | Tylko w oknie | Wygodne do pracy ręcznej, nie do produkcji. |
| pm2 | Tak | Tak (po skonfigurowaniu) | Własne | Kolejny program, tylko Node.js. |
| systemd | Tak | Tak | dziennik | Część systemu, działa z dowolnym językiem. |
Użytkownik dla aplikacji#
Aplikacja nie powinna działać jako root. Jeśli ktoś ją przejmie, dostanie tylko uprawnienia jej użytkownika. Utwórz użytkownika systemowego z katalogiem domowym w /opt:
useradd --system --create-home --home-dir /opt/mujbot --shell /usr/sbin/nologin mujbot--system– konto systemowe dla usługi (niższe ID użytkownika, bez hasła).--create-home --home-dir /opt/mujbot– tworzy katalog/opt/mujbotnależący do użytkownika.--shell /usr/sbin/nologin– nikt nie może zalogować się jako ten użytkownik przez SSH ani w terminalu.mujbot– nazwa użytkownika (i pasującej grupy). Wybierz własną i zamień ją wszędzie w tym poradniku.
Node.js LTS#
Repozytorium Ubuntu 24.04 dostarcza Node.js 18, a Debian 13 Node.js 20. Żadna z tych wersji nie jest już wspierana przez projekt Node.js (18 od 30 kwietnia 2025, 20 od 30 kwietnia 2026). Zainstaluj aktualne wydanie LTS z oficjalnych pakietów na nodejs.org (otwiera się w nowym oknie). W październiku 2026 aktywnym LTS jest Node.js 24 (pod koniec października 2026 LTS zostaje Node.js 26).
Najpierw pobierz archiwum i je zweryfikuj:
apt -y install curl xz-utils
cd /tmp
NODE_SOUBOR=$(curl -fsSL https://nodejs.org/dist/latest-v24.x/SHASUMS256.txt | grep -o 'node-v[0-9.]*-linux-x64.tar.xz' | head -n 1)
curl -fsSLO "https://nodejs.org/dist/latest-v24.x/$NODE_SOUBOR"
curl -fsSL https://nodejs.org/dist/latest-v24.x/SHASUMS256.txt | grep " $NODE_SOUBOR\$" | sha256sum -c -node-v24.21.0-linux-x64.tar.xz: OKKontynuuj tylko wtedy, gdy widzisz OK. Jeśli dostaniesz FAILED, usuń plik (rm /tmp/node-v*.tar.xz) i pobierz go ponownie. Następnie zainstaluj Node.js (w tym samym oknie terminala, zmienna NODE_SOUBOR musi być nadal ustawiona):
mkdir -p /opt/nodejs
tar -xJf "$NODE_SOUBOR" -C /opt/nodejs --strip-components=1
ln -sf /opt/nodejs/bin/node /usr/local/bin/node
ln -sf /opt/nodejs/bin/npm /usr/local/bin/npm
ln -sf /opt/nodejs/bin/npx /usr/local/bin/npx| Polecenie | Co robi |
|---|---|
apt -y install curl xz-utils | Narzędzie do pobierania i obsługa rozpakowywania archiwów .tar.xz. |
NODE_SOUBOR=$(… SHASUMS256.txt | grep -o … | head -n 1) | Czyta listę sum kontrolnych najnowszego wydania 24.x i znajduje nazwę pliku Linux x64, na przykład node-v24.. |
curl -fsSLO "…/$NODE_SOUBOR" | Pobiera archiwum; -O zapisuje je pod oryginalną nazwą. |
… | grep " $NODE_SOUBOR\$" | sha256sum -c - | Sprawdza sumę kontrolną pobranego pliku względem oficjalnej listy. Chroni przed uszkodzonym lub podmienionym plikiem. |
mkdir -p /opt/nodejs | Katalog dla Node.js. |
tar -xJf … -C /opt/nodejs --strip-components=1 | Rozpakowuje archiwum (J = format xz) do /opt/nodejs bez katalogu najwyższego poziomu nazwanego od wersji. |
ln -sf /opt/nodejs/bin/… /usr/local/bin/… | Tworzy dowiązania do node, npm i npx w katalogu, w którym system szuka programów. Ścieżka /usr/local/bin/node pozostaje taka sama po aktualizacjach. |
Kontrola: node -v wypisuje v24.…, npm -v wypisuje wersję npm.
Dla innej linii wydań zamień latest-v24.x we wszystkich URL na przykład na latest-v26.x. Aktualizacja Node.js = zatrzymanie usługi, usunięcie /opt/nodejs i powtórzenie kroków.
Dlaczego nie nvm
nvm (oferowany na stronie pobierania nodejs.org) instaluje Node.js w katalogu domowym użytkownika pod ścieżką zawierającą numer wersji. Usługa systemd potrzebuje stałej ścieżki bezwzględnej do programu, która nie zmienia się po aktualizacji. Dlatego używamy oficjalnego archiwum z dowiązaniem w /usr/local/bin.
Aplikacja#
Prześlij kod aplikacji do
/opt/mujbot/app(przez SFTP, poradnik, albo przezgit clone). Nie przesyłaj pliku.envz tokenem ani katalogunode_modules.Przekaż pliki użytkownikowi aplikacji:
bash chown -R mujbot:mujbot /opt/mujbotZainstaluj pakiety jako użytkownik aplikacji:
bash cd /opt/mujbot/app runuser -u mujbot -- npm ci --omit=devrunuser -u mujbot --– wykonuje polecenie jako użytkownikmujbot(działa nawet bez pakietu sudo).npm ci– instaluje dokładnie wersje zpackage-lock.json. Jeśli projekt go nie ma, użyjnpm install --omit=dev.--omit=dev– pomija pakiety potrzebne tylko do rozwoju.
Wypróbuj aplikację ręcznie:
runuser -u mujbot -- node index.js. Jeśli startuje bez błędów, zatrzymaj ją przez Ctrl+C. (Jeśli potrzebuje tokenu, zrób test ręczny po sekcji Dane poufne.)
Dane poufne w EnvironmentFile#
Tokeny i hasła nie należą do kodu ani do pliku unit. Przechowuj je w pliku zmiennych, który może czytać tylko root, a systemd przekaże je aplikacji przy starcie.
Utwórz plik przez
nano /etc/mujbot.env:/etc/ mujbot.env iniDISCORD_TOKEN=<TOKEN_BOTA> NODE_ENV=production- Każda linia to
NAZWA=wartość, bezexporti bez spacji wokół=. - Linie zaczynające się od
#to komentarze. - W aplikacji odczytujesz wartość jako
process..env. DISCORD_ TOKEN
- Każda linia to
Ogranicz uprawnienia:
bash chown root:root /etc/mujbot.env chmod 600 /etc/mujbot.envchmod 600– plik może czytać i zmieniać tylko root. systemd czyta go przed przełączeniem na użytkownika aplikacji, więc zmienne docierają do aplikacji, ale nikt inny nie może czytać pliku.
Plik unit usługi#
Utwórz plik przez nano /etc/:
[Unit]
Description=Moj bot Discord
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=mujbot
Group=mujbot
WorkingDirectory=/opt/mujbot/app
EnvironmentFile=/etc/mujbot.env
ExecStart=/usr/local/bin/node /opt/mujbot/app/index.js
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target| Dyrektywa | Co robi | Zalecana wartość | Kiedy zmieniać |
|---|---|---|---|
Description= | Opis usługi pokazywany na listach. | Nazwa aplikacji | – |
After= | Usługa startuje dopiero po uruchomieniu sieci. | network-online.target | – |
Wants= | Prosi system o poczekanie na sieć (samo After nie wystarcza). | network-online.target | – |
Type=simple | Proces z ExecStart jest głównym procesem usługi i działa na pierwszym planie. | simple | Tylko jeśli aplikacja sama przechodzi w tło (forking). |
User=, Group= | Jako który użytkownik działa aplikacja. | użytkownik aplikacji | Nigdy root. |
WorkingDirectory= | Katalog roboczy; ścieżki względne w aplikacji są liczone od niego. | katalog aplikacji | Gdy przenosisz aplikację. |
EnvironmentFile= | Plik ze zmiennymi środowiskowymi. Jeśli nie istnieje, usługa nie wystartuje (myślnik przed ścieżką, -/etc/…, czyni go opcjonalnym). | /etc/mujbot.env | – |
ExecStart= | Polecenie startowe. Program musi być podany ścieżką bezwzględną. | /usr/local/bin/node /opt/…/index.js | Zależnie od pliku głównego aplikacji. |
Restart=on-failure | Restartuje, gdy proces kończy się kodem niezerowym albo zostaje zabity sygnałem (poza SIGTERM, SIGINT, SIGHUP i SIGPIPE, które systemd traktuje jako czyste zakończenie). Zakończenie kodem 0 nie wywołuje restartu. | on-failure | always, jeśli ma startować ponownie nawet po czystym zakończeniu. |
RestartSec=5 | Czeka 5 sekund przed restartem (domyślnie 100 ms). | 5 | Dłużej, jeśli zawieszająca się aplikacja zalewa zewnętrzne API. |
NoNewPrivileges=true | Ani aplikacja, ani jej procesy potomne nie mogą zdobyć wyższych uprawnień (na przykład przez programy setuid). | true | – |
WantedBy= | Po enable usługa startuje przy uruchomieniu VPS. | multi-user.target | – |
Limit częstotliwości startów
Domyślnie systemd pozwala na najwyżej 5 startów usługi w ciągu 10 sekund. Przy RestartSec=5 ten limit nigdy nie zostaje osiągnięty, więc zawieszająca się usługa będzie próbować startu co 5 sekund w nieskończoność. Jeśli ma się poddać po serii awarii, dodaj do sekcji [Unit] na przykład StartLimitIntervalSec= i StartLimitBurst=10 (najwyżej 10 startów w 5 minut).
Uruchomienie usługi#
systemctl daemon-reload
systemctl enable --now mujbot
systemctl status mujbotsystemctl daemon-reload– wczytuje nowe i zmienione pliki unit. Uruchamiaj po każdej zmianie pliku.service.systemctl enable --now mujbot– włącza start przy uruchomieniu VPS i od razu uruchamia usługę.systemctl status mujbot– status usługi i kilka ostatnich linii logu.
W wyjściu status szukaj:
● mujbot.service - Moj bot Discord
Loaded: loaded (/etc/systemd/system/mujbot.service; enabled; preset: enabled)
Active: active (running) since …
Main PID: 12345 (node)Loaded: … enabled– usługa wystartuje po restarcie VPS.Active: active (running)– działa.failedoznacza awarię;activating (auto-restart)oznacza oczekiwanie na restart.Main PID– ID procesu aplikacji.
Inne polecenia: systemctl restart mujbot (restart), systemctl stop mujbot (zatrzymanie), systemctl disable mujbot (nie startuj przy uruchomieniu).
Logi#
Wszystko, co aplikacja wypisuje (console.log, print, błędy), systemd zapisuje w dzienniku.
| Polecenie | Co pokazuje |
|---|---|
journalctl -u mujbot -f | Wyjście na żywo jak w konsoli; zatrzymaj przez Ctrl+C. |
journalctl -u mujbot -n 100 | Ostatnie 100 linii. |
journalctl -u mujbot --since "1 hour ago" | Wyjście z ostatniej godziny. |
journalctl -u mujbot -b | Wyjście od ostatniego startu VPS. |
journalctl --disk-usage | Ile miejsca zajmuje dziennik. |
Dziennik sprząta się sam: domyślnie zajmuje najwyżej 10 % rozmiaru systemu plików, do 4 GB.
Python i Java#
Kroki są takie same; różni się tylko instalacja środowiska uruchomieniowego i linia ExecStart.
Python
Python 3 jest już w systemie. Utwórz dla aplikacji środowisko wirtualne, aby jej pakiety nie mieszały się z systemowymi:
apt -y install python3-venv
runuser -u mujbot -- python3 -m venv /opt/mujbot/venv
runuser -u mujbot -- /opt/mujbot/venv/bin/pip install -r /opt/mujbot/app/requirements.txtpython3 -m venv /opt/mujbot/venv– tworzy osobne środowisko Pythona w kataloguvenv./opt/mujbot/venv/bin/pip install -r …– instaluje pakiety zrequirements.txtdo tego środowiska.
W pliku unit użyj Pythona ze środowiska i wyłącz buforowanie wyjścia (inaczej logi pojawiają się w dzienniku z opóźnieniem):
Environment=PYTHONUNBUFFERED=1
ExecStart=/opt/mujbot/venv/bin/python /opt/mujbot/app/bot.pyJava
Zainstaluj środowisko Javy w wersji wymaganej przez aplikację. Dla Javy 21:
apt -y install openjdk-21-jre-headlessDla Javy 25 zainstaluj zamiast tego pakiet openjdk-25-jre-headless. Oba są w repozytoriach Ubuntu 24.04 i Debiana 13. Wersję sprawdź przez java -version.
W pliku unit:
ExecStart=/usr/bin/java -Xms512M -Xmx1G -jar /opt/mujbot/app/aplikace.jar
SuccessExitStatus=143-Xms512M -Xmx1G– początkowa i maksymalna pamięć Javy. Maksimum ustaw wyraźnie poniżej wolnej RAM VPS.SuccessExitStatus=143– przy czystym zatrzymaniu (sygnał SIGTERM) Java kończy się kodem 143. Bez tej linii systemd zgłaszałby usługę jakofailedpo każdymsystemctl stop.
Aktualizacja aplikacji#
systemctl stop mujbot
cd /opt/mujbot/app
runuser -u mujbot -- git pull
runuser -u mujbot -- npm ci --omit=dev
systemctl start mujbotgit pull– pobiera nową wersję kodu (jeśli aplikacja jest w Git; w przeciwnym razie prześlij pliki przez SFTP i uruchomchown -R mujbot:mujbot /opt/mujbot).npm ci --omit=dev– instaluje zmienione pakiety.
Jeśli zmieniły się tylko pliki kodu, wystarczy systemctl restart mujbot.
Weryfikacja#
systemctl is-enabled mujbotwypisujeenabled,systemctl is-active mujbotwypisujeactive.- Po restarcie VPS (
reboot) aplikacja działa bez Twojego udziału. - Test automatycznego restartu:
systemctl kill -s SIGKILL mujbot(zabija proces siłą, jakby się zawiesił). W ciągu 5 sekundsystemctl status mujbotznów pokazujeactive (running)z innymMain PID. Samosystemctl killbez-s SIGKILLwysyła SIGTERM, który systemd traktuje jako czyste zakończenie, więc przyRestart=on-failureusługa nie wystartuje ponownie.
Rozwiązywanie problemów#
status=203/EXEC#
systemd nie mógł uruchomić programu z ExecStart. Ścieżka programu nie istnieje, nie jest bezwzględna albo plik nie jest wykonywalny. Sprawdź ścieżkę (ls -l /usr/local/bin/node), a przy skryptach pierwszą linię (#!/usr/bin/env …) i uprawnienie do wykonywania.
status=217/USER#
Użytkownik z User= nie istnieje. Sprawdź nazwę (id mujbot) albo utwórz użytkownika zgodnie z sekcją Użytkownik dla aplikacji.
status=200/CHDIR#
Katalog z WorkingDirectory= nie istnieje albo użytkownik nie ma do niego dostępu. Sprawdź ścieżkę i właściciela (ls -ld /opt/mujbot/app).
Failed to load environment files: No such file or directory#
Plik z EnvironmentFile= nie istnieje. Sprawdź ścieżkę albo postaw przed nią -, jeśli plik jest opcjonalny.
Start request repeated too quickly. (start-limit-hit)#
Usługa zawiesiła się zbyt wiele razy z rzędu i systemd przestał ją uruchamiać. Przyczynę znajdziesz w logu (journalctl -u mujbot -n 50). Po naprawie wyzeruj licznik i uruchom usługę:
systemctl reset-failed mujbot
systemctl start mujbotWarning: The unit file … changed on disk. Run 'systemctl daemon-reload' to reload units.#
Edytowano plik unit bez uruchomienia systemctl daemon-reload. Uruchom je i zrestartuj usługę.
Aplikacja zgłasza EACCES lub permission denied#
Użytkownik aplikacji nie może czytać lub zapisywać plików. Zwykle dzieje się to po przesłaniu plików jako root. Popraw właściciela: chown -R mujbot:mujbot /opt/mujbot. Zwykły użytkownik nie może otworzyć portu poniżej 1024 (na przykład 80); niech aplikacja nasłuchuje na wyższym porcie, a przed nią postaw Nginx (poradnik).
Aplikacja nie widzi zmiennej z .env#
systemd sam nie wczytuje pliku .env z katalogu aplikacji. Umieść zmienne w pliku z EnvironmentFile= albo wczytaj je w aplikacji biblioteką (na przykład dotenv). Po zmianie pliku zmiennych zrestartuj usługę.
Podsumowanie
- Aplikacja działa jako własny użytkownik, nie jako root.
- Plik unit w
/etc/systemd/system/ustawia użytkownika, katalog, polecenie ze ścieżką bezwzględną i restart po awarii. - Dane poufne są w pliku z
EnvironmentFile=z uprawnieniami600. - Po edycji pliku unit:
systemctl daemon-reload; włączenie i start:systemctl enable --now. - Logi:
journalctl -u <NAZWA_USLUGI> -f.
Dalsza lektura#
- systemd.service (man) (otwiera się w nowym oknie)
- systemd.exec (man) (otwiera się w nowym oknie)
- systemd.unit (man) (otwiera się w nowym oknie)
- journalctl (man) (otwiera się w nowym oknie)
- Node.js: pobieranie i wydania (otwiera się w nowym oknie)
- Node.js: harmonogram wydań (otwiera się w nowym oknie)