Table of Contents
Understanding Custom systemd Services
Custom services let you control your own scripts and applications with systemd just like built‑in services: start, stop, restart, enable at boot, watch for crashes, and log via journalctl.
In this chapter, you’ll learn how to:
- Write basic unit files for simple services
- Install and manage them as a normal user or as root
- Control startup ordering and dependencies
- Use common
systemdfeatures likeExecStart,Environment,Restart, andUser - Debug when things go wrong
This chapter assumes you already know basic systemctl usage and what a systemd unit is.
Unit File Locations and Naming
Systemd service units usually have names like something.service.
Common locations:
- System-wide (root-managed):
/usr/lib/systemd/system/or/lib/systemd/system/— distribution-provided units/etc/systemd/system/— local admin/custom units (higher priority)- Per-user:
~/.config/systemd/user/— units for the current user session
In almost all cases, for your own custom services, you’ll use:
/etc/systemd/system/(system services)~/.config/systemd/user/(user services)
Systemd picks the higher-priority file if the same unit name appears in multiple locations. /etc/systemd/system overrides system-provided units.
A Minimal Custom Service
Here’s a basic example: run a Python script as a system service.
Assume you have:
- Script at
/opt/myapp/run.py - Python at
/usr/bin/python3
Create the unit file:
sudo nano /etc/systemd/system/myapp.serviceContent:
[Unit]
Description=My example Python service
[Service]
ExecStart=/usr/bin/python3 /opt/myapp/run.py
[Install]
WantedBy=multi-user.targetThen:
sudo systemctl daemon-reload # reload systemd configs
sudo systemctl start myapp.service # start now
sudo systemctl status myapp.service # see status/logs
sudo systemctl enable myapp.service # start at bootKey points:
- Unit file name:
myapp.service - Minimal sections:
[Unit],[Service],[Install] ExecStartis required in most simple servicesWantedBy=multi-user.targetmakes it start automatically in normal multi-user (non-graphical or server) mode when enabled
Common `[Service]` Options
The [Service] section is where you describe what to run and how it behaves.
ExecStart and Friends
ExecStart=— main command to run (required for most services)ExecStartPre=— commands to run before the main processExecStartPost=— commands to run after the main process startsExecStop=— custom command to stop the serviceExecReload=— how to reload configuration without restarting
You can have multiple ExecStartPre= and ExecStartPost= lines; they run in order.
Example with a pre-start check and post-start log:
[Service]
Type=simple
ExecStartPre=/usr/bin/test -f /etc/myapp/config.yml
ExecStart=/usr/bin/myapp --config /etc/myapp/config.yml
ExecStartPost=/usr/bin/logger "myapp started"
If any ExecStartPre= command fails (non-zero exit code), the service will not start.
Service Type
The Type= option tells systemd how your service behaves. Some common ones:
Type=simple(default):ExecStartruns the process directly, and systemd assumes it is the main daemon.Type=forking: application forks into background; parent exits, child keeps running (classic Unix daemons).Type=oneshot: runs a short-lived command that exits; good for setup tasks.Type=notify: application notifies systemd when it is ready (used by more advanced daemons).
Unless you know otherwise, use Type=simple.
Example oneshot service (runs once at boot):
[Unit]
Description=Initialize myapp at boot
[Service]
Type=oneshot
ExecStart=/usr/local/bin/myapp-init.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
RemainAfterExit=yes tells systemd to consider the service “active” even after the command finishes (useful for boot-time setup actions you may want to check with status).
Restart Policy
Control automatic restarts:
Restart=no— never restart (default)Restart=on-failure— restart on non-zero exit status or crashRestart=always— restart whenever it stops, even if it exits cleanlyRestart=on-abort— restart only if killed by a signal
Combine with RestartSec= (delay between restarts):
[Service]
ExecStart=/usr/bin/myapp
Restart=on-failure
RestartSec=5This will wait 5 seconds before attempting to restart after a failure.
Running as a Specific User and Group
By default, system services run as root; that’s often unnecessary and unsafe.
Use User= and optionally Group=:
[Service]
User=myappuser
Group=myappgroup
ExecStart=/usr/bin/myappMake sure the user and group exist and have permissions to access required files/directories.
Environment Variables and Working Directory
Setting Environment Variables
You can define environment variables directly:
[Service]
Environment=MYAPP_ENV=production
Environment=PORT=8080
ExecStart=/usr/bin/myappFor more complex sets, use an environment file:
- Create
/etc/myapp/myapp.env:
MYAPP_ENV=production
PORT=8080
LOG_LEVEL=info- Reference it in your unit:
[Service]
EnvironmentFile=/etc/myapp/myapp.env
ExecStart=/usr/bin/myapp
Multiple EnvironmentFile= lines are allowed.
WorkingDirectory
If your program expects to run from a specific directory:
[Service]
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/python3 app.py
This is better than embedding cd in a wrapper script.
Controlling Dependencies and Startup Order
Systemd uses targets and dependencies to control when and in what order services start. You typically express relations with After=, Before=, Requires=, and Wants= in the [Unit] section.
Basic Startup Ordering
After=— ensure your service starts only after the listed units have started (ordering only).Before=— ensure your service starts before the listed units.
Example: start after networking is up:
[Unit]
Description=My web app
After=network-online.target
Wants=network-online.target
Wants= indicates a soft dependency: it will try to start network-online.target, but your service doesn’t fail just because that didn’t start. Requires= is a hard dependency: if the required unit fails, your service is stopped.
Common patterns:
- Network-dependent servers:
[Unit]
Description=Custom HTTP service
Wants=network-online.target
After=network-online.target- Services that depend on another service:
[Unit]
Description=My app that uses PostgreSQL
Requires=postgresql.service
After=postgresql.service
Requires= ensures PostgreSQL is started and keeps it as a hard dependency.
Installing and Enabling Custom Services
Once you create or modify a unit file, systemd needs to reload its configuration:
sudo systemctl daemon-reloadThen typical workflow:
sudo systemctl start myapp.service # start now
sudo systemctl status myapp.service # verify running, check logs
sudo systemctl enable myapp.service # start automatically on boot
sudo systemctl disable myapp.service # remove from boot
sudo systemctl stop myapp.service # stop itRemember:
daemon-reloadis required whenever a unit file is changed or created.enablecreates symlinks (for example intomulti-user.target.wants/) using the[Install]section.
User Services vs System Services
You can create services that run as your user without needing root. These are user units.
User Unit Location and Commands
- Create directory if needed:
mkdir -p ~/.config/systemd/user- Create unit file, e.g.
~/.config/systemd/user/myuserservice.service
Example: a user-level script that runs when you log in (for systems using systemd user sessions):
[Unit]
Description=My user-level script
[Service]
Type=simple
ExecStart=/home/alice/bin/my-script.sh
[Install]
WantedBy=default.target
Manage user units with --user:
systemctl --user daemon-reload
systemctl --user start myuserservice.service
systemctl --user enable myuserservice.service
systemctl --user status myuserservice.serviceTo allow user services to run without an active login (lingering):
sudo loginctl enable-linger alice
Replace alice with your username.
Creating a Wrapper Script for Simplicity
Sometimes your service requires several setup steps that are easier to express in a shell script than in unit options. Example:
- Activate a virtualenv
- Set several complicated environment variables
- Run a command
Instead of packing all of this into the unit file:
- Create a script, e.g.
/usr/local/bin/myapp-wrapper.sh:
#!/usr/bin/env bash
set -e
cd /opt/myapp
source venv/bin/activate
exec python app.pyMake it executable:
sudo chmod +x /usr/local/bin/myapp-wrapper.sh- Use it in your unit:
[Unit]
Description=My app via wrapper
[Service]
ExecStart=/usr/local/bin/myapp-wrapper.sh
Restart=on-failure
[Install]
WantedBy=multi-user.target
The exec in the script replaces the shell process with your app, which allows systemd to track it cleanly.
Using Temporary Files, State, and Logs
Systemd offers options to manage where the service stores data, and to keep it separate from the rest of the filesystem. Some useful ones:
RuntimeDirectory=— creates a directory under/run(tmpfs, cleared on reboot) owned by your service’s user/group.StateDirectory=— creates and optionally manages a directory under/var/lib.CacheDirectory=— for cache data under/var/cache.Logsare normally handled byjournalctlwithout any extra work if you write to stdout/stderr.
Example:
[Service]
User=myapp
Group=myapp
ExecStart=/usr/bin/myapp --state-dir=/var/lib/myapp
StateDirectory=myapp
RuntimeDirectory=myappThis will ensure:
/run/myappexists and is owned bymyapp:myapp/var/lib/myappexists and is owned bymyapp:myapp
You can still add explicit mkdir steps if you prefer, but these tools reduce manual setup.
Basic Security Hardening Options
You can restrict what your custom service can access. A few simple, commonly used options:
ProtectSystem=full— make system directories read-only.ProtectHome=true— hide/home,/root, and/run/userfrom the service.PrivateTmp=true— give the service a private/tmpand/var/tmp.NoNewPrivileges=true— disallow gaining new privileges (like viasetuidbinaries).CapabilityBoundingSet=— remove unneeded Linux capabilities.
Example:
[Service]
User=myapp
Group=myapp
ExecStart=/usr/bin/myapp
ProtectSystem=full
ProtectHome=true
PrivateTmp=true
NoNewPrivileges=trueStart with simple measures and test thoroughly; strong restrictions can break applications that expect broad access.
Testing and Debugging Your Custom Service
When a custom service doesn’t work, systemd gives useful tools to find out why.
Checking Status
sudo systemctl status myapp.serviceThis shows:
- Active/inactive/failed state
- Last few log lines
- Exit status or signal if it crashed
Viewing Logs
Use journalctl:
sudo journalctl -u myapp.service # all logs for this unit
sudo journalctl -u myapp.service -f # follow in real time
sudo journalctl -u myapp.service --since "10 minutes ago"If your application prints to stdout/stderr, logs appear here automatically.
Checking Unit Syntax
Systemd intentionally has minimal syntax, but you can catch obvious errors by reloading:
sudo systemctl daemon-reloadIf something is badly wrong, you’ll see messages in:
journalctl -b -p error
sudo systemd-analyze verify /etc/systemd/system/myapp.service
systemd-analyze verify is particularly helpful to catch dependency loops, invalid options, etc.
Temporary Overrides and Testing Changes
While developing, it’s convenient to edit the unit, then:
sudo systemctl daemon-reload
sudo systemctl restart myapp.service
sudo systemctl status myapp.serviceRepeat until the service behaves as expected.
Overriding Existing Services (Drop-in Configs)
If you want to change options for an existing service without editing its main unit file (which may be owned by your distribution), use a drop-in:
sudo systemctl edit ssh.serviceThis opens an editor where you can place only the overrides, for example:
[Service]
Environment=SSH_LOG_LEVEL=VERBOSE
This creates something like /etc/systemd/system/ssh.service.d/override.conf.
To remove the override:
sudo systemctl revert ssh.serviceUsing drop-ins for your own services is optional, but it’s useful when you’d rather not edit the original file in place.
Practical Example: Custom Web App Service
Putting several concepts together, here’s a more complete example service for a simple web app.
Assume:
- Code lives in
/opt/webapp - Script
/opt/webapp/start.sh - Runs as user
webapp - Needs network
Unit file: /etc/systemd/system/webapp.service
[Unit]
Description=Simple Web Application
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=webapp
Group=webapp
WorkingDirectory=/opt/webapp
EnvironmentFile=/etc/webapp/webapp.env
ExecStart=/opt/webapp/start.sh
Restart=on-failure
RestartSec=5
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
NoNewPrivileges=true
[Install]
WantedBy=multi-user.targetThen:
sudo systemctl daemon-reload
sudo systemctl enable webapp.service
sudo systemctl start webapp.service
sudo systemctl status webapp.serviceIf it fails, check logs:
sudo journalctl -u webapp.service -f
Creating custom systemd services mostly comes down to defining what you want to run, under which user, when it should start and restart, and how it depends on other parts of the system. With a few unit files and some testing via systemctl and journalctl, you can turn your scripts and applications into robust, boot-managed services.