How to Write systemd Unit Files - Registering Custom Services

How to Write systemd Unit Files - Registering Custom Services

What Is a systemd Unit File?

A systemd unit file is a configuration file that registers services, mount points, timers, and other resources with the system. By placing a .service file in /etc/systemd/system/, you can manage any script or application as a daemon with full lifecycle control.

Quick Summary

  • Unit file location: /etc/systemd/system/myapp.service
  • Three sections: [Unit] / [Service] / [Install]
  • Always run systemctl daemon-reload after creating or modifying a unit file

Unit File Structure

A unit file has three sections:

[Unit]
Description=My Custom Service
After=network.target

[Service]
ExecStart=/usr/local/bin/myapp
Restart=on-failure
User=myuser

[Install]
WantedBy=multi-user.target
Section Role
[Unit] Description, startup order, dependencies
[Service] Start command, restart policy, execution user
[Install] Target for systemctl enable

1. Register a Custom Service with a Minimal Unit File

The fastest way to turn a shell script into a managed service.

1-1. Create the Script

sudo tee /usr/local/bin/myapp.sh > /dev/null << 'EOF'
#!/bin/bash
while true; do
    echo "$(date) running" >> /var/log/myapp.log
    sleep 10
done
EOF
sudo chmod +x /usr/local/bin/myapp.sh

1-2. Create the Unit File

sudo tee /etc/systemd/system/myapp.service > /dev/null << 'EOF'
[Unit]
Description=My Application
After=network.target

[Service]
ExecStart=/usr/local/bin/myapp.sh
Restart=on-failure

[Install]
WantedBy=multi-user.target
EOF

1-3. Reload, Enable, and Start

sudo systemctl daemon-reload
sudo systemctl enable --now myapp
sudo systemctl status myapp
● myapp.service - My Application
     Loaded: loaded (/etc/systemd/system/myapp.service; enabled)
     Active: active (running) since Sun 2026-05-31 12:00:00 UTC; 3s ago
   Main PID: 12345 (myapp.sh)

Forgetting daemon-reload causes "Unit not found" errors or leaves the old configuration active. Run it every time you modify a unit file.

2. [Unit] Section — Setting Startup Order and Dependencies

[Unit] controls service metadata and startup ordering.

Directive Meaning
Description= Human-readable description (shown in systemctl status)
After= Start this service after the specified units (ordering only)
Requires= If the specified unit fails, stop this service too
Wants= Try to start the specified unit, but continue even if it fails
ConditionPathExists= Only start if the specified file/directory exists
[Unit]
Description=Web Application Backend
After=network.target postgresql.service
Wants=postgresql.service

After=network.target is the standard pattern for services that need network access. Add After=postgresql.service for database-dependent services. Note that After= controls ordering, not dependency enforcement — use Requires= for hard dependencies.

3. [Service] Section — Startup Command and Behavior

The most configuration-dense section. These are the key directives.

Choosing Type=

Type= defines how systemd tracks the process state.

Type Use Case
simple (default) A process that stays in the foreground
forking A process that daemonizes itself (forks)
oneshot A script that runs once and exits
notify A process that signals readiness with READY=1

For most use cases, simple or oneshot is the right choice.

Key Directives

[Service]
Type=simple
ExecStart=/usr/local/bin/myapp --config /etc/myapp/config.yml
ExecStop=/bin/kill -TERM $MAINPID
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
User=www-data
Group=www-data
WorkingDirectory=/var/lib/myapp
EnvironmentFile=/etc/myapp/env
Directive Meaning
ExecStart= Start command (absolute path required)
ExecStop= Stop command (defaults to SIGTERM if omitted)
ExecReload= Reload command
Restart= Restart policy: no / on-failure / always
RestartSec= Seconds to wait before restarting
User= / Group= User and group to run as
WorkingDirectory= Working directory
EnvironmentFile= Path to a file containing environment variables

ExecStart= requires an absolute path — either the binary directly or a shell with an absolute path: /bin/bash /path/to/script.sh. Relative paths like ./script.sh do not work.

Passing Environment Variables

[Service]
Environment="NODE_ENV=production"
Environment="PORT=3000"
EnvironmentFile=/etc/myapp/env

Example /etc/myapp/env:

DB_HOST=localhost
DB_PORT=5432
SECRET_KEY=changeme

4. [Install] Section — Controlling enable Behavior

This section is read when you run systemctl enable.

[Install]
WantedBy=multi-user.target

WantedBy=multi-user.target is the standard choice. Running systemctl enable myapp creates a symlink at /etc/systemd/system/multi-user.target.wants/myapp.service, which triggers automatic startup at boot.

Value Use Case
multi-user.target Standard multi-user mode (typical for servers)
graphical.target Services that require a graphical environment
network-online.target Start only after network is fully online

5. Service Management Commands

Common operations after creating a unit file:

# Reload unit files (required after any change)
sudo systemctl daemon-reload

# Enable (auto-start at boot) + start immediately
sudo systemctl enable --now myapp

# Check status
sudo systemctl status myapp

# Restart
sudo systemctl restart myapp

# Reload configuration (requires ExecReload)
sudo systemctl reload myapp

# Stop
sudo systemctl stop myapp

# Disable auto-start
sudo systemctl disable myapp

# View recent logs (last 50 lines)
journalctl -u myapp -n 50 --no-pager

6. Common Failures and How to Fix Them

ExecStart Path Not Found

myapp.service: control process exited with error code
ExecStart=/usr/local/bin/myapp (code=exited, status=203/EXEC)

Fix: verify the full path.

which myapp
ls -la /usr/local/bin/myapp

Forgot daemon-reload

Changes to the unit file are not reflected, or "Unit not found" appears.

sudo systemctl daemon-reload
sudo systemctl restart myapp

Permission Denied

journalctl -u myapp -n 20
# myapp.sh: Permission denied

Fix: check execute permission.

ls -la /usr/local/bin/myapp.sh
# If missing execute permission:
sudo chmod +x /usr/local/bin/myapp.sh

If User= is set, also verify that the specified user can read the file.

EnvironmentFile Not Found

If EnvironmentFile= points to a missing file, the service will fail to start. To allow startup even when the file is absent, prefix the path with -:

EnvironmentFile=-/etc/myapp/env

Full Workflow Cheatsheet

# 1. Create the script
sudo vim /usr/local/bin/myapp.sh
sudo chmod +x /usr/local/bin/myapp.sh

# 2. Create the unit file
sudo vim /etc/systemd/system/myapp.service

# 3. Register and start
sudo systemctl daemon-reload
sudo systemctl enable --now myapp

# 4. Verify
sudo systemctl status myapp
journalctl -u myapp -f

Next Reading