NewHalo Edge 3 is here: HTTP/3 everywhere and 42 edge locations.Learn more
Docs / LAMP and app hosting

Host a Node.js app with systemd

LAMP and app hosting9 min readUbuntu 24.04 LTS

Running node server.js in a terminal stops the moment you close it. A systemd service keeps your Node.js app running in the background, starts it on boot, and restarts it if it crashes.

Before you begin

  • A VPS running Ubuntu 24.04 LTS, with SSH access as a regular user with sudo rights (this guide uses alex).
  • Your Node.js application, or the small example below to practice with.
  • Basic familiarity with the terminal. You do not need to know systemd in depth.

Step 1: Install Node.js with nvm

Ubuntu's packaged Node.js is often an older release. nvm (Node Version Manager) installs current versions into your home folder and makes switching easy. The first command downloads and runs nvm's official install script, version 0.40.1.

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc
nvm install --lts
node -v
which node
v22.x.x
/home/alex/.nvm/versions/node/v22.x.x/bin/node

Write down the full path that which node prints. systemd does not load your shell profile, so the service must use this exact path.

Step 2: Create the example app

Make a folder for the app and create a small server file. This example listens on the address 127.0.0.1, so only programs on the server can reach it directly.

mkdir -p ~/apps/hello-node
cd ~/apps/hello-node
nano server.js
const http = require('http');
const port = process.env.PORT || 3000;

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('Hello from Node.js\n');
});

server.listen(port, '127.0.0.1', () => {
  console.log(`Listening on port ${port}`);
});

Run it once in the foreground to make sure it works. In a second SSH session, or after stopping it, check the response:

node server.js
curl http://127.0.0.1:3000
Hello from Node.js

Press Ctrl+C to stop the foreground process.

Step 3: Write the systemd service file

A service file describes how to run the app. Create it in the system folder for services:

sudo nano /etc/systemd/system/hello-node.service
[Unit]
Description=Hello Node.js app
After=network.target

[Service]
User=alex
WorkingDirectory=/home/alex/apps/hello-node
ExecStart=/home/alex/.nvm/versions/node/v22.x.x/bin/node server.js
Environment=PORT=3000
Environment=NODE_ENV=production
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

Here is what the important lines do. User runs the app as your regular account rather than as root. WorkingDirectory sets the folder the app starts in. ExecStart is the full command, using the Node path from Step 1 and the version you actually installed. Restart=on-failure brings the app back if it exits with an error, and RestartSec=5 waits five seconds first. WantedBy=multi-user.target starts the service at boot.

Step 4: Start the service and enable it on boot

Tell systemd to read the new file, then start the service and enable it so it starts automatically after a reboot. enable --now does both at once.

sudo systemctl daemon-reload
sudo systemctl enable --now hello-node
sudo systemctl status hello-node --no-pager
● hello-node.service - Hello Node.js app
     Loaded: loaded (/etc/systemd/system/hello-node.service; enabled)
     Active: active (running)

Confirm the app answers:

curl http://127.0.0.1:3000

Step 5: Read the logs and test a restart

Anything your app writes to the console goes to the systemd journal. Read the last 20 lines with journalctl, or add -f to follow new lines live:

sudo journalctl -u hello-node -n 20 --no-pager

To prove that the restart policy works, stop the process by force. systemd should start it again within about five seconds:

sudo pkill -f "server.js"
sleep 6
sudo systemctl status hello-node --no-pager

The status should show active (running) again, with a new process ID.

Tip: After you edit the service file, run sudo systemctl daemon-reload and then sudo systemctl restart hello-node. Changes to the app's own code only need a restart.

Troubleshooting

  • Status shows "failed" or "activating (auto-restart)" repeatedly. Read the reason with sudo journalctl -u hello-node -n 50 --no-pager. A wrong Node path or a typo in ExecStart is the most common cause.
  • "Address already in use". Another program uses port 3000. Find it with sudo ss -tlnp | grep 3000, then change PORT in the service file.
  • Your changes to the service file have no effect. You skipped daemon-reload. Run it, then restart the service.
  • Missing modules. Run npm install in the app folder as the same user the service runs as.

Next steps

The app listens only on 127.0.0.1, which is a good, safe default. To serve it on your domain, put nginx in front of it as a reverse proxy, as described in Host a Python web app with Gunicorn, which uses the same pattern. Do not open port 3000 in the firewall.

Something not working? Chat or email our support team and we will help you get unstuck.

Contact support