October DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsWindows FixRecommendedWindows errors stealing your time? Find the fix fastScan stability, cleanup and performance issues.Fix NowOctober DealsAmazon USDeal season is back - check today's better picksAmazon US: current deals, useful picks and tech finds.See Picks×
Skip to content
World desk10 min

How to Run a Node.js App as a systemd Daemon on a Linux VPS

Set up a foreground Node.js process under systemd with a dedicated user, protected environment file, automatic restarts, journal logging, verification steps, and a practical systemd-versus-PM2 comparison.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The reliable way to keep a Node.js application running on a Linux VPS is to leave Node in the foreground and let systemd own its lifecycle. A service unit can start the app at boot, restart it after a failure, provide a fixed working directory and environment, send output to the journal, and run it as a dedicated non-root user. Do not background the process or make the Node program daemonize itself.

This guide builds a production-oriented service, enables it at boot, shows how to inspect failures, and explains when a Node-specific supervisor such as PM2 is a better fit.

As an Amazon Associate I earn from qualifying purchases.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

What a Node.js daemon on a VPS actually is

On Linux, “daemon” generally means a long-running process managed independently of an interactive shell. With systemd, your Node.js program does not need special daemon code. It remains a normal foreground process, while systemd supervises it.

The Linux man-pages description is precise: “A unit configuration file whose name ends in “.service” encodes information about a process controlled and supervised by systemd.” That supervision covers boot ordering, starting and stopping, restart policy, process status, and journal integration.

A typical deployment has three layers:

  • Node.js: listens on an internal address and port and serves the application.
  • systemd: starts Node, restarts it when appropriate, and records standard output and errors.
  • Nginx or another reverse proxy: accepts public HTTP or HTTPS traffic and forwards requests to Node on a private or loopback port.

Keeping the application behind a reverse proxy means the public web server, TLS configuration, and firewall exposure can be managed separately from the Node process.

Prerequisites and decisions to make first

You need a Linux VPS with administrative access, a working Node.js installation, your application files, and a port selected for local listening. Distribution-specific package commands and Node executable locations vary, so do not copy a path from another server without checking it.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Confirm Node and the application

From the account that installed Node, find the executable:

command -v node
node --version
npm --version

Use the absolute path printed by command -v node in ExecStart. An interactive shell’s PATH, including paths added by a version manager, is not automatically inherited by systemd.

Run the application manually once from its intended directory. Confirm which command starts it, which port it uses, whether it expects an environment file, and whether it writes files at runtime. A service that only works from your login shell usually has an unstated dependency on the shell’s working directory, environment, or permissions.

Choose a service identity

Use a dedicated account such as myapp, not root. The account should own only the files it must modify and should have no interactive shell where practical. A service account limits the damage from an application vulnerability or an accidental file operation.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Choose restart semantics

Restart=on-failure is suitable when a clean exit should remain stopped but crashes should be recovered automatically. Restart=always also restarts after a clean exit. Select the policy that matches your application’s intended behavior; automatic restarts do not fix a program that repeatedly crashes, and a tight restart loop can hide the original error.

Prepare the application directory and environment

Place the release in a fixed directory so systemd does not depend on a developer’s home directory or a temporary checkout. For example:

sudo install -d -o myapp -g myapp /opt/myapp

Copy the application and its production dependencies into /opt/myapp, then ensure the service account can read the code and execute the files it needs. If the application must write uploads, caches, or generated data, give write access only to those specific directories.

Keep secrets out of the unit file. Put values such as database credentials and signing keys in an environment file, for example /etc/myapp/myapp.env. Restrict that file so only root and the service’s group can read it:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
sudo install -o root -g myapp -m 0640 /dev/null /etc/myapp/myapp.env

Add one variable per line, using the syntax your application expects:

NODE_ENV=production
PORT=3000
DATABASE_URL=replace-with-your-secret

Do not place shell commands, exports, or unquoted assumptions in this file. Test that the service account can read it, but do not make it world-readable.

Write the systemd service unit

Create /etc/systemd/system/myapp.service. The following unit is a practical baseline:

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

