Skip to content
Commitpedia
Log in Join & edit

Write a systemd service unit for your own app

Turn any long-running program into a proper service: create a unit file, run it as an unprivileged user, restart it on failure and start it at boot.

Drafted with AI from official documentation. Scripts were run in a test environment and repository links verified. Awaiting a human check. Did it work for you? Last edited Outdated?

Edit history of Write a systemd service unit for your own app: 1 recorded edit
Edit history · 1 edit
Type
Article
Edits
1
Editors
1
First issued
15 Oct 2026
Last edit
15 Oct 2026
Reading
3 min

If you start an app with nohup ./myapp & or inside screen, it will not come back after a reboot or a crash, and its output goes nowhere useful. A small systemd unit file fixes all three problems. This guide uses an app at /opt/myapp/bin/myapp that listens on port 8080; replace names and paths with your own.

1. Create a user for the app

Services should not run as root unless they really need to. Create a system account with no login shell:

Check before you run this

  • Runs with administrator rights. Make sure you understand every line.
# Debian/Ubuntu and RHEL/Rocky/Alma
sudo useradd --system --home-dir /opt/myapp --shell /usr/sbin/nologin appuser
sudo chown -R appuser:appuser /opt/myapp

2. Write the unit file

Your own units go in /etc/systemd/system/. The file name becomes the service name.

Check before you run this

  • Runs with administrator rights. Make sure you understand every line.
sudo nano /etc/systemd/system/myapp.service
[Unit]
Description=My example app
After=network-online.target
Wants=network-online.target

[Service]
Type=exec
User=appuser
Group=appuser
WorkingDirectory=/opt/myapp
EnvironmentFile=-/etc/myapp/myapp.env
ExecStart=/opt/myapp/bin/myapp --port 8080
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

What each line does:

  • After= / Wants=network-online.target: start after the network is configured, useful when the app connects to a database on another host at startup.
  • Type=exec: the program stays in the foreground and is the service. The default when only ExecStart= is set is simple; the systemd manual recommends exec for long-running services because it reports a failure if the binary cannot be started at all. Only use forking for old daemons that put themselves into the background.
  • User= / Group=: run as the account from step 1.
  • EnvironmentFile=: read KEY=value lines from a file. The leading - makes the file optional. Keep secrets here with chmod 600, not in the unit file.
  • ExecStart=: the full path to the program and its arguments. systemd does not run a shell, so pipes and && do not work here.
  • Restart=on-failure with RestartSec=5: restart after a crash or non-zero exit, waiting 5 seconds. The default is Restart=no.
  • WantedBy=multi-user.target: what systemctl enable hooks into so the app starts at boot.

3. Load, start and enable it

Check before you run this

  • Runs with administrator rights. Make sure you understand every line.
sudo systemd-analyze verify /etc/systemd/system/myapp.service   # catches typos
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
systemctl status myapp

If the status shows active (running), you are done. If it shows failed, read the logs:

journalctl -u myapp -n 50 --no-pager

Common first-run problems: a wrong path in ExecStart= (use an absolute path), the app user cannot read its config or write its data directory, or the app forks into the background by itself (then it needs a foreground flag, or Type=forking).

4. Test the restart behavior

Check before you run this

  • Runs with administrator rights. Make sure you understand every line.
sudo systemctl kill --signal=SIGKILL myapp
sleep 6
systemctl status myapp     # should be running again with a new PID

5. Optional: sandbox the service

systemd can restrict what the service may touch. Add these lines to [Service] one at a time and test after each, because a too-strict setting shows up as “permission denied” errors in the app:

NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
StateDirectory=myapp
ReadWritePaths=/opt/myapp/data
  • ProtectSystem=strict makes the whole filesystem read-only for the service, except /dev, /proc and /sys; ReadWritePaths= re-opens the paths it must write.
  • StateDirectory=myapp creates /var/lib/myapp owned by the service user.
  • ProtectHome=yes hides /home, /root and /run/user.

systemd-analyze security myapp rates the unit’s sandboxing and lists settings you could still add.

Changing it later

After editing the unit file, always run sudo systemctl daemon-reload and then sudo systemctl restart myapp. To adjust a unit that came from a package instead, use sudo systemctl edit name, which writes only your changes to a drop-in file.

Official documentation

Did this work for you?

No votes yet. Be the first.

If it did not work, the report form below tells the editors what went wrong.

See also

Repos in the wild

History

Every saved edit is kept. The plot on the sleeve draws one line per edit. Open any edit to see exactly what changed.

  1. Dominik +3,955 chars

Report a problem

Something wrong, unsafe or copied? Tell the editors.
What is wrong?