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.
| Concern | systemd | PM2 |
|---|---|---|
| Extra install | None — built into Ubuntu | npm install -g pm2 |
| Auto-start on boot | systemctl enable | Needs pm2 startup + pm2 save |
| Crash restart | Restart=always | Built in |
| Log management | journald (built in, rotated) | Separate log files you must rotate |
| Runs as non-root user | User= in unit file | Runs as the user that started pm2 |
| Cluster mode (multi-core) | You write multiple units or use the node cluster API | One 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),
nodelives somewhere like/home/deploy/.nvm/versions/node/v22.14.0/bin/nodeand 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 wrongnodepath 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 asExecStartruns. Correct for a normal Node app that stays in the foreground.User/Group— run asdeploy, 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 tonode, 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, useEnvironmentFile=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 shellPATH. - Run as a dedicated non-root user with
User=and addNoNewPrivileges=trueandProtectSystem=fullto limit the blast radius if the app is compromised. - Keep secrets out of the unit file: use
EnvironmentFile=/etc/myapp.envwithchmod 600permissions instead of inlineEnvironment=lines. - Set
Restart=alwayswith a saneRestartSecso a crash loop does not hammer the CPU restarting instantly. - Run
daemon-reloadafter every edit, and verify withsystemctl statusplusjournalctlrather 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.