Skip to main content
โšก Calmops

Creating systemd Service Files on Ubuntu: Complete Guide

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

Comments