Illustration of a systemd service starting an app on a Linux server with restarts and sandboxing
Linux

Create a systemd Service to Run Your App on Boot

You wrote an app, logged into your VPS over SSH, typed node server.js, and it worked. Then you closed the laptop, the SSH session ended, and the app died with it. Maybe you got smarter and started it inside screen or with nohup, and it ran for three weeks, right up to the first kernel update and reboot. After that nobody noticed it was gone until a customer called. There is a better way, and it is already installed on your server: systemd. In this tutorial we create a systemd service that starts your app at boot, restarts it when it crashes, runs it as an unprivileged user, keeps secrets in a separate file, locks it into a sandbox and sends every log line to a place you can actually search.

We use a tiny Node.js app as the running example, then show exactly what changes for Python and Go. Everything was tested for Debian 13 and Ubuntu 24.04 on a fresh VPS, and where the two distributions behave differently, we say so.

What systemd is and why your app should use it

When Linux boots, the kernel starts exactly one program by itself: process number 1, or PID 1. On Debian and Ubuntu that program is systemd. Everything else (SSH, cron, the web server, the database) is started by systemd, watched by systemd and stopped by systemd at shutdown. It is a “service manager”: a supervisor whose job is to keep long-running programs, called services or daemons, in the state you asked for.

A few terms we will use all the time:

  • Unit: anything systemd manages. A service is one kind of unit; timers, mounts, sockets and targets are others.
  • Unit file: a small text file in INI format (sections in square brackets, Key=value lines) describing a unit. Ours will be called myapp.service.
  • Target: a named group of units that marks a stage of the boot. multi-user.target means “the system is up, networking and services are running, users can log in”. Hooking our service into it is what makes it start on boot.
  • Journal: systemd’s central log store, kept by the systemd-journald service and read with the journalctl command.

Why not screen, nohup or a process manager?

screen, tmux and nohup keep a process alive after you log out, and that is all they do. They do not start it again after a reboot, do not restart it after a crash, and leave logs scattered in random files. Language-specific process managers such as PM2 solve more of this, but they are one more daemon to install, update and start on boot, and they only cover one language. systemd is already running on every Debian and Ubuntu server, works the same way for Node.js, Python, Go, Java or a shell script, and offers sandboxing features no process manager has. If you already ship your app as a Docker container, Docker’s restart policies cover similar ground; for a single binary or a script, a systemd service is the lighter path.

What you need

  • A VPS running Debian 13 or Ubuntu 24.04. A KVM VPS supports everything in this guide; in an LXC container a few sandboxing options may need adjusting (we flag them in Step 8).
  • SSH access as a regular user with sudo rights, not as root.
  • An app that runs in the foreground and listens on a port, or our 10-line example.
  • A text editor; we use nano.
  • About 20 minutes.

How the pieces fit together

How systemd runs your appBoot orsystemctl startsystemdPID 1, supervisormyapp.serviceUser=myappRestart=on-failureProtectSystem=strict/etc/myapp/myapp.envvariables and secretsSandboxnode /opt/myapp/server.jsruns as user myapp/usr /etcread-only/var/lib/myappwritableprivate /tmp, no new privilegesno capabilities, filtered syscallscrash: restart after RestartSec=5stdout, stderrjournaldstores the logsjournalctl-u myapp -fmanaged by systemdsandbox boundaryThe app listens on 127.0.0.1:3000; a reverse proxy can sit in front.
systemd reads the unit file and the environment file, starts the app as the myapp user inside a sandbox, restarts it after a crash and collects everything it prints in the journal.

At boot, or whenever you run systemctl start, systemd reads two files we are about to create: the unit file, which says how to run the app, and the environment file, which holds configuration and secrets. It then starts the app as a dedicated user inside a sandbox where most of the filesystem is read-only. Anything the app prints on standard output or standard error goes straight into the journal, so there are no log files for you to rotate. If the process crashes, systemd waits a few seconds and starts it again.

The app only listens on 127.0.0.1 (the loopback interface, reachable only from the server itself), so it is not exposed to the internet. If it needs to be public, a reverse proxy such as Nginx with a TLS certificate goes in front of it.

Create a systemd service, step by step

Create a systemd service in 8 steps1Test the apprun it by hand2Create a usersystem account3Install to /optroot-owned code4Env file/etc/myapp5Unit filemyapp.service6Enable + starttest restarts7Read the logsjournalctl8Sandboxingsecurity score
The eight steps of this tutorial, from Step 1 (test the app by hand) to Step 8 (sandboxing and the security score). Each number matches a heading below.

