systemd unit file authoring and system management skill: service units, timer units, socket activation, drop-in overrides, journalctl, and security hardening options. USE WHEN: - Writing or modifying systemd service unit files for Node.js, Python, Go, or other apps - Setting up automatic restarts, dependency ordering, and environment variable loading - Creating timer units as a cron replacement for periodic tasks - Hardening a service with PrivateTmp, NoNewPrivileges, ProtectSystem=strict, e...
Scanned 9/8/2026
Install to Claude Code
npx -y skills add claude-dev-suite/claude-dev-suite --skill systemd --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Systemd?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/claude-dev-suite-systemd)More formats (shields.io, HTML) on the badges page.
---
name: systemd
description: |
systemd unit file authoring and system management skill: service units,
timer units, socket activation, drop-in overrides, journalctl, and security
hardening options.
USE WHEN:
- Writing or modifying systemd service unit files for Node.js, Python, Go, or other apps
- Setting up automatic restarts, dependency ordering, and environment variable loading
- Creating timer units as a cron replacement for periodic tasks
- Hardening a service with PrivateTmp, NoNewPrivileges, ProtectSystem=strict, etc.
- Using drop-in overrides to modify upstream package-provided unit files
- Debugging service startup failures, missing env vars, or timer misfires
- Managing logs with journalctl (filtering, following, exporting)
DO NOT USE FOR:
- Docker container orchestration (use docker or kubernetes skill)
- Application-level scheduling (job queues, cron-based business logic — use job-queues skill)
- Full init system replacement on non-systemd distros (Alpine uses OpenRC)
- Windows Task Scheduler or macOS launchd equivalents
allowed-tools: Read, Grep, Glob, Write, Edit, Bash
---
# systemd — Unit Files and System Management
## Unit File Anatomy
systemd units live in:
- `/lib/systemd/system/` — package-installed units (do not edit directly)
- `/etc/systemd/system/` — administrator units; override package units with same name
- `/etc/systemd/system/<name>.service.d/` — drop-in overrides (preferred)
Unit files use INI-style sections. Key sections for a service:
```
[Unit] — metadata, dependencies, ordering
[Service] — what to run and how to manage it
[Install] — how to enable/disable (which targets want this unit)
```
---
## Production Service Unit Template
Save as `/etc/systemd/system/myapp.service`:
```ini
[Unit]
Description=My Application Server
Documentation=https://docs.example.com/myapp
# Ordering: start after network is up and PostgreSQL is ready
After=network-online.target postgresql.service
Wants=network-online.target
# Hard dependency: if PostgreSQL stops, this unit also stops
Requires=postgresql.service
[Service]
# --- Process type ---
# simple: ExecStart is the main process; systemd tracks it directly
# forking: process daemonises (old-style); set PIDFile=
# notify: process signals systemd via sd_notify() when ready
# oneshot: for scripts that run and exit; combine with RemainAfterExit=yes
Type=notify
# --- Identity ---
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
# --- Environment ---
# Load secrets from a file not checked into source control
EnvironmentFile=/etc/myapp/environment
# Inline env vars (for non-secret values)
Environment=NODE_ENV=production
Environment=PORT=3000
# --- Process lifecycle ---
ExecStartPre=/opt/myapp/scripts/pre-start.sh # Validation / migration
ExecStart=/usr/bin/node /opt/myapp/dist/server.js
ExecStop=/bin/kill -SIGTERM $MAINPID # Graceful shutdown signal
ExecStopPost=/opt/myapp/scripts/post-stop.sh # Cleanup after stop
ExecReload=/bin/kill -SIGHUP $MAINPID # Signal for config reload (if supported)
# --- Restart behaviour ---
Restart=on-failure # Restart if process exits non-zero or is killed
RestartSec=5s # Wait 5 seconds before restarting
StartLimitIntervalSec=60s # Window for counting start attempts
StartLimitBurst=5 # Max 5 starts in 60s; unit enters failed state after
# --- Resource limits ---
LimitNOFILE=65535 # Open file descriptors (overrides /etc/security/limits.conf)
LimitNPROC=4096 # Max subprocesses
MemoryMax=2G # OOM-kill process if it exceeds 2 GB (cgroup-based)
CPUQuota=200% # Max 2 CPU cores (200% of one core = 2 cores)
TasksMax=512 # Max number of tasks (threads + processes)
# --- Output logging ---
StandardOutput=journal # stdout → journal
StandardError=journal # stderr → journal
SyslogIdentifier=myapp # Tag in journal (default: unit name without .service)
# --- Security hardening ---
PrivateTmp=true # Mount private /tmp and /var/tmp (other services can't see them)
NoNewPrivileges=true # Prevent setuid/setgid; process can't gain more privileges
ProtectSystem=strict # /usr, /boot, /etc are read-only
ProtectHome=true # /home, /root, /run/user are inaccessible
ReadWritePaths=/var/lib/myapp /var/log/myapp # Exceptions to ProtectSystem=strict
PrivateDevices=true # No access to physical devices
ProtectKernelTunables=true # Block writes to /proc/sys
ProtectKernelModules=true # Block kernel module loading
ProtectControlGroups=true # Block cgroup manipulation
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX # Restrict socket families
RestrictNamespaces=true # Block namespace creation
SystemCallFilter=@system-service # Whitelist common syscalls; block dangerous ones
# --- Timeout ---
TimeoutStartSec=30s # Fail if not ready within 30s (for Type=notify)
TimeoutStopSec=30s # Force-kill after 30s if graceful stop takes too long
[Install]
WantedBy=multi-user.target # Enable via: systemctl enable myapp
```
---
## EnvironmentFile Pattern for Secrets
```bash
# /etc/myapp/environment (mode 600, owned by root or myapp user)
DATABASE_URL=postgresql://myapp:secret@localhost:5432/myapp_prod
SECRET_KEY=your-256-bit-random-key-here
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
REDIS_URL=redis://:password@localhost:6379/0
```
```bash
# Set correct permissions
sudo install -m 600 -o root -g root /dev/stdin /etc/myapp/environment <<'EOF'
DATABASE_URL=...
EOF
# Or for apps running as a specific user
sudo install -m 640 -o root -g myapp /dev/stdin /etc/myapp/environment <<'EOF'
DATABASE_URL=...
EOF
```
The `EnvironmentFile` path in the unit file reads each `KEY=VALUE` line. Lines starting with `#` are ignored. A leading `-` makes the file optional: `EnvironmentFile=-/etc/myapp/environment`.
---
## Timer Unit — Daily Backup Job
### /etc/systemd/system/backup-myapp.service
```ini
[Unit]
Description=Daily backup of myapp database
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
User=backup
Group=backup
EnvironmentFile=/etc/myapp/environment
ExecStart=/opt/myapp/scripts/backup.sh
StandardOutput=journal
StandardError=journal
SyslogIdentifier=backup-myapp
```
### /etc/systemd/system/backup-myapp.timer
```ini
[Unit]
Description=Run myapp backup daily at 02:30 UTC
Requires=backup-myapp.service
[Timer]
# OnCalendar format: DayOfWeek Year-Month-Day Hour:Minute:Second
# Shortcuts: minutely, hourly, daily, weekly, monthly, yearly, quarterly
OnCalendar=*-*-* 02:30:00 # Every day at 02:30 UTC
Persistent=true # If timer was missed (machine off), run immediately on next boot
RandomizedDelaySec=10min # Jitter to avoid thundering herd on multiple servers
AccuracySec=1min # Allow 1 minute timing inaccuracy for better power management
[Install]
WantedBy=timers.target
```
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now backup-myapp.timer
# Verify timer status
sudo systemctl list-timers backup-myapp
sudo systemctl status backup-myapp.timer
# Trigger immediately (for testing)
sudo systemctl start backup-myapp.service
```
### OnCalendar Syntax Examples
```
OnCalendar=minutely # Every minute
OnCalendar=hourly # Every hour at :00
OnCalendar=daily # Every day at 00:00
OnCalendar=weekly # Every Monday at 00:00
OnCalendar=monthly # First day of month at 00:00
OnCalendar=*-*-* 02:30:00 # Every day at 02:30
OnCalendar=Mon *-*-* 03:00:00 # Every Monday at 03:00
OnCalendar=*-*-1 04:00:00 # First of every month at 04:00
OnCalendar=*-*-* *:0/15:00 # Every 15 minutes
OnCalendar=Mon..Fri *-*-* 09:00:00 # Weekdays at 09:00
# Verify calendar expression without executing
systemd-analyze calendar "Mon *-*-* 09:00:00"
```
---
## Drop-In Overrides
Override a package-provided unit without editing the original file:
```bash
# Opens editor; creates /etc/systemd/system/nginx.service.d/override.conf
sudo systemctl edit nginx
# Create override manually
sudo mkdir -p /etc/systemd/system/nginx.service.d/
sudo tee /etc/systemd/system/nginx.service.d/limits.conf > /dev/null <<'EOF'
[Service]
# Raise open file descriptor limit for Nginx
LimitNOFILE=65535
# Add environment variable
Environment=MALLOC_ARENA_MAX=2
EOF
sudo systemctl daemon-reload
sudo systemctl restart nginx
# Show effective merged unit (original + all drop-ins)
sudo systemctl cat nginx
```
Drop-in files in `<name>.service.d/` are applied alphabetically. Use numeric prefixes (e.g., `10-limits.conf`, `20-env.conf`) to control order.
---
## Socket Activation
systemd can hold a listening socket and pass it to the service on first connection. The service doesn't need to be running all the time.
### /etc/systemd/system/myapp.socket
```ini
[Unit]
Description=myapp socket activation
[Socket]
ListenStream=127.0.0.1:3000 # Or: ListenStream=/run/myapp/myapp.sock
Accept=false # false = pass socket fd to service (not per-connection)
[Install]
WantedBy=sockets.target
```
The service unit must accept the socket via `sd_listen_fds()` (Node.js: `LISTEN_FDS` env var, or use `systemd` npm package). Enable the socket, not the service:
```bash
sudo systemctl enable --now myapp.socket
# systemd starts myapp.service automatically on first connection
```
---
## systemctl Command Reference
```bash
# Start / stop / restart / reload
sudo systemctl start myapp
sudo systemctl stop myapp
sudo systemctl restart myapp
sudo systemctl reload myapp # Send reload signal (ExecReload); service must support it
# Enable / disable (controls WantedBy symlinks in /etc/systemd/system/multi-user.target.wants/)
sudo systemctl enable myapp # Enable but don't start
sudo systemctl enable --now myapp # Enable and start immediately
sudo systemctl disable myapp # Disable; running instance continues until stopped
sudo systemctl disable --now myapp # Disable and stop
# Mask / unmask (make impossible to start, even manually)
sudo systemctl mask myapp
sudo systemctl unmask myapp
# Reload unit files from disk (ALWAYS run after editing unit files)
sudo systemctl daemon-reload
# Status and inspection
sudo systemctl status myapp
sudo systemctl is-active myapp # Returns "active" or "inactive" (exit code 0/non-0)
sudo systemctl is-enabled myapp
sudo systemctl is-failed myapp
sudo systemctl cat myapp # Show effective unit file (with drop-ins)
sudo systemctl show myapp # Show all unit properties
# List units
sudo systemctl list-units --type=service --state=running
sudo systemctl list-units --type=timer
sudo systemctl list-timers --all
sudo systemctl list-unit-files --type=service | grep myapp
# Dependency tree
sudo systemctl list-dependencies myapp
sudo systemctl list-dependencies --reverse myapp # Who depends on myapp?
```
---
## journalctl Cheat-Sheet
```bash
# Follow logs for a specific unit (like tail -f)
journalctl -u myapp -f
# Last N lines
journalctl -u myapp -n 100
# Since / until (accepts many formats)
journalctl -u myapp --since "2024-01-15 14:00:00" --until "2024-01-15 15:00:00"
journalctl -u myapp --since "1 hour ago"
journalctl -u myapp --since today
# Filter by priority level
# 0=emerg, 1=alert, 2=crit, 3=err, 4=warning, 5=notice, 6=info, 7=debug
journalctl -u myapp -p err # Errors and above
journalctl -u myapp -p warning # Warnings and above
# No pager (output all at once — useful in scripts)
journalctl -u myapp --no-pager
# Boot logs
journalctl -b # Current boot
journalctl -b -1 # Previous boot
journalctl --list-boots # All recorded boots
# Kernel messages
journalctl -k # Equivalent to dmesg
# JSON output (for log shipping to Elasticsearch etc.)
journalctl -u myapp -n 100 --output=json | jq '.'
journalctl -u myapp -n 100 --output=json-pretty
# Plain message text only
journalctl -u myapp --output=cat
# Filter by field (systemd journal fields)
journalctl _SYSTEMD_UNIT=myapp.service
journalctl _PID=12345
journalctl _UID=1001
# Disk usage
journalctl --disk-usage
# Vacuum by size or time
sudo journalctl --vacuum-size=500M
sudo journalctl --vacuum-time=30d
# Export to file (for archival)
journalctl -u myapp --since "2024-01-01" --output=export > /tmp/myapp-jan.journal
```
---
## Anti-Patterns
| Anti-pattern | Why it's harmful | Fix |
|---|---|---|
| Running service as `root` | Full system compromise on any exploit in the service | Set `User=` and `Group=` to a dedicated unprivileged user |
| No `Restart=` directive | Service crash requires manual restart or monitoring system intervention | Set `Restart=on-failure`; add `RestartSec=5s` to rate-limit restarts |
| No `LimitNOFILE=` in service unit | Service hits OS default (typically 1024) FD limit → "too many open files" | Set `LimitNOFILE=65535` in `[Service]`; `limits.conf` does not affect systemd services |
| `ExecStart` with shell globs or pipes | systemd does not use a shell; globs not expanded; pipes create wrong PID | Wrap in a shell: `ExecStart=/bin/bash -c 'cmd1 | cmd2'`; or better, write a wrapper script |
| Not running `daemon-reload` after editing unit files | systemd continues using the cached (old) unit definition | Always run `sudo systemctl daemon-reload` before restart |
| `Type=simple` for slow-starting services | systemd may start dependents before the service is ready | Use `Type=notify` (if app supports sd_notify) or `Type=forking` + `PIDFile=`; or add `ExecStartPost` health check |
| Putting secrets in `Environment=` directly in unit file | Unit file is world-readable in `systemctl cat` output and journald | Use `EnvironmentFile=/etc/myapp/env` with `chmod 600` |
| `ProtectSystem=strict` without `ReadWritePaths=` | App can't write logs, cache, or state files → silent failures | Add `ReadWritePaths=/var/lib/myapp /var/log/myapp` |
| Timer `Persistent=false` for critical jobs | If server is down at scheduled time, job is silently skipped | Set `Persistent=true` so missed jobs run on next boot |
| Using `KillMode=none` to avoid killing child processes | Orphaned children not cleaned up when service stops; zombies accumulate | Default `KillMode=control-group` kills entire cgroup; use `ExecStop` for graceful drain |
---
## Troubleshooting
| Symptom | Likely cause | Diagnostic / Fix |
|---|---|---|
| **Service fails to start** | ExecStart path wrong, permission error, or missing dependency | `sudo systemctl status myapp -l`; `journalctl -u myapp -n 50`; test ExecStart manually as the service user |
| **Environment variables not loaded** | `EnvironmentFile` path wrong, file permissions, or quoting issue | Check `sudo systemctl show myapp \| grep Env`; `cat /etc/myapp/environment`; verify `EnvironmentFile=-` makes it optional |
| **"Failed to open file for writing: Permission denied"** | `ProtectSystem=strict` blocks write to path not in `ReadWritePaths=` | Add path to `ReadWritePaths=`; reload daemon; restart service |
| **Timer not firing** | Timer not enabled, missed due to Persistent=false, or clock wrong | `systemctl list-timers`; `systemctl status backup.timer`; `timedatectl` to check clock |
| **Service restart loop** | Crash in startup; `StartLimitBurst` exceeded | `journalctl -u myapp -n 100`; fix root cause; then `systemctl reset-failed myapp` before restarting |
| **Logs not appearing in journal** | App writes to a file instead of stdout/stderr | Set `StandardOutput=journal`; redirect app logging to stdout; or use `systemd-cat` as a pipe |
| **Drop-in not applied** | File in wrong directory, wrong extension, or daemon not reloaded | Must be in `/etc/systemd/system/<name>.service.d/`; must end in `.conf`; run `daemon-reload` |
| **Socket activation: service not starting on connection** | `Accept=false` set but service not calling `sd_listen_fds()` correctly | Check `LISTEN_FDS` env var; verify `ListenStream` address; `systemctl status myapp.socket` |
| **`systemctl cat` shows old unit** | `daemon-reload` not run after editing | `sudo systemctl daemon-reload` then verify with `systemctl cat` |
| **OOM-killer stops service** | `MemoryMax` too low, or system under memory pressure | `journalctl -k \| grep -i oom`; raise `MemoryMax=`; add swap; profile memory usage |
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!