}

Create and Manage systemd Service Files: Complete Guide (2026)

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

Related Articles