Overview

systemd ships on every Ubuntu and Debian box that Hostinger provisions. It supervises processes, restarts them on failure, logs to the journal, and starts them at boot. Use it instead of pm2, supervisor, or screen. This page covers writing a unit file, setting up environment files, operating the service, and reading logs. See hostinger-vps-hardening for the prerequisites that should be done before any app is deployed.

Write the service unit file

Place unit files in /etc/systemd/system/. The filename determines the service name.

# /etc/systemd/system/app.service
[Unit]
Description=My App
Documentation=https://github.com/yourorg/yourapp
After=network.target
 
[Service]
Type=simple
User=deploy
Group=deploy
WorkingDirectory=/srv/app
EnvironmentFile=/etc/app/env
ExecStart=/usr/bin/node /srv/app/server.js
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=app
 
[Install]
WantedBy=multi-user.target

After writing or editing the file, reload the daemon before starting the service:

systemctl daemon-reload
systemctl enable --now app

enable --now activates the service for current boot and registers it for future boots in one command.

Configure restart policy to match the failure mode

Restart=on-failure restarts only on non-zero exit codes. Use it for apps that may exit cleanly when signaled.

Restart=always restarts on any exit, including clean exits and signals. Use it for services that should never stop, such as a queue worker.

RestartSec=5 waits five seconds before restarting. Pair it with a StartLimitIntervalSec and StartLimitBurst to avoid tight crash loops:

[Service]
Restart=on-failure
RestartSec=5
StartLimitIntervalSec=60
StartLimitBurst=5

This allows five restarts in sixty seconds before systemd gives up. Without limits, a misconfigured app burns CPU in a tight restart loop. After StartLimitBurst failures, use systemctl reset-failed app to clear the counter and try again after fixing the root cause.

Store secrets in EnvironmentFile, not in the unit

Hard-coding secrets in the unit file exposes them in systemctl show output and in git history if the file is tracked.

Create the env file with restrictive permissions:

mkdir -p /etc/app
touch /etc/app/env
chmod 600 /etc/app/env
chown root:root /etc/app/env

Populate it with KEY=value pairs, one per line:

DATABASE_URL=postgres://user:pass@localhost/mydb
SECRET_KEY=replace_with_random_64_chars
REDIS_URL=redis://localhost:6379

Reference it in the unit with EnvironmentFile=/etc/app/env. systemd injects these into the process environment before exec. The deploy user does not need read access; the service runs as deploy but the env file is owned by root. If deploy must write to it during deploy, adjust ownership to deploy:deploy and chmod 600.

Manage the service lifecycle

Common commands for the deploy workflow:

# Start, stop, restart
systemctl start app
systemctl stop app
systemctl restart app
 
# Reload without full restart (if the app supports SIGHUP)
systemctl reload app
 
# Check current state
systemctl status app
 
# Enable/disable at boot
systemctl enable app
systemctl disable app
 
# After editing the unit file
systemctl daemon-reload && systemctl restart app

Keep a deploy script that does git fetch --tags && git checkout <tag> && systemctl restart app rather than manual steps. Tags make rollback deterministic.

Read logs with journalctl

All StandardOutput and StandardError from the service flow to the systemd journal. Query with journalctl:

# Follow live output
journalctl -u app -f
 
# Last 100 lines
journalctl -u app -n 100
 
# Since a specific time
journalctl -u app --since "2026-05-14 00:00" --until "2026-05-14 06:00"
 
# Filter by priority (err and above)
journalctl -u app -p err
 
# Output as JSON for log shipping
journalctl -u app -o json | head -5

Persist the journal across reboots by setting Storage=persistent in /etc/systemd/journald.conf. By default, Debian and Ubuntu store the journal in memory only and drop it on reboot.