Step 1: Write and test a small app

First we need Node.js. The distribution packages are enough for this example:

sudo apt update
sudo apt install -y nodejs curl
node --version
command -v node

Debian 13 ships Node.js 20 and Ubuntu 24.04 ships Node.js 18 (already past its end of life). Both run our example fine, but for a real app use a current LTS release, 22 or newer, from the official nodejs.org downloads, and check the Node.js releases page for the newest one. The last command prints where the node binary lives, usually /usr/bin/node. Write that path down: systemd needs absolute paths.

Now create a tiny web app. It has no dependencies and reads its port and greeting from environment variables, which is exactly how we will configure it later:

mkdir -p ~/myapp
nano ~/myapp/server.js

Paste this, then save with Ctrl+O, Enter and exit with Ctrl+X:

const http = require('http');
const port = Number(process.env.PORT || 3000);
const greeting = process.env.GREETING || 'Hello';

http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end(`${greeting} from myapp, pid ${process.pid}\n`);
}).listen(port, '127.0.0.1', () => {
  console.log(`listening on 127.0.0.1:${port}`);
});

Run it by hand to make sure it works before systemd gets involved:

PORT=3000 GREETING=Hi node ~/myapp/server.js

You should see listening on 127.0.0.1:3000. Open a second SSH session and run curl http://127.0.0.1:3000; the answer is Hi from myapp, pid 12345 (with a different number). Go back to the first session and stop the app with Ctrl+C. This manual test matters: if the app does not run by hand, it will not run as a service either, and debugging is much easier here.

Step 2: Create a dedicated system user

Running an app as root means any bug in it, or in one of its hundreds of dependencies, can take over the whole server. Instead we create a system user: an account with no password, no login shell and no home directory, whose only purpose is to own the running process.

sudo useradd --system --user-group --home-dir /opt/myapp --no-create-home --shell /usr/sbin/nologin myapp
id myapp

The output looks like uid=998(myapp) gid=998(myapp) groups=998(myapp); the numbers vary. --system gives the account a low ID reserved for services, --user-group creates a matching myapp group, and /usr/sbin/nologin makes sure nobody can log in as this user, on both Debian and Ubuntu.

Step 3: Install the app under /opt

Your home directory is for you, not for running services. We copy the app to /opt/myapp and make it owned by root but readable by the myapp group. That way the service can read its code but cannot modify it, so a compromised app cannot plant a backdoor in itself.

sudo mkdir -p /opt/myapp
sudo cp -r ~/myapp/. /opt/myapp/
sudo chown -R root:myapp /opt/myapp
sudo chmod -R u=rwX,g=rX,o= /opt/myapp
ls -l /opt/myapp

Expect a line like -rw-r----- 1 root myapp 352 Nov 10 10:02 server.js. In u=rwX,g=rX,o=, the capital X adds execute permission only to directories and files that are already executable, and o= removes all access for other users. For a real Node.js project, run npm ci --omit=dev in your build folder (~/myapp) before copying, so node_modules comes along.

Step 4: Create the environment file

Configuration and secrets do not belong in the code or in the unit file. We put them in a separate file that systemd loads when it starts the service:

sudo mkdir -p /etc/myapp
sudo nano /etc/myapp/myapp.env
# /etc/myapp/myapp.env
PORT=3000
GREETING=Hello
NODE_ENV=production
# Example secret, replace it with your own long random value
API_TOKEN=change-me-to-a-long-random-string

Then lock it down so only root can read it:

sudo chown root:root /etc/myapp/myapp.env
sudo chmod 600 /etc/myapp/myapp.env

You might wonder how the app reads a file it has no permission to open. It does not have to: systemd itself (running as root) reads the file before switching to the myapp user and hands the values to the process as environment variables. The format is strict because systemd is not a shell: one NAME=value per line, no spaces around =, no export keyword, no $(command) substitutions. Lines starting with # are comments, and values with spaces go in double quotes.

Step 5: Write the unit file

Unit files you write yourself live in /etc/systemd/system/. Units installed by packages live in /usr/lib/systemd/system/ (also reachable as /lib/systemd/system/ on both distributions); never put your own files there, because package updates own that directory.

sudo nano /etc/systemd/system/myapp.service
[Unit]
Description=My example web app
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=60
StartLimitBurst=5

