A systemd unit is the description file for anything systemd manages: services, timers, mount points, devices and more. If you want your own service to run permanently on a modern Linux system, a unit file is the way to do it.
Unit types
.service— services and one-shot commands..timer— scheduled execution (the modern replacement for cron)..socket— listen for connections and start the service on demand..target— grouping (roughly comparable to a runlevel)..mount/.automount— mount points..path— watch files or directories.
Where unit files live
# Search order and precedence (later wins)
/usr/lib/systemd/system/ packages - do NOT edit, gets overwritten
/etc/systemd/system/ your units and overrides <- work here
/run/systemd/system/ generated at runtime
# Create your own unit
/etc/systemd/system/my-service.service
# Modify an existing unit WITHOUT touching the original file:
systemctl edit my-service.service
# creates /etc/systemd/system/my-service.service.d/override.conf
# put only the lines you change in there
# -> survives package updates
# Alternative: copy the unit and adjust its paths
cp /usr/lib/systemd/system/nginx.service /etc/systemd/system/This email address is being protected from spambots. You need JavaScript enabled to view it.
# change the Name= line and paths inside - never run both at once!
A service unit by hand
[Unit]
Description=Example API server
Documentation=https://example.org/api
# Only start once these units are ready
After=network-online.target postgresql.service
Wants=network-online.target
# Do not start if these are running
Conflicts=apache2.service
[Service]
Type=simple
User=api
Group=api
WorkingDirectory=/srv/api
Environment=NODE_ENV=production
Environment="PORT=8080"
# Secrets must NOT live in the unit - use a separate file with chmod 600
EnvironmentFile=-/etc/api/api.env
ExecStart=/usr/bin/node /srv/api/server.js
# Graceful stop first, then hard kill - important for data integrity
ExecReload=/bin/kill -HUP $MAINPID
KillSignal=SIGTERM
TimeoutStopSec=30
Restart=on-failure
RestartSec=5
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=api
# Hardening - the most valuable options
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/srv/api/data /var/log/api
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictNamespaces=true
LockPersonality=true
MemoryDenyWriteExecute=true
# Resource limits
LimitNOFILE=65536
MemoryMax=1G
CPUQuota=200%
[Install]
WantedBy=multi-user.target
The four Type values that matter
simple(default): the command starts and keeps running. systemd considers it active immediately.exec: likesimple, but activation counts as successful only after the actual program call succeeded. Safer against failures inside wrapper scripts.forking: for classic daemons that detach into the background. Usually needsPIDFile=.oneshot: a task that is expected to finish. Correct for scripts, often combined withRemainAfterExit=yesandBefore=.notify: the program signals readiness itself (sd_notify). Best for services with a slow startup.
Controlling and diagnosing
# MANDATORY after changing any unit file
systemctl daemon-reload
systemctl enable --now my-service
systemctl disable --now my-service
systemctl start | stop | restart | reload my-service
systemctl status my-service
systemctl is-active my-service
systemctl is-enabled my-service
systemctl is-failed my-service
# Why is it not running? - the key question
systemctl status my-service
journalctl -u my-service -n 100 --no-pager
journalctl -u my-service -f
journalctl -u my-service --since '10 min ago'
journalctl -u my-service -p err
# Dependencies and ordering
systemctl list-dependencies my-service
systemctl list-dependencies --reverse my-service
systemctl show my-service | grep -E 'After|Wants|Requires'
# What went wrong?
systemctl show my-service -p Restart -p RestartUSec -p NRestarts -p Result
journalctl -u my-service | grep -iE 'fail|error|denied|not found'
systemctl --failed
systemd-analyze blame | head -20
systemd-analyze critical-chain
Timers instead of cron
# /etc/systemd/system/backup.timer
[Unit]
Description=Daily backup
[Timer]
OnCalendar=*-*-* 03:30:00
# If the machine was off at the scheduled time:
Persistent=true
# Random delay - relieves pressure from simultaneous jobs
RandomizedDelaySec=15m
Unit=backup.service
[Install]
WantedBy=timers.target
# /etc/systemd/system/backup.service
[Unit]
Description=Run daily backup
[Service]
Type=oneshot
User=backup
ExecStart=/usr/local/bin/backup.sh
systemctl daemon-reload
systemctl enable --now backup.timer
# Check - the most important command for scheduled work
systemctl list-timers --all
journalctl -u backup.service -n 50
# Validate the calendar expression BEFORE relying on it
systemd-analyze calendar '*-*-* 03:30:00'
systemd-analyze calendar 'Mon..Fri 08:00'
# The output shows the next firing times - typos become visible.
Advantages over cron
- Logging: output goes to the journal with timestamps, exit codes and runtime. No more mail silently discarded.
- Dependencies: a job can require that a service is running or a mount point is ready.
- Resource control: memory and CPU limits per job.
- Missed runs:
Persistent=truecatches up after downtime. - Status:
systemctl list-timersshows last and next run for every job.
Common mistakes
- Forgetting
daemon-reload. Unit file changes do nothing. This is the single most common error. - Relative paths in
ExecStart. systemd has no meaningfulPATH— always use the full path. - Missing environment. Shell variables are not inherited. Use
Environment=orEnvironmentFile=. - Wrong
Type. A forking daemon declared asType=simpleis considered exited and restarted endlessly. - Confusing
After=with a dependency.Afteronly orders; it does not require. UseRequires=orWants=for "only start if". - Over-tightening hardening.
ProtectSystem=strictmakes the whole filesystem read-only — write paths must be listed inReadWritePaths=or the service fails to start. - Secrets inside the unit file. Unit files are world-readable. Use
EnvironmentFile=withchmod 600orLoadCredential=. - Bad calendar expressions.
OnCalendaris not cron syntax. Validate withsystemd-analyze calendar. - Missing
WantedBy=. The service can be started but not enabled persistently.
Checklist for a new service
- Does it run as a dedicated user with no login shell?
- Are secrets out of the unit file and in a protected file?
- Is
Restart=sensible, with a start limit preventing a restart loop? - Does a signal stop the process cleanly before the hard kill?
- Is hardening active (
ProtectSystem,NoNewPrivileges,PrivateTmp)? - Are logs in the journal, discoverable with
journalctl -u? - Does it start automatically after a reboot (
enable)? - Are resources limited so the service cannot take over the whole host?
Related: systemd explained, cron and crontab, Shell, systemd-Units (DE).