Create and Manage systemd Service Files: Complete Guide (2026)
Last updated: March 2026
systemd is the init system and service manager used by virtually every major Linux distribution — Ubuntu, Debian, RHEL, CentOS, Fedora, Arch, and more. It manages services as "units" defined in plain-text files called unit files. Writing your own service file lets you run any application as a managed daemon: it starts on boot, restarts on failure, logs to journald, and integrates with systemctl. This guide covers every section of a service unit file with a complete working example: a Python Flask application running as a system service.
Unit File Location
| Location | Purpose |
|---|---|
/etc/systemd/system/ |
Administrator-defined services. Files here take precedence. Use this for your services. |
/lib/systemd/system/ or /usr/lib/systemd/system/ |
Package-installed services. Do not edit these directly. |
/run/systemd/system/ |
Runtime units, generated dynamically. |
~/.config/systemd/user/ |
Per-user units (user services, not system-wide). |
For system-wide services (started at boot, not tied to a logged-in user), place your file in /etc/systemd/system/.
Unit File Structure
A unit file has three main sections for service units:
[Unit]
# Metadata and dependencies
[Service]
# How to start, stop, and manage the service
[Install]
# How to enable the unit (what targets it belongs to)
[Unit] Section
[Unit]
Description=My Flask Web Application
Documentation=https://example.com/docs
After=network.target postgresql.service
Wants=postgresql.service
Requires=network.target
Key directives
Description: A human-readable description shown by systemctl status. Keep it concise.
Documentation: URL or man page reference. Optional but good practice.
After: Start this unit after the listed units. Does not create a dependency — if the listed unit is not active, this unit still starts. network.target is the most common entry, ensuring networking is up before your service.
Wants: Weak dependency. systemd tries to start the listed units alongside this one, but if they fail, this unit still starts. Use for non-critical dependencies.
Requires: Strong dependency. If the listed unit fails to start, this unit is also failed. If the required unit stops, this unit stops. Use sparingly — prefer Wants for most dependencies.
BindsTo: Stricter than Requires. If the bound unit stops, this unit stops immediately, even if it was running fine.
PartOf: This unit is part of another; stopping the other stops this one.
[Service] Section
[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/venv/bin/gunicorn -w 4 -b 0.0.0.0:8000 myapp:app
ExecStop=/bin/kill -s QUIT $MAINPID
ExecReload=/bin/kill -s HUP $MAINPID
Restart=on-failure
RestartSec=5
Environment=PYTHONUNBUFFERED=1
EnvironmentFile=/etc/myapp/environment
StandardOutput=journal
StandardError=journal
Type
Controls how systemd determines the service is ready:
| Type | Behavior |
|---|---|
simple |
Default. The process started by ExecStart is the main process. systemd considers it ready immediately after forking. |
exec |
Like simple, but systemd waits until the binary is actually executing (past the fork). |
forking |
The process forks and the parent exits. The child is the daemon. Requires PIDFile=. Used for traditional daemons. |
oneshot |
Runs once and exits. systemd waits for exit before considering the unit active. Use for tasks, not persistent daemons. |
notify |
The process sends a notification to systemd (via sd_notify) when ready. Most precise readiness signaling. |
dbus |
Ready when a specific D-Bus name is acquired. |
Use simple for modern applications. Use forking only for legacy daemons that daemonize themselves.
User and Group
Run the service as a specific user rather than root:
User=myapp
Group=myapp
Create a dedicated system user:
sudo useradd --system --no-create-home --shell /bin/false myapp
Never run services as root unless absolutely necessary.
WorkingDirectory
The working directory for the process:
WorkingDirectory=/opt/myapp
ExecStart, ExecStop, ExecReload
ExecStart: The command to start the service. Must use an absolute path:
ExecStart=/usr/bin/python3 /opt/myapp/app.py
ExecStart=/opt/myapp/venv/bin/gunicorn -w 4 myapp:app
ExecStop: How to stop the service gracefully. If omitted, systemd sends SIGTERM to the main process.
ExecReload: How to reload configuration without full restart. Often sends SIGHUP:
ExecReload=/bin/kill -s HUP $MAINPID
ExecStartPre and ExecStartPost: Commands to run before/after the main start:
ExecStartPre=/opt/myapp/scripts/check-db.sh
ExecStartPost=/bin/sleep 2
Restart and RestartSec
Restart: When to automatically restart:
| Value | Restart when |
|---|---|
no |
Never (default) |
on-success |
Exit code 0 |
on-failure |
Non-zero exit, signal, timeout |
on-abnormal |
Signal, timeout, watchdog |
always |
Any exit |
unless-stopped |
Any exit except systemctl stop |
For web services and daemons, on-failure or always is typical.
RestartSec: How long to wait before restarting. Default is 100ms:
Restart=on-failure
RestartSec=5s
Environment and EnvironmentFile
Environment: Set environment variables inline:
Environment=PYTHONUNBUFFERED=1
Environment=PORT=8000
Environment="DB_HOST=localhost"
EnvironmentFile: Load environment variables from a file (one KEY=VALUE per line):
EnvironmentFile=/etc/myapp/environment
EnvironmentFile=-/etc/myapp/optional.env
The - prefix makes the file optional — if it does not exist, no error is raised.
Example /etc/myapp/environment:
DATABASE_URL=postgresql://myapp:secret@localhost/myapp_db
SECRET_KEY=super-secret-production-key
DEBUG=false
PORT=8000
Set permissions to protect secrets:
sudo chmod 600 /etc/myapp/environment
sudo chown myapp:myapp /etc/myapp/environment
Security Hardening Directives
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/lib/myapp /var/log/myapp
These isolate the service from the rest of the system. PrivateTmp=yes is a quick win — gives the service its own /tmp namespace.
[Install] Section
[Install]
WantedBy=multi-user.target
WantedBy: Defines what target this service is added to when enabled. multi-user.target is the standard for services that should start at boot in a non-graphical environment (equivalent to runlevel 3).
Other common values:
- graphical.target: For services that need a desktop environment.
- network-online.target: For services that need a working network connection (stronger than network.target).
Complete Example: Python Flask App as a Service
Application setup
# Create user
sudo useradd --system --no-create-home --shell /bin/false flaskapp
# Create directory structure
sudo mkdir -p /opt/flaskapp
sudo chown flaskapp:flaskapp /opt/flaskapp
# Install the app (as flaskapp user)
sudo -u flaskapp python3 -m venv /opt/flaskapp/venv
sudo -u flaskapp /opt/flaskapp/venv/bin/pip install gunicorn flask
# Create environment file
sudo mkdir -p /etc/flaskapp
sudo tee /etc/flaskapp/environment <<EOF
FLASK_ENV=production
SECRET_KEY=changeme
DATABASE_URL=sqlite:////opt/flaskapp/app.db
EOF
sudo chmod 600 /etc/flaskapp/environment
sudo chown flaskapp:flaskapp /etc/flaskapp/environment
The service file
Create /etc/systemd/system/flaskapp.service:
[Unit]
Description=Flask Web Application
After=network.target
Wants=network.target
[Service]
Type=simple
User=flaskapp
Group=flaskapp
WorkingDirectory=/opt/flaskapp
EnvironmentFile=/etc/flaskapp/environment
ExecStart=/opt/flaskapp/venv/bin/gunicorn \
--workers 4 \
--bind 0.0.0.0:8000 \
--timeout 30 \
--access-logfile - \
--error-logfile - \
app:app
ExecReload=/bin/kill -s HUP $MAINPID
Restart=on-failure
RestartSec=5s
PrivateTmp=yes
NoNewPrivileges=yes
[Install]
WantedBy=multi-user.target
Load and start the service
# Reload systemd to pick up the new file
sudo systemctl daemon-reload
# Enable to start on boot
sudo systemctl enable flaskapp
# Start immediately
sudo systemctl start flaskapp
# Check status
sudo systemctl status flaskapp
systemctl daemon-reload
Any time you create or modify a unit file, you must reload the systemd manager:
sudo systemctl daemon-reload
Without this, systemd does not see your changes. You do not need to restart the service itself — daemon-reload only reloads the configuration files.
Managing Services with systemctl
# Start
sudo systemctl start flaskapp
# Stop
sudo systemctl stop flaskapp
# Restart (stop + start)
sudo systemctl restart flaskapp
# Reload configuration without restart (if ExecReload is defined)
sudo systemctl reload flaskapp
# Status (shows state, recent logs, PID)
sudo systemctl status flaskapp
# Enable on boot
sudo systemctl enable flaskapp
# Disable on boot
sudo systemctl disable flaskapp
# Check if enabled
systemctl is-enabled flaskapp
# Check if active
systemctl is-active flaskapp
Viewing Service Logs with journalctl
# All logs for the service
journalctl -u flaskapp
# Follow live (like tail -f)
journalctl -u flaskapp -f
# Last 100 lines
journalctl -u flaskapp -n 100
# Logs since last boot
journalctl -u flaskapp -b
# Logs since a specific time
journalctl -u flaskapp --since "2026-03-26 10:00:00"
# Only errors
journalctl -u flaskapp -p err
For more journalctl options and filtering, see the journalctl guide.
Troubleshooting
# Verify the unit file syntax
systemd-analyze verify /etc/systemd/system/flaskapp.service
# See why a service failed to start
journalctl -u flaskapp -n 50 --no-pager
# Check unit file content as parsed by systemd
systemctl cat flaskapp
# Show all properties of the unit
systemctl show flaskapp