[Service]
Type=exec
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
EnvironmentFile=/etc/myapp/myapp.env
ExecStart=/usr/bin/node /opt/myapp/server.js
Restart=on-failure
RestartSec=5
TimeoutStopSec=20
SyslogIdentifier=myapp

[Install]
WantedBy=multi-user.target

What each line does:

  • Description: the human-readable name shown in systemctl status and in the logs.
  • After= and Wants=network-online.target: Wants= pulls in the “network is configured” target, and After= makes our service wait for it. This matters if your app connects to a remote database at startup.
  • StartLimitIntervalSec and StartLimitBurst: if the service starts more than 5 times within 60 seconds, systemd gives up instead of looping forever. These two belong in [Unit], not [Service], a very common mistake.
  • Type=exec: the service counts as started once the program has actually been executed. If the path is wrong, systemctl start fails immediately with a clear error. Type=simple is the older default; Type=notify is for apps that report readiness themselves.
  • User and Group: the account from Step 2.
  • WorkingDirectory: relative paths in your app resolve from here.
  • EnvironmentFile: the file from Step 4.
  • ExecStart: the command to run. It must start with an absolute path, and there is no shell, so pipes, &&, > redirections and ~ do not work.
  • Restart=on-failure: restart after a non-zero exit code, a crash signal or a timeout, but not after a clean exit or systemctl stop. Use Restart=always for workers that are designed to exit and come back.
  • RestartSec=5: wait five seconds before restarting, so a crash loop does not burn the CPU.
  • TimeoutStopSec=20: on stop, systemd sends SIGTERM, waits 20 seconds for a graceful shutdown, then sends SIGKILL.
  • SyslogIdentifier: the tag in front of every log line.
  • WantedBy=multi-user.target: this is what enable uses to start the service on boot.

Note that unit files are readable by every user on the system, which is exactly why secrets stay in the environment file. Check the syntax before going further:

sudo systemd-analyze verify /etc/systemd/system/myapp.service

No output means no problems found.

Step 6: Enable, start and test automatic restarts

sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
systemctl status myapp.service --no-pager

daemon-reload tells systemd to re-read unit files from disk (needed every time you edit one). enable creates the link that starts the service on boot, and --now also starts it right away. The status output should look roughly like this:

● myapp.service - My example web app
     Loaded: loaded (/etc/systemd/system/myapp.service; enabled; preset: enabled)
     Active: active (running) since Tue 2026-11-10 10:15:02 EET; 4s ago
   Main PID: 2481 (node)
      Tasks: 7 (limit: 2307)
     Memory: 38.5M

Nov 10 10:15:02 vps systemd[1]: Started myapp.service - My example web app.
Nov 10 10:15:02 vps myapp[2481]: listening on 127.0.0.1:3000

The important parts are enabled (starts on boot) and active (running). Test it with curl http://127.0.0.1:3000, which should answer Hello from myapp, pid 2481: the greeting now comes from the environment file.

Now simulate a crash by killing the process the hard way:

sudo systemctl kill --signal=SIGKILL myapp.service
sleep 7
systemctl status myapp.service --no-pager

The service is running again with a new Main PID, and the log lines show Main process exited, code=killed, status=9/KILL followed by Scheduled restart job, restart counter is at 1. That is your safety net working. Finally, the real test: run sudo reboot, reconnect after a minute and check systemctl status myapp again. It should be running without you touching anything.

The everyday commands you will use from now on: sudo systemctl restart myapp after a deploy, sudo systemctl stop myapp to stop it (no automatic restart after a deliberate stop), sudo systemctl disable myapp to stop starting it on boot, and systemctl is-active myapp or systemctl is-enabled myapp for quick checks in scripts.

Step 7: Read the logs with journalctl

Because the app writes to standard output, every line ends up in the journal, tagged with the service name and a timestamp. The most useful commands:

journalctl -u myapp -f                     # follow live, like tail -f
journalctl -u myapp -n 50 --no-pager       # last 50 lines
journalctl -u myapp --since "1 hour ago"   # time window
journalctl -u myapp -b                     # only since the last boot
journalctl -u myapp -p err                 # only errors and worse

-u filters by unit, -f follows new lines (exit with Ctrl+C), -b limits output to the current boot and -p err shows only messages logged at error priority or higher. Add -o short-iso for ISO timestamps or -o cat to see just the message text.

