VPS Tutorials
What to Do When a systemd Service Fails to Start on a VPS (2026): systemctl status, journalctl -u, and Unit File Troubleshooting
When a service on a VPS fails to start with systemctl start, shows Active as failed, or keeps auto-restarting, common causes include Unit file syntax errors, wrong ExecStart paths, a missing WorkingDirectory, EnvironmentFile read failures, insufficient permissions for the run user, port conflicts, dependency services not started, or StartLimitHit. This article troubleshoots layer by layer using systemctl status, journalctl -u, systemctl cat, daemon-reload, permissions, and restart loops.

When a service fails to start on a VPS, many people just keep running:
sudo systemctl restart app
If the problem is that the configuration wasn't loaded, a path is wrong, permissions are insufficient, or the port is occupied, restarting a hundred times won't help. Worse, if the Unit is configured with Restart=always, the service may enter a restart loop, flooding the logs and slowing down CPU and disk.
This article is specifically about how to investigate a systemd service that fails to start. It isn't troubleshooting for one particular piece of software like Nginx, MySQL, or Redis—it's the foundational problem every self-hosted service runs into: how to read systemctl status, which lines to look at in journalctl -u, how to confirm the Unit file, when you need daemon-reload, and how to handle Start request repeated too quickly.
First look at the service status:
sudo systemctl status app --no-pager
Replace app with your service name, for example:
sudo systemctl status nginx --no-pager
sudo systemctl status docker --no-pager
sudo systemctl status myapp --no-pager
Focus on these fields:
Loaded:whether the Unit file exists and whether it's enabled;Active:whether it'sactive,failed,activating, orauto-restart;Main PID:whether the main process is still running;ExecStart=whether the command after it actually executed;code=exited, status=...what the exit code is;- whether the last few lines of logs contain a clear error.
If you only see a short snippet of logs, that's not enough. Next, look at the full journal.
The most common command:
sudo journalctl -u app -n 120 --no-pager
To see the last hour:
sudo journalctl -u app --since "1 hour ago" --no-pager
To follow in real time:
sudo journalctl -u app -f
If it just failed to start, you can reproduce it and immediately check the logs like this:
sudo systemctl restart app
sudo journalctl -u app -n 120 --no-pager
Don't just look at the last line. The real cause is often earlier, for example:
No such file or directory
Permission denied
Address already in use
Failed at step USER
Failed to load environment files
Working directory ... does not exist
Start request repeated too quickly
If you're not yet familiar with journalctl, you can first read this article: How to Read VPS Logs (journalctl, systemctl, Nginx, application logs).
Many people edit the wrong file.
First look at the Unit content systemd actually sees:
sudo systemctl cat app
It shows the main Unit file and drop-in override configurations, for example:
# /etc/systemd/system/app.service
[Service]
WorkingDirectory=/var/www/app
ExecStart=/usr/bin/node server.js
# /etc/systemd/system/app.service.d/override.conf
[Service]
Environment=NODE_ENV=production
If you clearly changed /etc/systemd/system/app.service but systemctl cat app shows no change, it's usually because you forgot:
sudo systemctl daemon-reload
Recommended order after changing a Unit file:
sudo systemctl daemon-reload
sudo systemctl restart app
sudo systemctl status app --no-pager
Changing only the application config file doesn't necessarily require daemon-reload; changing .service, .timer, or a drop-in override usually does.
In the Unit, ExecStart is best written as an absolute path:
[Service]
ExecStart=/usr/bin/node /var/www/app/server.js
Don't rely on the PATH, nvm, pyenv, or conda from your login shell. When systemd starts a service, the environment is different from after you log in via SSH.
First check the command path:
which node
which python3
which php
which gunicorn
If you installed Node with nvm, which node may be under the user's home directory:
/home/deploy/.nvm/versions/node/v20.11.1/bin/node
This kind of path also works in systemd, but you need to confirm the run user has permission, and don't assume .bashrc will be loaded. A more robust approach is to pin the runtime environment to an explicit path, or write a wrapper script.
Many applications start fine manually but fail under systemd because the working directory is different.
Common configuration:
[Service]
WorkingDirectory=/var/www/app
ExecStart=/usr/bin/npm start
Check the directory:
ls -ld /var/www/app
If the logs contain:
CHDIR spawning ... No such file or directory
Working directory ... does not exist
it means the WorkingDirectory path is wrong, or the directory was deleted.
If the directory exists but the service user can't enter it, it will also fail. For example, if the service uses User=www-data, test with that user:
sudo -u www-data test -x /var/www/app
sudo -u www-data ls /var/www/app
Common Unit configuration:
[Service]
User=deploy
Group=deploy
If the logs show:
Failed at step USER spawning ...: No such process
it means the user doesn't exist or systemd can't switch to that user.
Check the user:
id deploy
If it's a file permission issue, the logs may be:
Permission denied
Manually run the start command as the target user:
sudo -u deploy /usr/bin/node /var/www/app/server.js
This is more informative than running manually as root. Just because root can run it doesn't mean deploy, www-data, postgres, or redis can.
Many services are configured like this:
[Service]
EnvironmentFile=/etc/app/app.env
ExecStart=/usr/bin/node /var/www/app/server.js
If the file doesn't exist, the logs may show:
Failed to load environment files: No such file or directory
Check:
sudo ls -l /etc/app/app.env
sudo systemctl cat app
If this environment file isn't required, you can add - so it doesn't throw a hard error when missing:
EnvironmentFile=-/etc/app/app.env
But don't just add - to "make it start." If the application depends on a database address, secret, or port configuration, a missing environment file will only make the error more hidden.
Also note the environment file format—it's not a full shell script. Common format:
NODE_ENV=production
PORT=3000
DATABASE_URL=postgres://user:[email protected]:5432/app
Don't write complex shell logic in it.
If the logs contain:
Address already in use
EADDRINUSE
bind: address already in use
first check who's occupying the port:
sudo ss -lntup
To check only a specific port:
sudo ss -lntup | grep ':3000'
Common causes:
- An old process didn't exit;
- The same service was started twice;
- A Docker container is occupying the port;
- Nginx / Caddy / Apache is competing with the application for 80/443;
- Multiple services in the config are set to the same port.
Don't just kill -9. First confirm the process source:
ps -fp PID
systemctl status PID对应服务
If a port problem causes a website 502, you can read: What to Do About 502 / 504 Bad Gateway on a VPS.
Common in Units:
[Unit]
After=network.target postgresql.service
Requires=postgresql.service
This only guarantees ordering and dependency relationships; it doesn't mean PostgreSQL can already accept connections, nor that a remote database on the network is reachable.
If the application logs show:
connection refused
connection timed out
could not connect to server
check the dependency services as well:
sudo systemctl status postgresql --no-pager
sudo ss -lntup | grep ':5432'
If it's Docker, Redis, MySQL, or a remote API, also confirm reachability separately. Don't blame all dependency problems on systemd.
If you see:
Start request repeated too quickly
Failed with result 'start-limit-hit'
it means the service failed too many times in a short period and systemd has paused starting it.
First look at the failure reason:
sudo journalctl -u app -n 200 --no-pager
After fixing the root cause, reset the failure state:
sudo systemctl reset-failed app
sudo systemctl start app
If the Unit contains:
[Service]
Restart=always
RestartSec=1
the service will restart very frequently. In production you can increase it appropriately:
[Service]
Restart=on-failure
RestartSec=5s
Don't use a more aggressive restart policy to mask startup failures. It only makes the logs messier.
Common types:
Type=simple
Type=exec
Type=forking
Type=oneshot
For most applications that run in the foreground, simple or exec is enough. Don't write a foreground process as forking.
If the service exits immediately after starting but it's a one-shot task, you should consider:
[Service]
Type=oneshot
RemainAfterExit=yes
If it's a long-running service, the process exiting means the application itself ended—look at the application logs instead of disguising it as success.
It's recommended to follow this order every time you change a systemd Unit:
sudo systemd-analyze verify /etc/systemd/system/app.service
sudo systemctl daemon-reload
sudo systemctl restart app
sudo systemctl status app --no-pager
sudo journalctl -u app -n 80 --no-pager
systemd-analyze verify can catch some Unit syntax and configuration problems in advance. It can't guarantee the application will start, but it can avoid low-level Unit errors.
If the online service is important, back it up before changing:
sudo cp /etc/systemd/system/app.service /etc/systemd/system/app.service.bak.$(date +%F-%H%M)
To roll back:
sudo cp /etc/systemd/system/app.service.bak.时间戳 /etc/systemd/system/app.service
sudo systemctl daemon-reload
sudo systemctl restart app
When you encounter systemctl start or restart failing, check in this order:
systemctl status app --no-pagerto see Active, exit code, and the last logs.journalctl -u app -n 120 --no-pagerto see the full failure reason.systemctl cat appto confirm the actually loaded Unit and override.- After changing the Unit, run
systemctl daemon-reload. - Check whether
ExecStartis an absolute path and whether the command exists. - Check whether
WorkingDirectoryexists and whether the service user can access it. - Check whether
User/Groupexist and whether permissions are correct. - Check whether
EnvironmentFileexists and whether the format is correct. - Use
ss -lntupto check whether the port is occupied. - Check whether dependency services are started and truly connectable.
- When you encounter
start-limit-hit, fix the root cause first, thensystemctl reset-failed app. - Back up the Unit before modifying, and use
systemd-analyze verifyfor a basic check after modifying.
When a systemd service fails to start, don't rely on repeated restarts and luck. First use systemctl status to determine the state, then use journalctl -u to find the first real error, then look at systemctl cat to confirm whether the Unit is loaded as you intended.
Most problems ultimately fall into a few categories: wrong path, insufficient permissions, missing environment file, port conflict, dependency not up, Unit changed but no daemon-reload, or the service entered StartLimitHit. Troubleshooting in order is much faster than blindly changing configuration, and it won't make the situation messier the more you change.