Introduction
systemd is the init system on all modern Ubuntu and Debian systems. Creating a .service unit file is the standard way to run any program as a system service โ starting it automatically at boot, restarting it on failure, and managing it with systemctl.
Prerequisites: Ubuntu 18.04+ (or any systemd-based Linux), sudo access.
Step 1: Create Your Program
Your program must run in the foreground (not daemonize itself). systemd manages the process lifecycle.
# Example: a simple shell script that runs continuously
cat > /data/myapp/server.sh << 'EOF'
#!/bin/bash
while true; do
echo "$(date): Server running"
sleep 5
done
EOF
chmod +x /data/myapp/server.sh
For a Go/Python/Node.js binary, just ensure it doesn’t fork itself into the background.
Step 2: Create the Service File
Service files live in /etc/systemd/system/. The filename becomes the service name.
sudo nano /etc/systemd/system/myapp.service
Minimal Service File
[Unit]
Description=My Application
[Service]
ExecStart=/data/myapp/server.sh
[Install]
WantedBy=multi-user.target
This is enough to get started. Add more options as needed.
Full Production Service File
[Unit]
Description=My Application Server
Documentation=https://github.com/myorg/myapp
# Start after network is available
After=network-online.target
Wants=network-online.target
# If this service depends on another
# After=postgresql.service
# Requires=postgresql.service
[Service]
# Type of service
Type=simple # default: process stays in foreground
# Type=forking # process forks (old-style daemons)
# Type=oneshot # runs once and exits
# Type=notify # process signals systemd when ready
# Run as this user/group (not root unless necessary)
User=ubuntu
Group=ubuntu
# Working directory
WorkingDirectory=/data/myapp
# The main command
ExecStart=/data/myapp/server
# Optional: pre-start and post-stop commands
# ExecStartPre=/data/myapp/pre-start.sh
# ExecStopPost=/data/myapp/cleanup.sh
# Restart policy
Restart=always # always restart on exit
# Restart=on-failure # only restart on non-zero exit
RestartSec=5s # wait 5 seconds before restarting
# Environment variables
Environment=APP_ENV=production
Environment=PORT=8080
# Or load from a file:
EnvironmentFile=/data/myapp/.env
# Resource limits
LimitNOFILE=65536 # max open files
# LimitNPROC=512 # max processes
# Logging
StandardOutput=journal
StandardError=journal
# Security hardening (optional but recommended)
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
Step 3: Enable and Start the Service
# Reload systemd to pick up the new file
sudo systemctl daemon-reload
# Enable: start automatically at boot
sudo systemctl enable myapp.service
# Start now
sudo systemctl start myapp.service
# Check status
sudo systemctl status myapp.service
Expected output:
โ myapp.service - My Application Server
Loaded: loaded (/etc/systemd/system/myapp.service; enabled)
Active: active (running) since Mon 2026-03-30 10:00:00 UTC; 5s ago
Main PID: 12345 (server)
Tasks: 1 (limit: 4915)
Memory: 12.3M
CPU: 45ms
CGroup: /system.slice/myapp.service
โโ12345 /data/myapp/server
Step 4: View Logs
systemd captures stdout/stderr automatically:
# View all logs for the service
sudo journalctl -u myapp.service
# Follow logs in real-time
sudo journalctl -u myapp.service -f
# Last 50 lines
sudo journalctl -u myapp.service -n 50
# Since last boot
sudo journalctl -u myapp.service -b
# Since a specific time
sudo journalctl -u myapp.service --since "2026-03-30 10:00:00"
sudo journalctl -u myapp.service --since "1 hour ago"
Managing the Service
# Start / stop / restart
sudo systemctl start myapp
sudo systemctl stop myapp
sudo systemctl restart myapp
# Reload config without full restart (if app supports it)
sudo systemctl reload myapp
# Enable / disable autostart
sudo systemctl enable myapp
sudo systemctl disable myapp
# Check if enabled
sudo systemctl is-enabled myapp
# Check if running
sudo systemctl is-active myapp
# Show all services
sudo systemctl list-units --type=service
sudo systemctl list-units --type=service --state=failed
Environment Variables
systemd does NOT inherit your shell’s environment variables. You must specify them explicitly:
[Service]
# Inline
Environment=DATABASE_URL=postgres://localhost/mydb
Environment=SECRET_KEY=abc123
# Multiple on one line
Environment="KEY1=value1" "KEY2=value2"
# From a file (one KEY=value per line, no export keyword)
EnvironmentFile=/etc/myapp/env
EnvironmentFile=-/etc/myapp/env.local # the - means ignore if missing
# /etc/myapp/env
DATABASE_URL=postgres://localhost/mydb
SECRET_KEY=abc123
PORT=8080
Common mistake: Using export KEY=value in the EnvironmentFile โ systemd doesn’t understand export.
Service Types
| Type | Use When |
|---|---|
simple |
Process stays in foreground (default, most common) |
forking |
Process forks to background (old-style daemons) |
oneshot |
Runs once and exits (scripts, migrations) |
notify |
Process signals systemd when ready (advanced) |
idle |
Like simple, but waits for other jobs to finish |
For oneshot services (run once at boot):
[Service]
Type=oneshot
ExecStart=/data/scripts/setup.sh
RemainAfterExit=yes # show as "active" after script exits
User Services (No sudo Required)
For per-user services that start when you log in:
# Place in user directory
mkdir -p ~/.config/systemd/user/
nano ~/.config/systemd/user/myapp.service
[Unit]
Description=My User Application
[Service]
ExecStart=%h/bin/myapp # %h = home directory
Restart=on-failure
[Install]
WantedBy=default.target
# Manage without sudo
systemctl --user daemon-reload
systemctl --user enable myapp
systemctl --user start myapp
systemctl --user status myapp
journalctl --user -u myapp -f
# Enable lingering (keep running after logout)
loginctl enable-linger $USER
Troubleshooting
# Service failed to start โ check the error
sudo systemctl status myapp.service
sudo journalctl -u myapp.service -n 50
# Common issues:
# 1. Script not executable
chmod +x /data/myapp/server.sh
# 2. Wrong path (use absolute paths in ExecStart)
which myapp # find the full path
# 3. Permission denied
# Check User= in [Service] has access to the files
# 4. Environment variable missing
# Add to EnvironmentFile or Environment= in [Service]
# 5. Port already in use
ss -tlnp | grep :8080
# Validate the service file syntax
systemd-analyze verify /etc/systemd/system/myapp.service
Complete Example: Go Web Server
# /etc/systemd/system/api-server.service
[Unit]
Description=API Server
After=network-online.target postgresql.service
Wants=network-online.target
[Service]
Type=simple
User=api
Group=api
WorkingDirectory=/opt/api-server
ExecStart=/opt/api-server/api-server
Restart=on-failure
RestartSec=5s
EnvironmentFile=/opt/api-server/.env
StandardOutput=journal
StandardError=journal
NoNewPrivileges=true
PrivateTmp=true
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
Resources
- systemd Service Documentation
- systemd Unit Files
- Ubuntu systemd Guide
- DigitalOcean: systemd Essentials
Comments