Create systemd Service for Node.js and Python on VPS

A common pain point on VPS is an application stopping after a server reboot or crash without recovering automatically. systemd — the init system and service manager built into every modern Linux distribution — solves this by running your app as a background service that starts on boot and restarts automatically after a failure.

What Is a systemd Service Unit?

A service unit is a plain text file with a .service extension stored in /etc/systemd/system/. It describes which process to run, under which user, with what environment variables, and how to handle restarts. systemd reads this file and manages the process lifecycle for you.

Why not just use PM2? PM2 is popular for Node.js, but systemd is a native Linux solution that requires no extra installation. It works for any language, integrates with journald for centralized logging, and is the standard way to manage services on Ubuntu.

Service Unit File Structure

Every .service file has three sections:

[Unit]
Description=Human-readable service name
After=network.target         # Wait for network before starting

[Service]
Type=simple                  # Process type
User=ubuntu                  # Run as this user (not root)
WorkingDirectory=/home/ubuntu/myapp
ExecStart=/usr/bin/node server.js
Restart=always               # Restart whenever the process exits
RestartSec=5                 # Wait 5s before restarting

[Install]
WantedBy=multi-user.target   # Enable at boot

Example 1: Node.js Application

Create the Service File

sudo nano /etc/systemd/system/myapp.service
[Unit]
Description=My Node.js Application
After=network.target

[Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/myapp
ExecStart=/usr/bin/node /home/ubuntu/myapp/server.js
Restart=on-failure
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp
Environment=NODE_ENV=production
Environment=PORT=3000

[Install]
WantedBy=multi-user.target

Enable and Start the Service

# Reload systemd to pick up the new file
sudo systemctl daemon-reload

# Enable auto-start on boot
sudo systemctl enable myapp

# Start the service now
sudo systemctl start myapp

# Check status
sudo systemctl status myapp

Managing Environment Variables and Secrets in systemd

Hardcoding API keys, database passwords, or other secrets directly in a unit file is unsafe because anyone with access to systemctl cat myapp can read them. systemd provides a secure way to inject secrets into services without exposing them in the unit file.

Option 1: EnvironmentFile (Recommended)

# Create a separate environment file
sudo nano /etc/myapp.env

# Contents of /etc/myapp.env (no "export" keyword — KEY=VALUE only)
DB_PASSWORD=SuperSecretPassword123
API_KEY=sk-xxxxxxxxxxxxxxxxxxxx
PORT=3000

# Restrict permissions so only root can read it
sudo chmod 600 /etc/myapp.env
sudo chown root:root /etc/myapp.env

Reference the file in your unit file with EnvironmentFile:

[Service]
User=ubuntu
EnvironmentFile=/etc/myapp.env
ExecStart=/usr/bin/node /home/ubuntu/myapp/server.js
# DB_PASSWORD, API_KEY, and PORT are injected automatically

Option 2: systemd Credentials (Ubuntu 22.04+)

# Encrypt a secret with systemd-creds (optionally sealed to TPM2)
echo -n "SuperSecretPassword" | sudo systemd-creds encrypt --name=db-password -

# Reference in unit file
[Service]
LoadCredential=db-password:/etc/credentials/db-password
# App reads the secret at $CREDENTIALS_DIRECTORY/db-password

Security Rule: Never write passwords directly as Environment=DB_PASS=secret in a unit file. Running systemctl show myapp exposes all environment variables. Always use EnvironmentFile with restricted permissions instead.

Example 2: Python App (FastAPI / Flask)

Set Up a Virtual Environment

cd /home/ubuntu/myapi
python3 -m venv venv
source venv/bin/activate
pip install fastapi uvicorn

Create the Service File for Python/FastAPI

sudo nano /etc/systemd/system/myapi.service
[Unit]
Description=FastAPI Application
After=network.target

[Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/myapi
ExecStart=/home/ubuntu/myapi/venv/bin/uvicorn main:app --host 0.0.0.0 --port 8000
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapi
Environment=ENV=production

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable myapi
sudo systemctl start myapi
sudo systemctl status myapi

Common Service Management Commands

# Check status
sudo systemctl status myapp

# Stop / Start / Restart
sudo systemctl stop myapp
sudo systemctl start myapp
sudo systemctl restart myapp

# Disable from auto-start
sudo systemctl disable myapp

# View last 100 log lines
sudo journalctl -u myapp -n 100

# Follow logs in real time
sudo journalctl -u myapp -f

Production Best Practices

Choosing the Right Restart Policy

Resource Limits

[Service]
# Cap memory at 512 MB
MemoryMax=512M

# Limit CPU to 50%
CPUQuota=50%

Security Tip: Always specify a dedicated non-root User= in [Service]. Create a system user with no login shell: sudo useradd -r -s /bin/false appuser and run the service as that user to minimize privilege.

Restart Policy Comparison Table

Choosing the right restart policy is critical for production stability. Restarting too aggressively may mask application bugs; never restarting means a crashed service stays down indefinitely.

Restart Policy Restarts When Does Not Restart When Best For
no Never Any stop One-time tasks
on-failure Non-zero exit, signal kill systemctl stop Most web apps
always Any stop (including clean exit) Nothing Critical services
on-abnormal Signal, timeout, watchdog Normal non-zero exit Background workers

Prevent Restart Loops with StartLimitInterval

[Unit]
Description=My Application
StartLimitIntervalSec=60   # Within a 60-second window
StartLimitBurst=3           # Stop trying after 3 restarts

[Service]
Restart=on-failure
RestartSec=10
# If the service restarts more than 3 times in 60 seconds, systemd gives up.
# Run: sudo systemctl reset-failed myapp — before trying to start it again.

Test Auto-Restart

After setting up your service, always simulate a crash in a staging environment to confirm that systemd restarts the process correctly. Check the restart count and the log output to make sure everything behaves as expected before moving to production.

# Kill the process — systemd should restart it within RestartSec seconds
sudo kill -9 $(sudo systemctl show myapp -p MainPID | cut -d= -f2)

# Verify it restarted
sudo systemctl status myapp

# Check how many times the service has restarted
sudo systemctl show myapp -p NRestarts

Ready to Run Your App on a VPS?

Linux VPS starting at 500 THB/month with Full Root Access — deploy Node.js, Python, FastAPI, or any stack you need.

View VPS Plans

View all affordable VPS Thailand plans →