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

GitHub
安装命令
npx skhub add claude-dev-suite/systemd
Markdown
SKILL.md

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:

[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

# /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
# 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

[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

[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
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:

# 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

[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:

sudo systemctl enable --now myapp.socket
# systemd starts myapp.service automatically on first connection

systemctl Command Reference

# 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

# 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-patternWhy it's harmfulFix
Running service as rootFull system compromise on any exploit in the serviceSet User= and Group= to a dedicated unprivileged user
No Restart= directiveService crash requires manual restart or monitoring system interventionSet Restart=on-failure; add RestartSec=5s to rate-limit restarts
No LimitNOFILE= in service unitService 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 pipessystemd does not use a shell; globs not expanded; pipes create wrong PIDWrap in a shell: `ExecStart=/bin/bash -c 'cmd1
Not running daemon-reload after editing unit filessystemd continues using the cached (old) unit definitionAlways run sudo systemctl daemon-reload before restart
Type=simple for slow-starting servicessystemd may start dependents before the service is readyUse Type=notify (if app supports sd_notify) or Type=forking + PIDFile=; or add ExecStartPost health check
Putting secrets in Environment= directly in unit fileUnit file is world-readable in systemctl cat output and journaldUse EnvironmentFile=/etc/myapp/env with chmod 600
ProtectSystem=strict without ReadWritePaths=App can't write logs, cache, or state files → silent failuresAdd ReadWritePaths=/var/lib/myapp /var/log/myapp
Timer Persistent=false for critical jobsIf server is down at scheduled time, job is silently skippedSet Persistent=true so missed jobs run on next boot
Using KillMode=none to avoid killing child processesOrphaned children not cleaned up when service stops; zombies accumulateDefault KillMode=control-group kills entire cgroup; use ExecStop for graceful drain

Troubleshooting

SymptomLikely causeDiagnostic / Fix
Service fails to startExecStart path wrong, permission error, or missing dependencysudo systemctl status myapp -l; journalctl -u myapp -n 50; test ExecStart manually as the service user
Environment variables not loadedEnvironmentFile path wrong, file permissions, or quoting issueCheck 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 firingTimer not enabled, missed due to Persistent=false, or clock wrongsystemctl list-timers; systemctl status backup.timer; timedatectl to check clock
Service restart loopCrash in startup; StartLimitBurst exceededjournalctl -u myapp -n 100; fix root cause; then systemctl reset-failed myapp before restarting
Logs not appearing in journalApp writes to a file instead of stdout/stderrSet StandardOutput=journal; redirect app logging to stdout; or use systemd-cat as a pipe
Drop-in not appliedFile in wrong directory, wrong extension, or daemon not reloadedMust be in /etc/systemd/system/<name>.service.d/; must end in .conf; run daemon-reload
Socket activation: service not starting on connectionAccept=false set but service not calling sd_listen_fds() correctlyCheck LISTEN_FDS env var; verify ListenStream address; systemctl status myapp.socket
systemctl cat shows old unitdaemon-reload not run after editingsudo systemctl daemon-reload then verify with systemctl cat
OOM-killer stops serviceMemoryMax too low, or system under memory pressurejournalctl -k | grep -i oom; raise MemoryMax=; add swap; profile memory usage
发现
标签

此技能尚未发布标签。

版本
最新版本元数据

版本

v2026.09.24

发布时间

Sep 24, 2026

分类

未分类

许可证

MIT

源路径

skills/infrastructure/systemd

默认分支

main

最新提交

9496306

Tree SHA

fe4e2f1