MeatButton

Your Systemd Service Won't Start: How to Keep Your App Running on a VPS

For anyone trying to keep an app running on a Linux server

You deployed your app to a VPS. You type the command, it starts up, everything works. Then you close your laptop, go to bed, and wake up to find your app is dead. Or you set up a service file because the AI told you to, ran systemctl start myapp, and it immediately failed.

You're not alone. This is one of the most common problems people hit after deploying to a server. Here's what's going on.

Why your app dies when you close the terminal

When you SSH into your server and run node app.js or python main.py, that program is attached to your terminal session. When you disconnect, the session ends, and everything running inside it gets killed. It's like unplugging a lamp from the wall.

What you need is something that runs your app independently of your terminal session — something that starts it automatically when the server boots, restarts it if it crashes, and keeps it alive 24/7.

On Linux, that something is called systemd.

What systemd actually is

Systemd is the program that manages all the background services on your Linux server. Your web server (Nginx, Apache), your database (Postgres, MySQL), your firewall — systemd is what starts them, monitors them, and restarts them if they go down.

When you want your app to behave like a real service — start on boot, survive disconnects, restart after crashes — you create a "unit file" that tells systemd how to run it. That file goes in /etc/systemd/system/ and looks something like this:

[Unit]
Description=My App
After=network.target

[Service]
Type=simple
User=deploy
WorkingDirectory=/home/deploy/myapp
ExecStart=/usr/bin/node server.js
Restart=on-failure

[Install]
WantedBy=multi-user.target

Simple enough on paper. In practice, this is where everything goes sideways.

The "it works when I run it manually" problem

This is the #1 source of confusion. You type node server.js in your terminal and it works perfectly. You put the same command in a systemd service file and it fails immediately.

Here's why: your terminal and systemd are completely different environments.

When you log in to your server, your terminal session has:

Systemd has none of that. It runs your app in a bare, stripped-down environment. No .bashrc. No nvm. No pyenv. If your app depends on any of those things — and it almost certainly does — it will fail.

The most common causes (and how to fix each one)

1. Wrong path to Node, Python, or your runtime

This is the single most common problem. If you installed Node through nvm, the actual binary isn't at /usr/bin/node — it's buried somewhere like /home/youruser/.nvm/versions/node/v20.11.0/bin/node. Same story with Python through pyenv.

Systemd doesn't know about nvm or pyenv. It doesn't run your .bashrc. When you tell it ExecStart=/usr/bin/node server.js and there's no node at /usr/bin/node, it fails.

Fix: Find the real path to your executable. On your server, run:

which node
# or
which python3

Use that full path in your service file. If which node says /home/deploy/.nvm/versions/node/v20.11.0/bin/node, then your ExecStart should be:

ExecStart=/home/deploy/.nvm/versions/node/v20.11.0/bin/node server.js

2. Missing environment variables

Your app probably needs environment variables — a database URL, API keys, a port number, NODE_ENV=production. You might have these in a .env file or set in your terminal. Systemd doesn't read any of that automatically.

Fix: Add them directly in the service file or point to a file:

# Option 1: Inline
[Service]
Environment=NODE_ENV=production
Environment=PORT=3000
Environment=DATABASE_URL=postgres://user:pass@localhost/mydb

# Option 2: From a file (cleaner for lots of variables)
[Service]
EnvironmentFile=/home/deploy/myapp/.env

Note: if you use EnvironmentFile, the file format must be KEY=value — no export, no quotes, no comments on the same line as a value.

3. Wrong or missing WorkingDirectory

Your app probably expects to run from a specific folder — where it can find its package.json, its config files, its node_modules. If you don't set WorkingDirectory in the service file, systemd runs your app from / (the root of the filesystem), and it can't find anything.

Fix: Always set WorkingDirectory to your app's folder:

WorkingDirectory=/home/deploy/myapp

4. Wrong User

If your service file says User=root but your app and its dependencies were installed as a regular user, things like npm packages or virtual environments won't be found. If you leave out User= entirely, systemd runs as root by default, which can cause both permission issues and security problems.

Fix: Set the User to whoever installed and owns the app:

User=deploy
Group=deploy

5. Your app crashes on startup and systemd gives up

Systemd is patient, but not infinitely patient. If your app crashes immediately after starting, systemd will try to restart it a few times (usually 5 times within 10 seconds), then give up and mark the service as failed. You'll see something like "start request repeated too quickly" in the logs.

The app is crashing for one of the reasons above (bad path, missing env vars, wrong directory), but systemd's rapid-restart-then-give-up behavior makes it look like a different problem.

Fix: Fix the underlying crash first. Then, if you want systemd to be more patient with restarts:

Restart=on-failure
RestartSec=5
StartLimitIntervalSec=60
StartLimitBurst=10

How to see what's actually going wrong

Systemd doesn't leave you guessing. It keeps logs. Two commands will tell you almost everything:

# See the current status and the last few log lines
systemctl status myapp

# See the full log output (jump to the end)
journalctl -u myapp -e

The output from journalctl will show you the actual error your app is throwing — "MODULE_NOT_FOUND," "ENOENT: no such file or directory," "Error: Cannot find module," "address already in use." That error message is the real problem. Everything else is symptoms.

One more trick: test the ExecStart command manually, as the same user systemd uses. If your service file says User=deploy, do this:

sudo -u deploy /home/deploy/.nvm/versions/node/v20.11.0/bin/node /home/deploy/myapp/server.js

If it fails here, it will fail in systemd too. If it works here but fails in systemd, the problem is an environment variable or a PATH issue that your shell session has but systemd doesn't.

Why AI generates bad service files

If you asked ChatGPT or Claude to write a systemd service file for your app, there's a good chance it gave you something that doesn't work. Here's why:

The AI gives you a template. Templates are starting points, not solutions. The details that make it work — the exact path to your runtime, your specific environment variables, your actual user and directory layout — are things only someone who can see your server can fill in correctly.

The pm2 alternative (for Node.js)

If you're running a Node.js app specifically, there's a simpler tool called pm2 that handles process management without writing service files:

npm install -g pm2
pm2 start server.js
pm2 startup    # generates the systemd service for you
pm2 save       # saves your process list

pm2 handles restarts, logs, environment variables, and even generates the correct systemd service file automatically. It's not the "right" way for every situation, but it eliminates most of the manual configuration that trips people up.

That said, pm2 is Node-specific. If you're running Python, Go, Rust, or anything else, systemd is the tool.

A working service file, explained

Here's what a real, working service file looks like for a Node.js app installed via nvm:

[Unit]
Description=My Node App
After=network.target

[Service]
Type=simple
User=deploy
Group=deploy
WorkingDirectory=/home/deploy/myapp
ExecStart=/home/deploy/.nvm/versions/node/v20.11.0/bin/node server.js
Restart=on-failure
RestartSec=5
Environment=NODE_ENV=production
Environment=PORT=3000
EnvironmentFile=/home/deploy/myapp/.env

[Install]
WantedBy=multi-user.target

After creating or editing the file, you need to tell systemd to reload and start it:

sudo systemctl daemon-reload
sudo systemctl enable myapp    # start on boot
sudo systemctl start myapp     # start now
systemctl status myapp         # check if it worked

If status shows green "active (running)" — you're done. If it shows red "failed" — go back to journalctl -u myapp -e and read the error.

Still failing after all that?

Some service failures are straightforward. Some are a tangled mess of permissions, paths, and environment issues that take an hour of back-and-forth with AI to go nowhere. Press the MeatButton and a real expert will look at your actual server, read the actual logs, and fix it. First one's free.

Get MeatButton