Cheatsheet and Quiz
Use this page as a quick reference after you've completed the full documentation. The cheat sheet is designed for copy-paste during daily operations. The quiz tests your understanding of key concepts.
Cheat Sheet
Complete systemd.path Cheat Sheet — Click to Expand
# ══════════════════════════════════════════════════════════════════════════════
# systemd.path — Quick Reference Cheat Sheet
# ══════════════════════════════════════════════════════════════════════════════
# ── STEP 1: Create the Path Unit ──────────────────────────────────────────────
# File: /etc/systemd/system/mytask.path
[Unit]
Description=Watch for incoming files
[Path]
DirectoryNotEmpty=/var/www/incoming # See directive reference below
MakeDirectory=yes # Create the directory if it doesn't exist
DirectoryMode=0775 # Permissions for auto-created dirs
Unit=mytask.service # Optional: override target service name
[Install]
WantedBy=paths.target
# ── STEP 2: Create the Service Unit ───────────────────────────────────────────
# File: /etc/systemd/system/mytask.service
[Unit]
Description=Process incoming files
StartLimitBurst=10 # Rate limit: max starts in window
StartLimitIntervalSec=60 # Rate limit: window size
[Service]
Type=oneshot
User=www-data
Group=www-data
ExecStart=/usr/local/bin/process.sh # MUST be absolute path
RuntimeMaxSec=30m # Kill after 30 minutes
StandardOutput=append:/var/log/mytask.log
StandardError=append:/var/log/mytask.log
NoNewPrivileges=true # Basic security hardening
PrivateTmp=true
# No [Install] section — activated by the .path unit
# ── STEP 3: Activate ─────────────────────────────────────────────────────────
sudo systemctl daemon-reload
sudo systemctl enable --now mytask.path
# ── Watch Directive Reference ─────────────────────────────────────────────────
PathExists=PATH # Triggers when path exists (LOOPS if not deleted)
PathExistsGlob=PATTERN # Triggers on glob match (LOOPS if matches remain)
PathChanged=PATH # Triggers after file is written AND fd is closed
PathModified=PATH # Triggers immediately on ANY write
DirectoryNotEmpty=PATH # Triggers if directory is non-empty (LOOPS until empty)
# ── Management Commands ──────────────────────────────────────────────────────
sudo systemctl daemon-reload # Re-read unit files (REQUIRED after edits)
sudo systemctl enable --now mytask.path # Enable on boot + start now
sudo systemctl disable --now mytask.path # Disable + stop
sudo systemctl start mytask.path # Start the watcher
sudo systemctl stop mytask.path # Stop the watcher
sudo systemctl restart mytask.path # Restart the watcher
systemctl status mytask.path # Check watch state
systemctl status mytask.service # Check service result
# ── Log Commands ──────────────────────────────────────────────────────────────
journalctl -u mytask.service -f # Follow live output
journalctl -u mytask.service -n 50 # Last 50 lines
journalctl -u mytask.service --since today # Today's logs
journalctl -u mytask.service -p err # Errors only
journalctl -u mytask.path -u mytask.service # Combined path+service logs
# ── Debug Commands ────────────────────────────────────────────────────────────
sudo systemd-analyze verify mytask.path # Validate syntax
sudo systemd-analyze verify mytask.service # Validate syntax
systemctl cat mytask.path # Show unit file content
systemctl show -p Triggers mytask.path # What service does it trigger?
systemctl list-units --type=path --all # List all path units
sudo systemctl reset-failed # Clear failed states
systemd-analyze security mytask.service # Security audit
# ── User Units (no root) ─────────────────────────────────────────────────────
# Files go in: ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now mytask.path
systemctl --user status mytask.path
journalctl --user -u mytask.service -f
sudo loginctl enable-linger "$USER" # Keep running after logout
# ── inotify Watch Limits ─────────────────────────────────────────────────────
cat /proc/sys/fs/inotify/max_user_watches # Check current limit
echo 65536 | sudo tee /proc/sys/fs/inotify/max_user_watches # Temporary increase
echo "fs.inotify.max_user_watches=65536" | sudo tee -a /etc/sysctl.conf # Permanent
sudo sysctl -p
# ── Full Cleanup ──────────────────────────────────────────────────────────────
sudo systemctl disable --now mytask.path
sudo rm /etc/systemd/system/mytask.path /etc/systemd/system/mytask.service
sudo systemctl daemon-reload
sudo systemctl reset-failed
Directive Quick Reference
| Directive | Fires When | Pre-existing? | Re-triggers? | Loop Risk | Script Must... |
|---|---|---|---|---|---|
PathExists= | Path exists | Yes | Yes | High | Delete the file |
PathExistsGlob= | Glob match exists | Yes | Yes | High | Delete/move all matches |
PathChanged= | File changed + fd closed | No | No | Low | Nothing |
PathModified= | Any write | No | No | Low | Nothing |
DirectoryNotEmpty= | Dir is non-empty | Yes | Yes | Medium | Delete/move files |
Decision Quick Reference
| I need to... | Use this directive |
|---|---|
| Process files in a drop folder | DirectoryNotEmpty= |
| React to a signal file (touch) | PathExists= |
| React to a file being uploaded (SFTP) | PathChanged= |
| React instantly to any file write | PathModified= |
| Process only specific file types | PathExistsGlob= |
| Reload a service when config changes | PathChanged= on the config dir |
| Alert on critical file modification | PathModified= |
| Wait for a file to appear before starting something | PathExists= |
Hands-On Lab
Lab 1 — Basic Drop Folder (5 minutes)
- Create
/etc/systemd/system/lab-drop.pathwithDirectoryNotEmpty=/tmp/lab-dropandMakeDirectory=yes. - Create
/etc/systemd/system/lab-drop.servicewithType=oneshotandExecStart=/bin/sh -c 'echo "Files:" && ls /tmp/lab-drop && rm -f /tmp/lab-drop/*'. - Run
sudo systemctl daemon-reload && sudo systemctl enable --now lab-drop.path. - Open a second terminal:
journalctl -f -u lab-drop.service. - Drop files:
touch /tmp/lab-drop/a.txt /tmp/lab-drop/b.txt. - Watch the journal show the files being listed and removed.
- Verify:
ls /tmp/lab-dropshould be empty.
Lab 2 — Signal File Pattern (5 minutes)
- Create
/etc/systemd/system/lab-signal.pathwithPathExists=/tmp/lab-signal.txt. - Create
/etc/systemd/system/lab-signal.servicewithExecStart=/bin/sh -c 'echo "Signal received at $(date)" && rm /tmp/lab-signal.txt'. - Reload and enable:
sudo systemctl daemon-reload && sudo systemctl enable --now lab-signal.path. - Trigger:
touch /tmp/lab-signal.txt. - Check:
journalctl -u lab-signal.service -n 5 --no-pager. - Verify:
/tmp/lab-signal.txtshould not exist.
Lab 3 — Config Change Watcher (5 minutes)
- Create
/etc/systemd/system/lab-config.pathwithPathChanged=/tmp/lab-config.conf. - Create
/etc/systemd/system/lab-config.servicewithExecStart=/bin/sh -c 'echo "Config changed at $(date): $(cat /tmp/lab-config.conf)"'. - Mark the config file as existing:
echo "original" > /tmp/lab-config.conf. - Reload and enable:
sudo systemctl daemon-reload && sudo systemctl enable --now lab-config.path. - Modify the config:
echo "updated value" > /tmp/lab-config.conf. - Check:
journalctl -u lab-config.service -n 5 --no-pager. - Verify: The log shows "Config changed" with the new value.
Lab 4 — Debugging Practice (5 minutes)
- Create a path unit with a deliberate typo — use
Directorynotempty=(wrong case) instead ofDirectoryNotEmpty=. - Run
sudo systemd-analyze verify /etc/systemd/system/lab-broken.path— observe the error. - Fix the typo and run verify again — observe no output (success).
- Create a service unit that references a script that doesn't exist.
- Enable the path unit and trigger it.
- Use
journalctl -u lab-broken.service -n 10to see the error. - Fix the script path and test again.
Lab 5 — Cleanup
for unit in lab-drop lab-signal lab-config lab-broken; do
sudo systemctl disable --now "${unit}.path" 2>/dev/null || true
sudo rm -f "/etc/systemd/system/${unit}.path" "/etc/systemd/system/${unit}.service"
done
sudo systemctl daemon-reload
rm -f /tmp/lab-signal.txt /tmp/lab-config.conf
rm -rf /tmp/lab-drop
Build Task — Full Cache-Clear Signal System
Write the complete units and the accompanied trigger script to allow clearing the WordPress object cache by an SFTP client (without SSH), with full logging and security hardening.
Solution
[Unit]
Description=Watch for WP cache flush signal
[Path]
PathExists=/var/www/html/clear_cache.txt
[Install]
WantedBy=paths.target
[Unit]
Description=Flush WordPress cache on signal
StartLimitBurst=5
StartLimitIntervalSec=60
[Service]
Type=oneshot
User=www-data
ExecStart=/usr/local/bin/flush-wp-cache.sh
RuntimeMaxSec=2m
StandardOutput=append:/var/log/cache-flush.log
StandardError=append:/var/log/cache-flush.log
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/var/log /var/www/html
#!/usr/bin/env bash
set -euo pipefail
echo "[$(date -Is)] Cache flush triggered"
/usr/local/bin/wp cache flush --path=/var/www/html
rm /var/www/html/clear_cache.txt
echo "[$(date -Is)] Done"
sudo chmod +x /usr/local/bin/flush-wp-cache.sh
sudo systemctl daemon-reload
sudo systemctl enable --now clear-cache.path
# Test:
touch /var/www/html/clear_cache.txt
sleep 1
journalctl -u clear-cache.service -n 10
Mini Quiz
Test your understanding of systemd.path. Try answering before checking the solutions.
Questions
-
What is the difference between
PathChanged=andPathModified=, and when does each fire? -
Which unit do you
systemctl enable— the.pathor the.service? -
What happens if you use
PathExists=and the service never deletes the trigger file? -
What directive would you use to process a "drop folder" where files are continuously being added?
-
Why is
Type=oneshotthe recommended service type for path-triggered services? -
A colleague added a new path unit but it isn't triggering. What is the first command they likely forgot to run?
-
Can you watch multiple files or directories in a single
.pathunit? If yes, is the logic AND or OR? -
How do you restrict a path-triggered service to run no more than 3 times per minute?
-
What
journalctlcommand shows live output from a triggered service? -
Your path unit is
active (waiting)but a pre-existing file is not triggeringPathChanged=. Why, and what directive would have fired immediately instead?
Answers
-
PathChanged=fires after the file is written and the file descriptor is closed (safe for uploads).PathModified=fires immediately on any write, even partial writes (unsafe for uploads, good for instant alerts). -
The
.pathunit.systemctl enable --now mytask.path. Enabling the.servicedirectly would run it once at boot — not on filesystem events. -
Infinite loop. The path unit detects the file still exists after the service exits, so it starts the service again, forever. The journal fills up and CPU spikes.
-
DirectoryNotEmpty=. It triggers when the directory contains at least one file and re-triggers after the service processes and removes a file — if more files remain. -
Type=oneshottells systemd the process runs once and exits. systemd won't start a second instance while the first is running (concurrency safety). Path-triggered jobs are inherently "run once, exit" — not long-running daemons. -
sudo systemctl daemon-reload. Without this, systemd is still using the old (or non-existent) unit file from before the edit. -
Yes, you can use multiple watch directives. The logic is OR — the service triggers when any directive matches.
-
Add to the
[Unit]section of the.servicefile:StartLimitBurst=3StartLimitIntervalSec=60 -
journalctl -u mytask.service -f— the-fflag follows live output liketail -f. -
PathChanged=only fires on future changes, not pre-existing files. UsePathExists=if you need the service to trigger immediately for a file that already exists when the path unit starts.
What's Next
You've completed the full systemd.path documentation. Here are related topics to explore:
- systemd timer — time-based scheduling (complements path-based triggers)
- Cron — traditional time-based task scheduling
- inotifywait — lower-level filesystem event monitoring
- Automation Task Ranking — choosing the right automation tool