Permissions differ between distributions. On Ubuntu, the first user is a member of the adm group and can read all logs. On Debian, the user created at install time often is not, so you only see your own messages. Either prefix the commands with sudo, or add yourself to the systemd-journal group and log in again:

sudo usermod -aG systemd-journal "$USER"

Make sure logs survive a reboot. Current Debian and Ubuntu releases normally store the journal persistently in /var/log/journal. Check with ls /var/log/journal and journalctl --disk-usage. If the directory is missing, logs are kept in memory only; create it and restart journald:

sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo systemctl restart systemd-journald

To cap disk usage, use a drop-in file instead of editing the main journald.conf (if the file below already exists, merge the setting into it rather than overwriting it):

sudo mkdir -p /etc/systemd/journald.conf.d
sudo nano /etc/systemd/journald.conf.d/size.conf
[Journal]
SystemMaxUse=500M

Apply it with sudo systemctl restart systemd-journald.

Step 8: Add sandboxing and check the security score

Our service already runs as an unprivileged user, but it can still read most of the filesystem, see other processes and call almost any kernel function. systemd can shrink that dramatically with a few lines. First, measure where we are:

systemd-analyze security myapp.service

You get a long table of checks and, at the end, a line like Overall exposure level for myapp.service: 9.2 UNSAFE followed by a small icon. The score goes from 0 (locked down) to 10 (fully exposed). Do not panic: “UNSAFE” here just means “no sandboxing configured”, which is the default for every service.

Open the unit file again (sudo nano /etc/systemd/system/myapp.service) and add these lines at the end of the [Service] section, just before [Install]:

# Writable data directory: /var/lib/myapp, exported as $STATE_DIRECTORY
StateDirectory=myapp
UMask=0077

# Filesystem
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes

# Privileges
NoNewPrivileges=yes
CapabilityBoundingSet=
RestrictSUIDSGID=yes

# Kernel and system
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectHostname=yes
ProtectProc=invisible

# Network, namespaces, system calls
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictNamespaces=yes
RestrictRealtime=yes
LockPersonality=yes
RemoveIPC=yes
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallFilter=~@privileged
SystemCallErrorNumber=EPERM

# Resource limits
MemoryMax=512M
TasksMax=200

In plain words:

  • ProtectSystem=strict makes the entire filesystem read-only for the app, except the directories you explicitly allow. StateDirectory=myapp is that allowance: systemd creates /var/lib/myapp, owned by myapp, and passes its path in the STATE_DIRECTORY variable. Store uploads, SQLite files or caches there. UMask=0077 makes new files private to the service.
  • ProtectHome hides /home and /root, PrivateTmp gives the app its own empty /tmp, PrivateDevices hides hardware devices.
  • NoNewPrivileges, an empty CapabilityBoundingSet and RestrictSUIDSGID guarantee the process can never gain root powers, even through a setuid binary.
  • The Protect… kernel options stop it from changing kernel settings, loading modules, reading kernel logs, changing the clock or the hostname. ProtectProc=invisible hides other users’ processes.
  • RestrictAddressFamilies allows only IPv4, IPv6 and local Unix sockets (add AF_NETLINK if your app lists network interfaces). SystemCallFilter allows the system calls a normal service needs and blocks privileged ones; with SystemCallErrorNumber=EPERM a blocked call returns an error instead of killing the process.
  • MemoryMax and TasksMax stop a memory leak or a fork bomb from taking the whole VPS down.

We deliberately left out MemoryDenyWriteExecute=yes: it breaks Node.js, whose JavaScript engine generates machine code at runtime. It is a good addition for Go binaries (see below). Apply and measure again:

sudo systemctl daemon-reload
sudo systemctl restart myapp.service
systemctl status myapp.service --no-pager
curl http://127.0.0.1:3000
systemd-analyze security myapp.service | tail -n 1

The app answers as before, and the last line now reads something like Overall exposure level for myapp.service: 2.1 OK. Exact numbers vary with the systemd version (Debian 13 ships systemd 257, Ubuntu 24.04 ships 255).

systemd-analyze security: exposure score (approx.)Lower is better. Values vary with the systemd version.Root, no options9.6 UNSAFEUser=myapp only9.2 UNSAFE+ filesystem rules6.4 MEDIUMFull hardening2.1 OK0246810Overall exposure level (0 = locked down, 10 = fully exposed)Unit configurationRatings: 9.0+ UNSAFE, 7.5+ EXPOSED, 5.0+ MEDIUM, 1.0+ OK, below 1.0 SAFE
Approximate exposure scores for our example in Step 8. "+ filesystem rules" means NoNewPrivileges, ProtectSystem, ProtectHome and PrivateTmp only; "Full hardening" is the complete block above.

