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-reloadafter 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.sh1-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
If EnvironmentFile= contains passwords, set the file permissions to 600 so only the service user can read it.
sudo chmod 600 /etc/myapp/env sudo chown root:root /etc/myapp/env
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