[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/node /opt/myapp/server.js
EnvironmentFile=/etc/myapp/myapp.env
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

Replace /usr/bin/node with the exact path returned by command -v node. It is an example, not a universal location. Replace the script path and service account with your own values.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

What each directive does

Directive Purpose Operational note
Description Human-readable name shown by status tools. Use a name that identifies the application and environment.
After=network.target Starts the unit after basic network initialization. This orders startup; it does not guarantee that a remote database is ready.
Type=simple Tells systemd that the process started by ExecStart is the main process. Keep Node in the foreground.
User and Group Drop privileges to the dedicated account. Check ownership of code, logs, sockets, and writable directories.
WorkingDirectory Sets the process’s current directory. Relative paths in the application resolve from this location.
ExecStart Defines the exact executable and arguments. Use absolute paths; systemd does not use your interactive shell setup.
EnvironmentFile Loads runtime variables separately from the unit. Protect the file because it may contain secrets.
Restart and RestartSec Control recovery after process exit. Investigate repeated crashes instead of masking them with restarts.
StandardOutput and StandardError Send normal output and errors to journald. Read them with journalctl.
WantedBy=multi-user.target Allows the unit to be enabled for normal multi-user boot. Enabling affects future boots; it does not by itself start a currently stopped service.

Do not add a shell wrapper unless you need one for a documented reason. A wrapper can obscure exit codes, signals, and the actual process systemd is supervising.

Install, enable, and start the daemon

After saving the unit, tell systemd to reread unit files and start the service:

  1. Install the unit.
    sudo install -m 0644 myapp.service /etc/systemd/system/myapp.service
  2. Reload unit definitions.
    sudo systemctl daemon-reload

    Run this after every edit to a unit file. It does not restart a process that is already running.

  3. Enable and start in one command.
    sudo systemctl enable --now myapp.service

    enable schedules startup at boot, while --now starts it immediately.

  4. Inspect the current state.
    sudo systemctl status myapp.service
  5. Follow live logs.
    sudo journalctl -u myapp.service -f

If you change only application code, a restart is normally sufficient:

sudo systemctl restart myapp.service

If you change the unit, run daemon-reload before restarting. To stop the service without removing boot enablement, use sudo systemctl stop myapp.service. To prevent it from starting at future boots, use sudo systemctl disable myapp.service.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Verify that the service is really usable

A green “active (running)” state proves that systemd has a live process; it does not prove that the application is accepting useful requests. Verify each layer in order.

  1. Check the status output for the main PID, recent exit code, and startup messages.
  2. Read recent logs rather than only following new ones:
    sudo journalctl -u myapp.service -n 100 --no-pager
  3. Confirm the process is listening on the intended interface and port with the socket inspection tool available on your distribution, such as:
    sudo ss -ltnp
  4. Call the local health endpoint from the VPS, for example:
    curl --fail http://127.0.0.1:3000/health
  5. If a reverse proxy is configured, test the public hostname separately. A successful local request does not rule out proxy routing, TLS, DNS, or firewall errors.

Bind an internal backend to loopback or a private interface when the proxy is the public entry point. Exposing the application port directly can bypass proxy access controls and TLS.

Diagnose common startup and runtime failures

“Executable path is not absolute” or Node cannot be found

Run command -v node and copy the resulting absolute path into ExecStart. A path that works in your shell may come from a version manager or profile script unavailable to systemd. After editing, run daemon-reload and restart.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The service exits immediately

Inspect the journal for syntax errors, missing modules, invalid environment variables, and permission failures:

sudo journalctl -u myapp.service -b --no-pager

Run the same Node command manually as the service account from WorkingDirectory. This often exposes files that are readable by your login account but not by myapp.

The service restarts continuously

Look at the first failure in the journal, not only the latest restart. Common causes include an occupied port, a failed database connection, an invalid configuration value, or an application that exits intentionally after a one-time task. Fix the cause, then restart. Do not stack another process manager on top of systemd merely to hide a restart loop.

Environment variables are missing

Confirm the EnvironmentFile path, spelling, permissions, and variable syntax. Remember that systemd does not source .bashrc, .profile, or shell-specific Node version-manager setup.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Local health works but public traffic fails

Check the reverse proxy’s upstream address and port, its own logs, firewall rules, DNS, and TLS configuration. Ensure Node is listening on an address reachable from the proxy. If Node is bound only to loopback, the proxy must run on the same VPS.

Use journald as the first diagnostic record

With the unit above, anything written to standard output or standard error is available through the system journal. Useful commands include:

sudo journalctl -u myapp.service -f
sudo journalctl -u myapp.service --since "1 hour ago"
sudo journalctl -u myapp.service -b
sudo journalctl -u myapp.service -p err

Include timestamps, request identifiers, and meaningful error context in application logs, but never print passwords, tokens, or full connection strings. Monitor restart frequency as well as log volume: a service that remains “active” only because it is repeatedly restarted is not healthy.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Put Nginx or another proxy at the edge

For a public web application, keep Node on an internal port such as 3000 and let the edge proxy listen on ports 80 and 443. The proxy can terminate HTTPS, select the correct virtual host, serve static assets, and forward dynamic requests to Node.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The exact proxy configuration depends on your distribution and application, but the traffic path should be explicit: client to proxy, proxy to the Node health endpoint or application port. Open only the ports required by that design in the VPS firewall. If no public client needs direct access to port 3000, do not expose it.

systemd versus PM2

Both can keep a Node application running, but they solve different operational problems. systemd is the native Linux service manager and adds no Node-specific runtime dependency. PM2 can be attractive when Node-specific clustering and a Node-oriented command-line workflow are requirements.

Concern systemd PM2
Boot integration Native Linux targets and dependencies. Requires PM2 startup integration with systemd or another boot mechanism.
Restart semantics Unit-level policies such as on-failure, delays, and dependency ordering. Node-focused restart controls and CLI-managed process definitions.
Logs Integrated with journald and standard Linux log tooling. PM2’s own log and process-management workflow.
Clustering Can run multiple separately configured instances, but provides no Node-specific cluster command. Convenient Node-oriented clustering features.
Dependencies Available as part of the operating system. Adds a Node package and its operational lifecycle.
Team fit Best when administrators already standardize on systemd. Useful when developers need PM2’s CLI and clustering model.

Choose one supervisor as the owner of start, stop, and restart operations. Running PM2 under a systemd unit that also restarts the same application can create overlapping policies, confusing status output, and restart storms. If you choose PM2, let it own the Node process and use systemd only for the clearly defined PM2 service; if you choose systemd directly, do not add PM2 just for redundancy.

Handle child processes correctly in Node.js

Some daemons launch workers, encoders, command-line utilities, or other subprocesses. Node’s child_process.spawn() returns a child object with a process ID and emits an error when spawning fails.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import { spawn } from 'node:child_process';

const child = spawn('/usr/bin/my-worker', ['--job', '42'], {
  stdio: 'inherit'
});

child.on('error', (error) => {
  console.error('Could not start worker:', error);
});

child.on('close', (code, signal) => {
  console.log('Worker finished', { code, signal });
});

A successful child.kill() means that a signal was sent; it does not mean the child has finished terminating. Use the child’s exit or close event to perform completion handling, release resources, or decide whether a job should be retried. Make sure the parent handles termination signals so children do not remain behind during a service stop.

Security and maintenance checklist

  • Run the service as a dedicated non-root account with no interactive shell where practical.
  • Keep the unit file, application directory, and environment file owned and readable only as required.
  • Store secrets in the protected environment file or another secret-management mechanism, never in a publicly readable unit or source repository.
  • Bind backend listeners to loopback or a private interface when a reverse proxy is the public entry point.
  • Open only required firewall ports.
  • Patch the operating system and Node.js runtime on a planned schedule.
  • Review journal output and restart frequency after every deployment.
  • Keep releases recoverable so you can restore the previous application version if a new one fails.

A repeatable deployment sequence

  1. Copy the new release into a versioned or otherwise recoverable directory.
  2. Install production dependencies and verify ownership for the service account.
  3. Update symlinks or configuration only after the new files are complete.
  4. Run sudo systemctl restart myapp.service.
  5. Check systemctl status, inspect the journal, and call the local health endpoint.
  6. Test the public route through the reverse proxy.
  7. If the health check fails, stop the rollout and restore the last known-good release before investigating further.

Final operating model

A robust Node.js daemon on a VPS is intentionally uneventful: one foreground process, one clearly owned systemd unit, one dedicated account, an explicit executable path, protected configuration, and logs you can inspect without guessing. Once the local health check and public proxy path both work, systemd provides the boot persistence and failure recovery that an interactive shell cannot.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
PC Slower Than It Used to Be?Free scan - under a minute
Crashes, No Sound, or Screen Glitches?Free driver scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.