Skip to content
DevOps devops linux-admin 5 min read

Creating a Custom systemd Service

When you deploy your own app to a server, you need it to keep running after you close your terminal, start again automatically when the server reboots, and restart itself if it crashes. The standard way to do this on a modern Ubuntu server is to write a small text file called a systemd unit file (the instructions that tell the system how to run your program). This page shows you how to turn any Node.js or Python app into a managed background service, completely replacing tools like PM2.

What is systemd and why use it

systemd is the program that starts and supervises every background service on Ubuntu. A background program that runs without a terminal attached is called a daemon (pronounced “demon”). systemd is already installed and running as process number 1 on every Ubuntu 22.04 and 24.04 server, so there is nothing extra to install.

A service in systemd is just one daemon that systemd manages for you. You describe your app once in a unit file, and from then on systemd handles starting it, stopping it, restarting it on crash, and bringing it back after a reboot.

When to use this: Use a systemd service for any long-running app you control — a web API, a worker, a bot, a small script that loops forever. Do NOT use it for short, one-off tasks that run and exit (use a cron job or a systemd timer for those instead).

systemd vs PM2 — when to use which

Many Node tutorials reach for PM2 (a Node-specific process manager). On a production Linux server, systemd is the better default.

FeaturesystemdPM2
Pre-installed on UbuntuYesNo (needs npm install)
Works for any languageYesNode-focused
Starts on bootBuilt inNeeds extra setup
LogsCentral journalctlSeparate log files
Resource limits (memory/CPU)Built inLimited

Step 1: Have your app ready

For this example we have a Node.js API living in /opt/myapp, started with node server.js. The same steps work for a Python app — you would just point ExecStart at python3 and your script.

First, find the full path to your runtime so we can use it later.

which node

Output:

/usr/bin/node

Always use the full path in the unit file, because systemd runs with a minimal PATH (the list of folders the system searches for commands) and may not find node by name.

Running your app as the all-powerful root user is a security risk. If the app is hacked, the attacker gets full control of the server. Instead, create a locked-down user just for the app.

sudo useradd --system --no-create-home --shell /usr/sbin/nologin myapp

The --system flag makes a system account (not a login account), and --shell /usr/sbin/nologin means nobody can log in as it.

Make sure that user can read your app files:

sudo chown -R myapp:myapp /opt/myapp

Step 3: Write the unit file

Custom services go in /etc/systemd/system/. The file name becomes the service name, so myapp.service gives you a service called myapp.

sudo nano /etc/systemd/system/myapp.service

Paste the following:

[Unit]
Description=My Node.js API
# Wait until the network is ready before starting
After=network.target

[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/node /opt/myapp/server.js
# Restart the app if it ever exits or crashes
Restart=on-failure
RestartSec=5
# Pass environment variables to your app
Environment=NODE_ENV=production
Environment=PORT=3000

[Install]
WantedBy=multi-user.target

What each key means

KeyWhat it does
DescriptionHuman-readable name shown in systemctl status.
After=network.targetDon’t start until networking is up. Use After to set ordering.
Type=simpleThe default: systemd considers the service started as soon as ExecStart runs.
User / GroupRun the app as this unprivileged user, not root.
WorkingDirectoryThe folder the app runs from, as if you had cd’d into it.
ExecStartThe exact command to launch the app. Must use full paths.
Restart=on-failureRestart only if the app exits with an error. Use always to also restart on clean exits.
RestartSecSeconds to wait before restarting, so a broken app doesn’t spin in a tight loop.
EnvironmentSets an environment variable for the process. Repeat for each one.
WantedBy=multi-user.targetMakes the service start at boot when you enable it.

For a Python app the only line that changes is ExecStart:

ExecStart=/usr/bin/python3 /opt/myapp/main.py

Gotcha: Do not start the command with sudo inside ExecStart, and do not add an & to background it. systemd manages the process directly — adding either will break the service.

Step 4: Reload, enable, and start

Whenever you add or edit a unit file, you must tell systemd to re-read its configuration.

sudo systemctl daemon-reload

Now enable the service (so it starts automatically on every boot) and start it right now. You can do both in one command:

sudo systemctl enable --now myapp

Output:

Created symlink /etc/systemd/system/multi-user.target.wants/myapp.service → /etc/systemd/system/myapp.service.

Step 5: Check that it is running

sudo systemctl status myapp

Output:

● myapp.service - My Node.js API
     Loaded: loaded (/etc/systemd/system/myapp.service; enabled; preset: enabled)
     Active: active (running) since Mon 2026-06-15 10:22:14 UTC; 6s ago
   Main PID: 4127 (node)
      Tasks: 11 (limit: 4631)
     Memory: 38.4M
        CPU: 240ms
     CGroup: /system.slice/myapp.service
             └─4127 /usr/bin/node /opt/myapp/server.js

Active: active (running) and enabled mean the app is up now and will return after a reboot.

Step 6: View the logs

Anything your app prints to the screen is captured by systemd’s logging system, the journal. Read it with journalctl:

sudo journalctl -u myapp -f

The -u flag picks your unit, and -f “follows” the log live (like tail -f). Press Ctrl+C to stop watching.

Everyday commands

CommandWhat it does
sudo systemctl restart myappRestart after a code deploy.
sudo systemctl stop myappStop the app now.
sudo systemctl disable myappStop it from starting at boot.
sudo systemctl daemon-reloadRe-read unit files after editing.

Best Practices

  • Run every service as a dedicated unprivileged user, never as root.
  • Always use absolute paths in ExecStart (e.g. /usr/bin/node) — the minimal systemd PATH won’t find bare command names.
  • Set Restart=on-failure with a RestartSec of a few seconds so a crashing app recovers without hammering the CPU.
  • Keep secrets out of the unit file; load them from an EnvironmentFile= (e.g. /etc/myapp/env) with tight chmod 600 permissions.
  • Run sudo systemctl daemon-reload after every edit, then restart, or your changes are ignored.
  • Add hardening keys like NoNewPrivileges=true and ProtectSystem=full to limit what a compromised app can touch.
Last updated June 15, 2026
Was this helpful?