If you’re running a Node.js app, a Python script, a Go binary, or anything else that needs to stay alive, don’t use screen or nohup. Create a systemd service. This applies to any GoZen VPS or dedicated server. It starts on boot, restarts on crash, logs properly, and gives you systemctl start/stop/restart like any other service.

The Basics

Systemd service files live in /etc/systemd/system/. Each file describes one service - what to run, as which user, when to start, and what to do if it crashes.

Minimal Service File

  sudo nano /etc/systemd/system/myapp.service
  
  [Unit]
Description=My Web Application
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
  
  # Reload systemd so it picks up the new file
sudo systemctl daemon-reload

# Start the service
sudo systemctl start myapp

# Enable it on boot
sudo systemctl enable myapp

# Check status
sudo systemctl status myapp
  

That’s it. Your app is now a proper service.

Real-World Examples

Node.js App

  [Unit]
Description=Node.js API Server
After=network.target

[Service]
Type=simple
User=deploy
Group=deploy
WorkingDirectory=/home/deploy/api
ExecStart=/usr/bin/node dist/index.js
Restart=always
RestartSec=5
Environment=NODE_ENV=production
Environment=PORT=3000

# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=node-api

# Security
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/home/deploy/api/logs /home/deploy/api/uploads

[Install]
WantedBy=multi-user.target
  

Python App (with virtualenv)

  [Unit]
Description=Python Flask Application
After=network.target

[Service]
Type=simple
User=deploy
Group=deploy
WorkingDirectory=/home/deploy/webapp
ExecStart=/home/deploy/webapp/venv/bin/gunicorn --workers 3 --bind 0.0.0.0:8000 app:app
Restart=always
RestartSec=5
Environment=FLASK_ENV=production

StandardOutput=journal
StandardError=journal
SyslogIdentifier=flask-app

[Install]
WantedBy=multi-user.target
  

Go Binary

  [Unit]
Description=Go Microservice
After=network.target

[Service]
Type=simple
User=deploy
Group=deploy
ExecStart=/home/deploy/goapp/server
Restart=always
RestartSec=3
Environment=APP_PORT=8080
Environment=APP_LOG_LEVEL=info

StandardOutput=journal
StandardError=journal
SyslogIdentifier=goapp

NoNewPrivileges=true

[Install]
WantedBy=multi-user.target
  

Key Directives Explained

[Unit] Section

DirectiveWhat It Does
Description=Human-readable name (shows in systemctl status)
After=network.targetWait for networking before starting
After=mysql.serviceWait for MySQL to start first
Requires=mysql.serviceWon’t start unless MySQL is running
Wants=redis.serviceStarts Redis too, but doesn’t fail if Redis fails

[Service] Section

DirectiveWhat It Does
Type=simpleThe default. Your command is the main process.
Type=forkingFor daemons that fork (background themselves). Use PIDFile= with this.
User= / Group=Run as this user. Never use root unless absolutely necessary.
WorkingDirectory=cd to this directory before running ExecStart
ExecStart=The command to run. Must be an absolute path.
ExecStartPre=Run before the main command (e.g., run migrations)
ExecStop=Custom stop command (default: sends SIGTERM)
Restart=alwaysRestart on any exit (crash, signal, clean exit)
Restart=on-failureOnly restart on non-zero exit or signal
RestartSec=5Wait 5 seconds between restarts
Environment=Set environment variables
EnvironmentFile=Load env vars from a file (great for secrets)

[Install] Section

DirectiveWhat It Does
WantedBy=multi-user.targetStart in normal multi-user mode (the standard for servers)

Using Environment Files

Don’t put secrets in the service file. Use an environment file instead:

  sudo nano /home/deploy/myapp/.env
  
  DATABASE_URL=mysql://user:password@localhost:3306/mydb
SECRET_KEY=your-secret-key-here
REDIS_URL=redis://localhost:6379
  
  # Restrict permissions
sudo chmod 600 /home/deploy/myapp/.env
sudo chown deploy:deploy /home/deploy/myapp/.env
  

Reference it in the service file:

  [Service]
EnvironmentFile=/home/deploy/myapp/.env
  

Managing Your Service

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

# Reload without full restart (if your app supports it)
sudo systemctl reload myapp

# Check status
sudo systemctl status myapp

# View logs
journalctl -u myapp -f              # follow live
journalctl -u myapp --since "1 hour ago"
journalctl -u myapp -p err          # errors only

# Enable / disable on boot
sudo systemctl enable myapp
sudo systemctl disable myapp

# After editing the service file
sudo systemctl daemon-reload
sudo systemctl restart myapp
  

Restart Limits

By default, systemd stops restarting a service if it crashes too many times in a short period (5 times in 10 seconds). You can adjust this:

  [Service]
Restart=always
RestartSec=5

# Allow up to 10 restarts in 60 seconds before giving up
StartLimitIntervalSec=60
StartLimitBurst=10
  

If systemd stops restarting your service, you’ll see “start request repeated too quickly” in the logs. Fix the underlying crash, then:

  sudo systemctl reset-failed myapp
sudo systemctl start myapp
  

Security Hardening

Systemd offers process isolation features. Use them - they cost nothing and reduce the blast radius if your app gets compromised:

  [Service]
# Drop all capabilities except what's needed
NoNewPrivileges=true

# Read-only filesystem (except specified paths)
ProtectSystem=strict
ReadWritePaths=/home/deploy/myapp/data /home/deploy/myapp/logs

# Isolate from home directories
ProtectHome=read-only

# No access to /tmp (gets its own tmp)
PrivateTmp=true

# Can't modify kernel variables
ProtectKernelTunables=true

# Can't load kernel modules
ProtectKernelModules=true

# Restrict network protocols
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
  

Test after adding hardening options - some apps need access to directories you’ve just restricted.

Troubleshooting

ProblemFix
“Failed to start” - exit code 217User doesn’t exist. Check the User= line.
“Failed to start” - exit code 203ExecStart binary not found. Use absolute paths (/usr/bin/node, not node). Find it with which node.
Service starts then immediately stopsCheck logs: journalctl -u myapp -n 50. Usually a crash in the app itself, not a systemd issue.
“Start request repeated too quickly”The app is crash-looping. Fix the app bug, then systemctl reset-failed myapp.
Environment variables not loadingUse EnvironmentFile= with absolute path. The file must be readable by root (systemd reads it before switching users).
Service runs fine manually but fails in systemdYour shell has PATH and env vars that systemd doesn’t. Use full paths in ExecStart and set all env vars explicitly.
“Permission denied” on filesCheck User= and Group=. The service runs as that user, not as you. Fix ownership: chown deploy:deploy /path/to/files.
Can’t bind to port 80/443Ports below 1024 need root or CAP_NET_BIND_SERVICE. Better approach: run on 3000+ and use Nginx as a reverse proxy.

Last updated 21 Apr 2026, 08:08 +0300. history

Was this page helpful?