A note for LXC containers. If your VPS is an unprivileged LXC container rather than a KVM virtual machine, systemd runs inside the container and cannot always create the namespaces some of these options rely on. The symptom is a service that fails with status=226/NAMESPACE. Remove the options one at a time, starting with PrivateDevices, ProtectClock, ProtectKernelLogs, ProtectKernelModules and ProtectProc, until it starts, or ask your provider whether nesting can be enabled. On a KVM VPS everything works as shown.

Adapting the unit for Python and Go apps

Steps 2, 3, 4, 6, 7 and 8 stay the same. Only the way you install the app and the ExecStart= line change.

Python

Python apps should run from a virtual environment (venv), an isolated folder holding the interpreter link and the app’s own libraries. Debian and Ubuntu need the python3-venv package for that. Here is the same example app in Python, saved as ~/myapp/app.py:

import os
from http.server import BaseHTTPRequestHandler, HTTPServer

PORT = int(os.environ.get("PORT", "3000"))
GREETING = os.environ.get("GREETING", "Hello")

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        body = f"{GREETING} from myapp, pid {os.getpid()}\n".encode()
        self.send_response(200)
        self.send_header("Content-Type", "text/plain")
        self.end_headers()
        self.wfile.write(body)

print(f"listening on 127.0.0.1:{PORT}", flush=True)
HTTPServer(("127.0.0.1", PORT), Handler).serve_forever()

Install it with a venv (skip the pip line if you have no requirements.txt):

sudo apt install -y python3-venv
sudo mkdir -p /opt/myapp
sudo cp -r ~/myapp/. /opt/myapp/
sudo python3 -m venv /opt/myapp/venv
sudo /opt/myapp/venv/bin/pip install -r /opt/myapp/requirements.txt
sudo chown -R root:myapp /opt/myapp
sudo chmod -R u=rwX,g=rX,o= /opt/myapp

In the unit file, replace the ExecStart= line and add one environment line:

Environment=PYTHONUNBUFFERED=1
ExecStart=/opt/myapp/venv/bin/python /opt/myapp/app.py

PYTHONUNBUFFERED=1 makes Python write log lines immediately instead of buffering them, otherwise they show up in the journal late or in bursts. For a Flask or Django app, run a production server instead, for example ExecStart=/opt/myapp/venv/bin/gunicorn --bind 127.0.0.1:3000 --workers 2 app:app.

Go

Go compiles to a single static binary, which makes it the easiest case. Build it on your workstation for the server’s architecture (most VPS are amd64; use arm64 for ARM servers), copy it over and install it with the right owner and mode in one command:

# on your workstation
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o myapp .
scp myapp youruser@your-vps-ip:/tmp/

# on the VPS
sudo mkdir -p /opt/myapp
sudo install -o root -g myapp -m 0750 /tmp/myapp /opt/myapp/myapp

The unit uses ExecStart=/opt/myapp/myapp. Go programs do not generate code at runtime, so you can also add MemoryDenyWriteExecute=yes to the hardening block for an even lower score.

Deploying a new version

Updating the app is always the same three moves: copy the new files, fix ownership, restart.

sudo cp -r ~/myapp/. /opt/myapp/
sudo chown -R root:myapp /opt/myapp
sudo chmod -R u=rwX,g=rX,o= /opt/myapp
sudo systemctl restart myapp.service
journalctl -u myapp -n 20 --no-pager

If you change the environment file, restart too: variables are read only when the service starts. If you change the unit file, run sudo systemctl daemon-reload before restarting.

Troubleshooting and common mistakes

