Unit File Anatomy
By the end of this lesson you will be able to write a complete .path and .service unit pair from memory, understand every directive available in the [Path] section, and know which [Service] options are critical for path-triggered jobs.
Unit File Format
systemd unit files use the INI format — sections in brackets, directives as Key=Value pairs. A .path unit has up to three sections:
[Unit] # Metadata, dependencies, ordering
[Path] # What to watch and how to watch it
[Install] # How to enable the unit on boot
Its paired .service unit has:
[Unit] # Metadata, dependencies, ordering
[Service] # What to run, how to run it, security
# No [Install] section — activated by the .path unit
The .path Unit File
[Unit] Section
The [Unit] section provides metadata and declares dependencies. These directives are common to all systemd unit types.
| Directive | Description | Example |
|---|---|---|
Description= | Human-readable description shown in systemctl status | Description=Watch /var/www/drop for incoming files |
Documentation= | Link to documentation (man page, URL) | Documentation=man:systemd.path(5) |
After= | Start this unit after the listed units are active | After=network.target |
Wants= | Weak dependency — try to start the listed units, but don't fail if they can't | Wants=network-online.target |
Requires= | Strong dependency — fail if the listed units can't start | Requires=local-fs.target |
ConditionPathExists= | Only start if this path exists | ConditionPathExists=/var/www/html |
ConditionPathIsDirectory= | Only start if this path is a directory | ConditionPathIsDirectory=/var/www/drop |
[Unit]
Description=Watch for incoming media files
Documentation=man:systemd.path(5)
After=local-fs.target
Wants=network-online.target
[Path] Section
The [Path] section is specific to .path units. It defines what to watch and how to handle the watched path.
Watch Directives
| Directive | Description | Loop Risk |
|---|---|---|
PathExists=PATH | Trigger when the path exists | High |
PathExistsGlob=PATTERN | Trigger when at least one glob match exists | High |
PathChanged=PATH | Trigger when path changes and fd is closed | Low |
PathModified=PATH | Trigger on any write to the path | Low |
DirectoryNotEmpty=PATH | Trigger when directory contains at least one entry | Medium |
See Watch Directives for a deep dive on each.
Configuration Directives
| Directive | Description | Default | Example |
|---|---|---|---|
Unit= | Name of the service unit to activate (if different from base name) | Same base name .service | Unit=nginx-reload.service |
MakeDirectory= | Create the watched path and all parents if they don't exist | no | MakeDirectory=yes |
DirectoryMode= | Octal permission mode for auto-created directories | 0755 | DirectoryMode=0775 |
TriggerLimitIntervalSec= | Rate-limit window for path triggers (systemd 250+) | 2s | TriggerLimitIntervalSec=5s |
TriggerLimitBurst= | Max triggers within the rate-limit window (systemd 250+) | 200 | TriggerLimitBurst=10 |
[Path]
DirectoryNotEmpty=/var/www/drop
MakeDirectory=yes
DirectoryMode=0775
Unit=process-drop.service
About MakeDirectory=
When MakeDirectory=yes is set:
- systemd creates the entire path (including parents) if it doesn't exist.
- Permissions are set according to
DirectoryMode=. - The directory is created before the watch is registered.
- This eliminates the common "directory not found" startup error.
[Path]
DirectoryNotEmpty=/opt/reports/incoming/2026
MakeDirectory=yes
DirectoryMode=0775 # Group-writable for www-data
[Install] Section
The [Install] section determines how the unit is enabled on boot.
| Directive | Description | Example |
|---|---|---|
WantedBy= | Target(s) that want this unit when enabled | WantedBy=paths.target |
RequiredBy= | Target(s) that require this unit when enabled | RequiredBy=multi-user.target |
Also= | Additional units to enable/disable together | Also=process-drop.service |
[Install]
WantedBy=paths.target
paths.target?paths.target is a systemd target that groups all .path units. By setting WantedBy=paths.target, your path unit starts automatically at boot after all filesystems are mounted. This is the standard practice for all .path units.
Complete .path Template
[Unit]
Description=Watch /var/www/drop for incoming files
Documentation=man:systemd.path(5)
After=local-fs.target
[Path]
DirectoryNotEmpty=/var/www/drop
MakeDirectory=yes
DirectoryMode=0775
# Unit=custom-service.service # Uncomment to override default name match
[Install]
WantedBy=paths.target
The Paired .service Unit File
Why the Service Must Be Type=oneshot
A path-triggered service runs once, processes the event, and exits. This maps directly to Type=oneshot:
| Service Type | Behavior | Suitable for .path? |
|---|---|---|
Type=oneshot | Runs once, exits when done. systemd considers it "active" during execution. | Yes — recommended |
Type=simple | Starts a long-running process. | No — path triggers expect short-lived jobs |
Type=forking | Starts a daemon that forks. | No — wrong model for event processing |
Type=exec | Similar to simple but waits for exec. | Rarely — only if your command is a long-running processor |
Why No [Install] Section
The .service unit should not have an [Install] section because:
- It is activated by the
.pathunit, not by systemd at boot. - If you
systemctl enablethe service directly, it would run once at boot — which is usually not what you want. - The
.pathunit handles enabling and boot integration.
[Unit] Section
Same as the .path unit's [Unit] section. Critical directives for the service:
| Directive | Description | Example |
|---|---|---|
Description= | Human-readable description | Description=Process one file from the drop folder |
After= | Start after these units | After=network-online.target |
Wants= | Weak dependency | Wants=network-online.target |
StartLimitBurst= | Max starts within the limit window (rate-limit) | StartLimitBurst=5 |
StartLimitIntervalSec= | Time window for the start rate limit | StartLimitIntervalSec=30 |
[Service] Section
| Directive | Description | Default | Example |
|---|---|---|---|
Type= | Service type | simple | Type=oneshot |
ExecStart= | Command to execute (full absolute path required) | — | ExecStart=/usr/local/bin/process.sh |
ExecStartPre= | Command to run BEFORE ExecStart | — | ExecStartPre=/usr/bin/test -d /var/www/drop |
ExecStartPost= | Command to run AFTER ExecStart succeeds | — | ExecStartPost=/usr/bin/logger "Job done" |
User= | Run the service as this user | root | User=www-data |
Group= | Run the service as this group | root | Group=www-data |
WorkingDirectory= | Set the current working directory | / | WorkingDirectory=/var/www/html |
Environment= | Set environment variables | — | Environment="PATH=/usr/local/bin:/usr/bin" |
EnvironmentFile= | Load environment variables from a file | — | EnvironmentFile=/etc/default/myapp |
StandardOutput= | Where to send stdout | journal | StandardOutput=append:/var/log/job.log |
StandardError= | Where to send stderr | journal | StandardError=append:/var/log/job.log |
RuntimeMaxSec= | Kill the process after this duration | infinity | RuntimeMaxSec=30m |
TimeoutStartSec= | Max time to wait for start | 90s | TimeoutStartSec=5m |
Restart= | When to restart the service | no | Restart=on-failure |
RestartSec= | Delay between restart attempts | 100ms | RestartSec=5s |
Nice= | Process niceness (-20 to 19) | 0 | Nice=10 |
IOSchedulingClass= | I/O scheduler class | none | IOSchedulingClass=idle |
Logging Options
| Value | Behavior |
|---|---|
StandardOutput=journal | Send to journald (default, use journalctl to read) |
StandardOutput=journal+console | Send to both journald and console |
StandardOutput=append:/path/to/file | Append to a file |
StandardOutput=truncate:/path/to/file | Overwrite a file each time |
StandardOutput=null | Discard output |
Security Hardening Directives
These restrict what the service can do, reducing the blast radius of a compromised script:
| Directive | Description | Example |
|---|---|---|
NoNewPrivileges= | Prevent privilege escalation | NoNewPrivileges=true |
PrivateTmp= | Give the service its own /tmp | PrivateTmp=true |
ProtectSystem= | Make /usr, /boot, /efi read-only | ProtectSystem=strict |
ProtectHome= | Hide or make home directories read-only | ProtectHome=read-only |
ReadWritePaths= | Whitelist writable paths (used with ProtectSystem=strict) | ReadWritePaths=/var/www/drop /var/log |
ReadOnlyPaths= | Force specific paths to be read-only | ReadOnlyPaths=/etc |
PrivateDevices= | Hide physical devices | PrivateDevices=true |
ProtectKernelTunables= | Prevent writing to kernel tunables | ProtectKernelTunables=true |
ProtectControlGroups= | Prevent writing to cgroups | ProtectControlGroups=true |
MemoryDenyWriteExecute= | Prevent creating writable+executable memory | MemoryDenyWriteExecute=true |
RestrictRealtime= | Prevent real-time scheduling | RestrictRealtime=true |
Complete .service Template
[Unit]
Description=Process incoming files from the drop folder
After=network-online.target
Wants=network-online.target
StartLimitBurst=10
StartLimitIntervalSec=60
[Service]
Type=oneshot
User=www-data
Group=www-data
WorkingDirectory=/var/www/html
Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
ExecStart=/usr/local/bin/process.sh
RuntimeMaxSec=30m
StandardOutput=append:/var/log/process-job.log
StandardError=append:/var/log/process-job.log
# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/var/log /var/www/drop
Specifiers (Variables in Unit Files)
systemd supports specifiers — special % tokens that are expanded at runtime:
| Specifier | Expands To | Example |
|---|---|---|
%n | Full unit name | process-drop.path |
%N | Unescaped full unit name | process-drop.path |
%p | Unit name prefix (before the first .) | process-drop |
%i | Instance name (for template units) | csv in import@csv.service |
%h | Home directory of the user running the unit | /home/user |
%u | Username of the user running the unit | www-data |
%U | UID of the user running the unit | 33 |
%t | Runtime directory | /run (system) or /run/user/1000 (user) |
%H | Hostname | vps |
Specifier Example
[Unit]
Description=Watch %h/Downloads for new files
[Path]
DirectoryNotEmpty=%h/Downloads
MakeDirectory=yes
[Install]
WantedBy=default.target
This expands to /home/user/Downloads when run as a user unit.
Template Units (Advanced)
Template units allow you to create reusable unit files that accept a parameter (instance name). The template file name contains an @:
import@.path → import@csv.path, import@json.path, import@xml.path
import@.service → import@csv.service, import@json.service, import@xml.service
The instance name is accessed via the %i specifier:
[Unit]
Description=Watch for incoming %i files
[Path]
PathExistsGlob=/mnt/import/*.%i
[Install]
WantedBy=paths.target
[Unit]
Description=Import %i files
[Service]
Type=oneshot
User=www-data
ExecStart=/usr/local/bin/import-%i.sh
# Enable for CSV files
sudo systemctl enable --now import@csv.path
# Enable for JSON files
sudo systemctl enable --now import@json.path
# Check status
systemctl status import@csv.path
systemctl status import@json.path
Drop-In Overrides
Instead of editing a unit file directly (especially package-provided ones), you can create a drop-in override file:
# Create an override directory
sudo mkdir -p /etc/systemd/system/process-drop.service.d/
# Create the override file
sudo tee /etc/systemd/system/process-drop.service.d/override.conf > /dev/null <<'EOF'
[Service]
# Override the runtime limit
RuntimeMaxSec=1h
# Add security hardening
NoNewPrivileges=true
ProtectSystem=strict
EOF
# Or use the built-in editor
sudo systemctl edit process-drop.service
The override is merged with the original unit file. You can override, add, or clear specific directives.
Clearing a Directive
To remove a directive set in the original unit, set it to an empty value:
[Service]
# Clear the Environment directive from the original
Environment=
# Set new environment
Environment="NEW_VAR=value"
Full Example — Annotated
Here is a complete, production-ready unit pair with every line annotated:
[Unit]
Description=Watch for incoming media files to process
Documentation=man:systemd.path(5)
# Wait for local filesystems to be mounted
After=local-fs.target
[Path]
# Trigger when files appear in the queue directory
DirectoryNotEmpty=/var/media/queue
# Create the directory if it doesn't exist
MakeDirectory=yes
# Set permissions: owner rwx, group rwx, other r-x
DirectoryMode=0775
# Optional: explicitly name the target service (default: media-processor.service)
# Unit=media-processor.service
[Install]
# Start this path unit on boot as part of the paths.target group
WantedBy=paths.target
[Unit]
Description=Process one media file from the queue
# Require network for uploading processed files
After=network-online.target
Wants=network-online.target
# Rate limit: max 10 starts in 60 seconds
StartLimitBurst=10
StartLimitIntervalSec=60
[Service]
# Run once and exit
Type=oneshot
# Run as www-data, not root
User=www-data
Group=www-data
# Set working directory for relative paths in the script
WorkingDirectory=/var/www/html
# Ensure PATH includes common binary locations
Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
# The actual command to run
ExecStart=/usr/local/bin/process-media.sh
# Kill the job if it runs longer than 30 minutes
RuntimeMaxSec=30m
# Log to both journald and a file
StandardOutput=append:/var/log/media-processor.log
StandardError=append:/var/log/media-processor.log
# --- Security hardening ---
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=read-only
# Only allow writes to these paths
ReadWritePaths=/var/log /var/media/queue /var/media/done
# No [Install] section — this service is activated by media-processor.path
Key Takeaways
- A
.pathunit has three sections:[Unit],[Path],[Install]. - A
.serviceunit paired with a.pathshould useType=oneshotand have no[Install]section. MakeDirectory=yeseliminates startup errors by creating the watched path.- Security hardening (
NoNewPrivileges,ProtectSystem, etc.) should be applied to every production service. - Template units (
@) allow reusable unit files with instance-specific parameters. - Drop-in overrides (
name.d/override.conf) let you customize units without modifying the original file.
What's Next
- Commands and Management — all the
systemctlandjournalctlcommands you need.