Mastering Systemd Service Files: A Comprehensive Guide
Create reliable systemd service files with correct unit sections, restart behavior, logs, security, and timers.
Mastering Systemd Service Files: A Comprehensive Guide
Systemd service files tell Linux how to start, stop, restart, and supervise your application. If your service starts manually but fails at boot, restarts too aggressively, or writes logs to the wrong place, the unit file is usually where you need to look.
This guide focuses on creating and configuring systemd service unit files from scratch. You will see the core sections, a working Python service example, common troubleshooting commands, and a few security and resource controls worth using in production.
Understanding Systemd Unit Files
Systemd uses unit files to describe various system resources such as services, sockets, devices, mount points, and more. A service unit file, typically ending with the .service extension, defines how systemd should manage a specific daemon or application.
These files are organized into sections, with each section containing key-value pairs representing configuration directives. The primary sections we'll focus on are [Unit], [Service], and [Install].
Anatomy of a Systemd Service File
A typical systemd service file has the following structure:
[Unit]
Description=A brief description of the service.
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/my_application --config /etc/my_app.conf
Restart=on-failure
User=myuser
Group=mygroup
[Install]
WantedBy=multi-user.target
Let's break down each section and its common directives:
The [Unit] Section
This section provides metadata about the unit and defines its relationship with other units. It's used for dependencies and ordering.
Description=: A human-readable name for the service. This is what you'll see insystemctl statusoutput.Documentation=: URLs or paths to documentation for the service.Requires=: Defines strong dependencies. If a unit listed here fails to start, this unit will also fail to start.Wants=: Defines weak dependencies. If a unit listed here fails to start, this unit will still attempt to start.Before=: Ensures this unit starts before the units listed.After=: Controls ordering only. For example,After=network.targetstarts this unit after the basic network target, but it does not guarantee external connectivity. Network-dependent services may needAfter=network-online.targetplus the distribution's wait-online service.Conflicts=: If a unit listed here is started, this unit will be stopped, and vice-versa.
The [Service] Section
This section configures the behavior of the service itself. It's where you define how to start, stop, and manage the process.
Type=: Specifies the process startup type. Common values include:simple(default): The main process is the one specified inExecStart=. Systemd assumes the service is started immediately after theExecStart=process is forked.forking: TheExecStart=process forks a child, and the parent exits. Systemd considers the service started when the parent exits. You often need to specifyPIDFile=with this type.oneshot: Similar tosimple, but the process is expected to exit after its work is done. Useful for setup scripts.notify: The daemon sends a notification message to systemd when it has successfully started. This is the preferred type for modern daemons that support it.dbus: The service acquires a D-Bus name.
ExecStart=: The command to execute to start the service. For most service types, use oneExecStart=command. MultipleExecStart=lines are valid forType=oneshot, where they run sequentially.ExecStop=: The command to execute to stop the service.ExecReload=: The command to execute to reload the service's configuration without restarting.Restart=: Defines when the service should be automatically restarted. Common values:no(default): Never restart.on-success: Restart only if the service exits cleanly (exit code 0).on-failure: Restart if the service exits with a non-zero exit code, is terminated by a signal, or times out.on-abnormal: Restart if terminated by a signal or times out.on-abort: Restart only if terminated uncleanly by a signal.always: Always restart, regardless of the exit status.
RestartSec=: The time to sleep before restarting the service (default is 100ms).User=: The user to run the service as.Group=: The group to run the service as.WorkingDirectory=: The directory to change to before executing the commands.Environment=: Sets environment variables for the service.EnvironmentFile=: Reads environment variables from a file.PIDFile=: Path to the PID file (often used withType=forking).StandardOutput=/StandardError=: Controls where stdout and stderr go, such asjournal,null, orinherit. On common systemd-based distributions, service output normally lands in the journal unless the manager defaults have been changed.
The [Install] Section
This section defines how the unit should be enabled or disabled, typically by creating symbolic links.
WantedBy=: Specifies the target that should "want" this service when it's enabled. Common values:multi-user.target: For services that should start when the system reaches a multi-user command-line state.graphical.target: For services that should start when the system reaches a graphical login state.
Creating Your First Systemd Service File
Let's create a simple service file for a hypothetical Python script named my_app.py located at /opt/my_app/my_app.py.
1. Create the service file:
Service files for custom applications are typically placed in /etc/systemd/system/. Let's name our file my_app.service.
# Create the directory if it doesn't exist
sudo mkdir -p /etc/systemd/system/
# Create the service file using a text editor
sudo nano /etc/systemd/system/my_app.service
2. Add the following content to my_app.service:
[Unit]
Description=My Custom Python Application
After=network.target
[Service]
Type=simple
User=appuser
Group=appgroup
WorkingDirectory=/opt/my_app/
ExecStart=/usr/bin/python3 /opt/my_app/my_app.py
Restart=on-failure
[Install]
WantedBy=multi-user.target
Explanation of the example:
Description: Clearly identifies our application.After=network.target: Ensures the network is available before starting.Type=simple: Assumesmy_app.pyis the main process and doesn't fork.User=appuser,Group=appgroup: Specifies the user and group the application should run as. Make sure these users and groups exist on your system and have appropriate permissions. You might need to create them:sudo groupadd appgroup sudo useradd -r -g appgroup appuser sudo chown -R appuser:appgroup /opt/my_app/WorkingDirectory: Sets the context for the script.ExecStart: The command to run the Python script. Ensure/usr/bin/python3is the correct path to your Python interpreter and that the script is executable.Restart=on-failure: If the script crashes, systemd will try to restart it.WantedBy=multi-user.target: This service will be started automatically when the system boots into a multi-user environment.
3. Reload the systemd manager configuration:
After creating or modifying a service file, you must tell systemd to reload its configuration.
sudo systemctl daemon-reload
4. Enable and Start the Service:
- Enable: This makes the service start automatically on boot.
sudo systemctl enable my_app.service - Start: This starts the service immediately.
sudo systemctl start my_app.service
5. Check the Service Status:
To verify if your service is running and to see any potential errors:
sudo systemctl status my_app.service
If there are issues, the status command will often show error messages or logs from journald.
6. Viewing Logs:
Systemd integrates with journald for logging. You can view logs for your service using:
sudo journalctl -u my_app.service
You can also follow logs in real-time:
sudo journalctl -f -u my_app.service
Other Useful Commands
- Stop the service:
sudo systemctl stop my_app.service - Restart the service:
sudo systemctl restart my_app.service - Reload configuration (if supported by the app):
sudo systemctl reload my_app.service - Disable auto-start on boot:
sudo systemctl disable my_app.service
Advanced Configuration and Best Practices
Security Considerations
- Run services as non-root users: Always specify
User=andGroup=unless absolutely necessary. This follows the principle of least privilege. - Isolate services: Consider sandboxing features like
PrivateTmp=true,ProtectSystem=strict,ProtectHome=true, andNoNewPrivileges=true. Test them with your application because they can block legitimate file writes.PrivateTmp=true: Gives the service its own private/tmpand/var/tmpdirectories.ProtectSystem=strict: Makes most of the filesystem read-only for the service. UseReadWritePaths=for directories the service must write to.NoNewPrivileges=true: Prevents the service from gaining new privileges.
Handling Complex Startups
Type=forkingwithPIDFile=: For older applications that fork, ensurePIDFile=points to the correct file.Type=notify: If your application supports it, this is the most robust way for systemd to know when it's truly ready.ExecStartPre=andExecStartPost=: Commands to run before and afterExecStart=. Useful for setup or cleanup tasks.
Resource Control
Systemd allows you to limit resource usage:
CPUWeight=: Relative CPU weight for the service.MemoryMax=: Maximum memory the service can use.IOWeight=: Relative I/O weight where supported by the kernel and cgroup setup.
Example:
[Service]
# ... other directives ...
MemoryMax=512M
CPUWeight=50
Timers vs. Cron
Systemd timers offer a modern alternative to traditional cron jobs. They are more flexible and integrate better with systemd's logging and dependency management.
- Cron: Scheduled tasks defined in
crontabfiles. - Systemd Timers (
.timerunits): These units schedule.serviceunits. You define a.timerfile that specifies when a corresponding.servicefile should run.
Example:
To run a script daily at 3 AM:
my_script.service: The service to run.[Unit] Description=My daily script [Service] Type=oneshot ExecStart=/opt/my_scripts/run_daily.sh User=scriptusermy_script.timer: The timer that schedules the service.[Unit] Description=Run my daily script once a day [Timer] # Run at 03:00 every day OnCalendar=*-*-* 03:00:00 # Run soon after boot if the scheduled time was missed while the machine was off. Persistent=true [Install] WantedBy=timers.target
To use this:
- Place both files in
/etc/systemd/system/. - Run
sudo systemctl daemon-reload. - Enable and start the timer:
sudo systemctl enable my_script.timerandsudo systemctl start my_script.timer.
Timers offer advantages like Persistent=true (runs missed jobs upon boot), calendar events (like hourly, daily, weekly), and better integration with journalctl.
Troubleshooting Common Issues
- Service not starting: Check
systemctl status <service_name>andjournalctl -u <service_name>. Look for typos, incorrect paths, missing dependencies, or permission errors. - Incorrect
Type=: If a service fails immediately or hangs, theType=might be wrong. Trysimpleorforkingand ensurePIDFileis correct if usingforking. - Permission denied: Ensure the
User=andGroup=specified have read/write access to necessary files and directories. - Environment variables: If your application relies on specific environment variables, ensure they are set correctly using
Environment=orEnvironmentFile=. - Dependencies: Verify that
After=,Wants=, andRequires=match what you mean.After=orders startup; it does not pull another unit in by itself.
Before enabling a new unit on a production host, run:
sudo systemd-analyze verify /etc/systemd/system/my_app.service
This catches many syntax and directive mistakes before you rely on the service at boot.
Key Takeaway
Write the smallest service file that accurately describes your app, then add restart policy, logging, security restrictions, and resource limits deliberately. After every change, run systemctl daemon-reload, verify the unit, and check systemctl status plus journalctl -u before you trust it in production.