When a service fails, systemctl status myapp shows an exit status, and journalctl -u myapp -n 50 shows the reason. The most frequent ones:

  • status=203/EXEC: systemd could not execute ExecStart. The path is wrong, not absolute, not executable, or the runtime is installed inside your home directory (nvm, pyenv), where the myapp user cannot reach it. Use a system-wide runtime and the path from command -v node.
  • status=200/CHDIR: the WorkingDirectory is missing or not readable by the service user. Test with sudo -u myapp ls /opt/myapp.
  • status=217/USER: the user in User= does not exist. Check id myapp.
  • status=226/NAMESPACE: the sandbox could not be built, usually in an unprivileged LXC container (see Step 8), or a path listed in ReadWritePaths= does not exist.
  • “The unit file changed on disk”: you forgot sudo systemctl daemon-reload after editing.
  • “Start request repeated too quickly”: the start limit was hit. Read the journal, fix the cause, then run sudo systemctl reset-failed myapp.service and start it again.
  • EROFS or EACCES when writing files: that is ProtectSystem=strict doing its job. Write to $STATE_DIRECTORY or add a ReadWritePaths= line for the exact folder.
  • Environment variables not applied: check the file format (no export, no spaces around =) and remember to restart after changes.
  • Port below 1024 fails with EACCES: unprivileged users cannot bind ports like 80 or 443. Put a reverse proxy in front, or allow it with AmbientCapabilities=CAP_NET_BIND_SERVICE plus CapabilityBoundingSet=CAP_NET_BIND_SERVICE.
  • Works by hand, fails as a service: run it exactly like systemd does, sudo -u myapp /usr/bin/node /opt/myapp/server.js, and read the error.

Security notes

  • Never run apps as root. The dedicated user has no shell, no password and no sudo rights; give each app its own user.
  • Code is read-only for the app. Root owns /opt/myapp, so even a compromised app cannot modify itself. Only /var/lib/myapp is writable.
  • Keep secrets out of unit files. Unit files and the output of systemctl show are readable by every user. Secrets go in the 600 environment file. Keep in mind that environment variables are still visible to root and to processes of the same user; for stronger isolation, look at systemd’s LoadCredential=, which exposes secrets as files in $CREDENTIALS_DIRECTORY.
  • Do not expose the app port. It listens on 127.0.0.1 and a reverse proxy with TLS handles the public traffic. If you really must bind to all interfaces, open the port deliberately with sudo ufw allow 3000/tcp. Since this is a normal host service, ufw does filter it (unlike Docker-published ports, which bypass ufw).
  • Sandboxing is defense in depth, not a replacement for updates. Keep the system and the runtime patched with sudo apt update && sudo apt upgrade. On Ubuntu, reboot when /var/run/reboot-required exists; Debian does not always create that file, so reboot whenever a new linux-image package was installed. Your service will come back on its own.
  • Use the limits. MemoryMax and TasksMax keep one misbehaving app from starving everything else on the VPS.

So, is a systemd service all you need?

For most single apps on a VPS, yes. With one unit file of about 50 lines you get start on boot, automatic restarts with a sane crash-loop limit, a dedicated user, secrets kept out of the code, central searchable logs and a sandbox that turns an “UNSAFE” score into an “OK” one. No extra daemon, no extra language runtime, and the same recipe works for Node.js, Python, Go and anything else that runs in the foreground. When you outgrow it, the same skills carry over to timers, socket activation and template units.

Grab a VPS from us and you can have your app running as a hardened service in twenty minutes. If you would rather have someone else write, harden and monitor your services, our server administration team does exactly that every day.

FAQ

What is the difference between enable and start?

start runs the service now, until the next reboot or stop. enable makes it start automatically on every boot but does not start it now. enable --now does both, which is usually what you want.

Can I run several copies of the same app?

Yes, with a template unit. Name the file myapp@.service and use %i inside it, for example EnvironmentFile=/etc/myapp/%i.env. Then sudo systemctl enable --now myapp@shop and myapp@blog run two independent instances, each with its own configuration.

Can I create a service without sudo?

Yes, as a user service: place the unit in ~/.config/systemd/user/, manage it with systemctl --user, and ask an admin to run sudo loginctl enable-linger yourname once so it keeps running after you log out and starts at boot. System services as shown here are still the better choice on a server you control.

Should I edit unit files installed by packages?

No. Use sudo systemctl edit nginx.service, which creates an override file in /etc/systemd/system/nginx.service.d/ that survives package updates. Your own units in /etc/systemd/system/ can be edited directly.

Do I still need PM2 or supervisord?

Not for keeping an app alive. systemd already covers starting on boot, restarts, logs and resource limits. PM2’s cluster mode can still be handy for Node.js, but you can also run several instances with a template unit behind a reverse proxy.

Trusted By & Member Of

We are proud members of leading internet infrastructure organizations.

RIPE NCC MANRS PeeringDB RoTLD DSIX SBIX 4IXP LOCIX Euro-IX RIPE NCC MANRS PeeringDB RoTLD DSIX SBIX 4IXP LOCIX Euro-IX