Skip to content
DevOps devops app-deployment 5 min read

Running a Node App as a systemd Service

When you deploy a Node.js app, you need something to keep it running: starting it when the server boots, restarting it if it crashes, and capturing its logs. Many tutorials reach for PM2 (a popular Node process manager), but Ubuntu already ships a powerful tool that does all of this — systemd. systemd (the “system and service manager”) is the program that starts and supervises every background service on Ubuntu, from SSH to your database. This page shows you how to run your Node app as a native systemd service, with no extra dependency to install.

Why use systemd instead of PM2

A process manager is a program that launches your app, watches it, and restarts it if it dies. PM2 is one such tool. But systemd is already running on your Ubuntu server (it is PID 1, the very first process the kernel starts), so using it means one less moving part to install, update, and learn.

ConcernsystemdPM2
Extra installNone — built into Ubuntunpm install -g pm2
Auto-start on bootsystemctl enableNeeds pm2 startup + pm2 save
Crash restartRestart=alwaysBuilt in
Log managementjournald (built in, rotated)Separate log files you must rotate
Runs as non-root userUser= in unit fileRuns as the user that started pm2
Cluster mode (multi-core)You write multiple units or use the node cluster APIOne flag: pm2 start -i max

When to use systemd: single app or a handful of apps per server, you want zero extra tooling, and you are comfortable editing one config file. This is the cleanest, most “Linux-native” option and what most production servers use.

When PM2 is nicer: you want built-in load balancing across CPU cores with one flag, or a dashboard (pm2 monit), or you deploy many Node apps and like PM2’s ecosystem file. See PM2 process manager for that path.

Prerequisites

You need Node installed and your app’s code on the server. Confirm the exact path to the node binary — systemd needs an absolute path because it does not load your shell’s PATH.

which node

Output:

/usr/bin/node

If you installed Node with nvm (Node Version Manager), node lives somewhere like /home/deploy/.nvm/versions/node/v22.14.0/bin/node and is NOT on the system path. Either use that full path in the unit file, or install Node system-wide (for example via NodeSource) so it sits at /usr/bin/node. A wrong node path is the number-one reason these services fail to start.

It is good practice to run the app as a dedicated, non-privileged user rather than root. Create one if you do not have it:

sudo adduser --system --group --no-create-home deploy

Make sure that user can read your app files (assume the app lives in /var/www/myapp):

sudo chown -R deploy:deploy /var/www/myapp

Write the service unit file

A systemd unit file is a plain-text config (ending in .service) that tells systemd how to start, stop, and supervise your app. They live in /etc/systemd/system/. Create one for your app:

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

Paste this in, then save (in nano: Ctrl+O, Enter, Ctrl+X):

[Unit]
Description=My Node.js application
After=network.target

[Service]
Type=simple
User=deploy
Group=deploy
WorkingDirectory=/var/www/myapp
ExecStart=/usr/bin/node /var/www/myapp/server.js
Restart=always
RestartSec=5
Environment=NODE_ENV=production
Environment=PORT=3000

# Security hardening
NoNewPrivileges=true
ProtectSystem=full

[Install]
WantedBy=multi-user.target

What each line does:

  • After=network.target — wait until networking is up before starting (your app likely listens on a port).
  • Type=simple — the default: systemd considers the service started as soon as ExecStart runs. Correct for a normal Node app that stays in the foreground.
  • User / Group — run as deploy, not root, so a compromised app has limited power.
  • WorkingDirectory — the folder the process runs in, so relative paths in your code resolve correctly.
  • ExecStart — the exact command. Absolute path to node, then absolute path to your entry file.
  • Restart=always + RestartSec=5 — if the app exits for any reason, restart it after 5 seconds.
  • Environment= — set environment variables (one per line). For lots of secrets, use EnvironmentFile= instead — see environment config.
  • WantedBy=multi-user.target — what makes it start on boot once enabled.

Load, enable, and start the service

Whenever you create or edit a unit file, you must tell systemd to re-read its config:

sudo systemctl daemon-reload

Now enable the service (so it starts automatically on every boot) and start it right now:

sudo systemctl enable --now myapp.service

Output:

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

Check that it is running:

sudo systemctl status myapp.service

Output:

● myapp.service - My Node.js application
     Loaded: loaded (/etc/systemd/system/myapp.service; enabled; preset: enabled)
     Active: active (running) since Mon 2026-06-15 10:21:04 UTC; 8s ago
   Main PID: 4187 (node)
      Tasks: 11 (limit: 4631)
     Memory: 41.2M
        CPU: 612ms
     CGroup: /system.slice/myapp.service
             └─4187 /usr/bin/node /var/www/myapp/server.js

Active: active (running) and enabled mean you are done — the app is up and will come back after a reboot or crash.

Reading logs with journalctl

systemd captures everything your app prints to standard output and standard error into the journal (its central, rotated log store). You read it with journalctl.

See the most recent logs and follow new ones live (like tail -f):

sudo journalctl -u myapp.service -f

Output:

Jun 15 10:21:04 web-01 systemd[1]: Started My Node.js application.
Jun 15 10:21:05 web-01 node[4187]: Server listening on port 3000
Jun 15 10:21:05 web-01 node[4187]: Connected to database

Other useful forms:

sudo journalctl -u myapp.service --since "1 hour ago"   # recent window
sudo journalctl -u myapp.service -n 100 --no-pager       # last 100 lines
sudo journalctl -u myapp.service -p err                  # errors only

Common operations

sudo systemctl restart myapp.service    # restart (e.g. after a deploy)
sudo systemctl stop myapp.service       # stop
sudo systemctl disable myapp.service    # don't start on boot anymore
sudo systemctl reload-or-restart myapp.service

After editing the .service file, always run sudo systemctl daemon-reload before restarting, or systemd keeps using the old config.

Best Practices

  • Always use absolute paths in ExecStart (/usr/bin/node, full path to your entry file) — systemd does not read your shell PATH.
  • Run as a dedicated non-root user with User= and add NoNewPrivileges=true and ProtectSystem=full to limit the blast radius if the app is compromised.
  • Keep secrets out of the unit file: use EnvironmentFile=/etc/myapp.env with chmod 600 permissions instead of inline Environment= lines.
  • Set Restart=always with a sane RestartSec so a crash loop does not hammer the CPU restarting instantly.
  • Run daemon-reload after every edit, and verify with systemctl status plus journalctl rather than assuming it worked.
  • Put a reverse proxy like Nginx in front of the app rather than exposing port 3000 directly — see Nginx in front of your app.
Last updated June 15, 2026
Was this helpful?