Best AI Tools for Writing Systemd Units 2026
Systemd units look simple but have hundreds of options — many of which interact in non-obvious ways. AI tools vary significantly in how well they handle security hardening, resource limits, and dependency ordering. This guide covers real prompts and outputs for service units, timer units, socket activation, and path units, with a direct comparison between Claude and ChatGPT.
Why AI Tools Matter for Systemd
Writing a minimal systemd service unit is easy. Writing one that correctly handles security sandboxing, restart policies, resource limits, dependency ordering, and logging takes significantly more knowledge. The systemd man pages are thorough but dense — systemd.exec(5) alone covers hundreds of directives.
AI tools collapse that research time. The key question is whether they apply the right directives by default, understand the interactions between them, and flag common mistakes before you discover them in production.
Service Unit: Production Web App
Prompt to Claude:
Write a systemd service unit for a Node.js web server.
Binary: /usr/local/bin/node /opt/myapp/server.js
User: myapp, Group: myapp
Environment: PORT=3000, NODE_ENV=production
Auto-restart on failure with backoff. Start after network-online.target.
Apply security hardening: no new privileges, private tmp, protect system, restrict address families.
Limit to 2 CPU shares and 512MB memory.
Claude output:
[Unit]
Description=MyApp Node.js Web Server
Documentation=https://github.com/example/myapp
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=myapp
Group=myapp
ExecStart=/usr/local/bin/node /opt/myapp/server.js
WorkingDirectory=/opt/myapp
Restart=on-failure
RestartSec=5
RestartBurst=5
TimeoutStartSec=30
TimeoutStopSec=30
# Environment
Environment=PORT=3000
Environment=NODE_ENV=production
# Prefer EnvironmentFile for secrets:
# EnvironmentFile=/etc/myapp/env
# Security hardening
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictNamespaces=true
RestrictRealtime=true
RestrictSUIDSGID=true
LockPersonality=true
MemoryDenyWriteExecute=true
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
ReadWritePaths=/opt/myapp/data /var/log/myapp
# Resource limits
CPUShares=200
MemoryMax=512M
TasksMax=256
[Install]
WantedBy=multi-user.target
This is a production-quality unit. Claude adds Wants=network-online.target (not just After=) so systemd actually waits for a real network connection, not just the systemd-networkd socket. It uses SystemCallFilter=@system-service which restricts to a safe set of syscalls rather than trying to enumerate them individually — this is the maintainable approach.
One note: MemoryDenyWriteExecute=true can break JIT-enabled runtimes like V8 (Node.js). Claude flags this in its explanation, but will include it by default — remove it for Node.js, Python, or any JIT language.
Comparison: ChatGPT
ChatGPT produces syntactically correct units but tends to use weaker hardening defaults:
- Uses
After=network.targetinstead ofnetwork-online.target(the difference:network.targetis reached when systemd-networkd starts,network-online.targetrequires an actual routable IP) - Omits
ProtectKernelModules,RestrictNamespaces, andSystemCallFilter - Sets
Restart=alwaysinstead ofRestart=on-failure, which restarts even on clean exit
For teams that use systemd-analyze security to audit units, ChatGPT’s output typically scores “UNSAFE” or “EXPOSED” without additional prompting. Claude’s output usually lands at “MEDIUM” out of the box.
Timer Unit (Scheduled Task)
Prompt:
Write a systemd timer that runs /opt/scripts/backup.sh daily at 02:30 local time.
The service should not run if the previous run is still active. Add persistent=true.
Include both the .timer and .service unit files.
Claude output:
# /etc/systemd/system/daily-backup.service
[Unit]
Description=Daily backup script
After=local-fs.target network-online.target
ConditionACPower=true
[Service]
Type=oneshot
ExecStart=/opt/scripts/backup.sh
User=backup
Group=backup
StandardOutput=journal
StandardError=journal
# Hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/var/backups /opt/data
TimeoutStartSec=3600
# /etc/systemd/system/daily-backup.timer
[Unit]
Description=Run daily backup at 02:30
[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
AccuracySec=1min
[Install]
WantedBy=timers.target
Persistent=true is important — it means if the system was off at 02:30, the timer fires immediately when the system boots (catches missed runs). ConditionACPower=true prevents the backup from running on battery.
Note that systemd timers use Type=oneshot for the service (not Type=simple) — Claude gets this right. ChatGPT sometimes generates Type=simple for timer services, which can cause issues with service tracking.
The “service should not run if the previous run is still active” requirement is handled by Type=oneshot combined with no Restart= directive. If you add Restart=on-failure to an oneshot service, it only retries on non-zero exit — it does not overlap with a running instance.
Socket Activation
Prompt:
Write systemd socket activation units for a Python socket server.
The socket should listen on port 8080. Use socket activation so the service
starts on first connection.
# /etc/systemd/system/myserver.socket
[Unit]
Description=MyServer socket activation
[Socket]
ListenStream=8080
Accept=false
NoDelay=true
[Install]
WantedBy=sockets.target
# /etc/systemd/system/myserver.service
[Unit]
Description=MyServer (socket-activated)
Requires=myserver.socket
After=myserver.socket
[Service]
Type=simple
ExecStart=/usr/bin/python3 /opt/myserver/server.py
User=myserver
StandardInput=socket
StandardOutput=journal
StandardError=journal
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
In the Python server, use systemd.daemon.notify and read from sys.stdin (fd 0) when socket-activated, or use socket.fromfd(3, ...) to get the pre-bound socket.
Accept=false means systemd passes the listening socket to one service instance (not a new instance per connection). For most servers this is the right choice — the application handles accept() itself.
Path Unit (File Watcher)
Prompt:
Write a systemd path unit that watches /var/spool/incoming/ for new files
and triggers a processing service when files appear. The service should run
/opt/scripts/process_incoming.sh.
## Service Dependencies and Ordering
**Prompt:**
```text
I have three services:
- database.service (PostgreSQL)
- cache.service (Redis)
- api.service (Node.js app, depends on both)
Write the systemd units with proper dependency ordering
and ensure the app doesn't start before both are ready.
Claude output:
# /etc/systemd/system/incoming-watcher.path
[Unit]
Description=Watch /var/spool/incoming for new files
[Path]
PathChanged=/var/spool/incoming
Unit=incoming-processor.service
# /etc/systemd/system/database.service
[Unit]
Description=PostgreSQL Database
After=network-online.target
Wants=network-online.target
[Service]
Type=notify
ExecStart=/usr/lib/postgresql/15/bin/postgres -D /var/lib/postgresql/15/main
ExecReload=/bin/kill -HUP $MAINPID
User=postgres
Group=postgres
Restart=on-failure
RestartSec=10
# Allow 60 seconds for startup (DB can be slow)
TimeoutStartSec=60
[Install]
WantedBy=multi-user.target
---
# /etc/systemd/system/cache.service
[Unit]
Description=Redis Cache
After=network-online.target
Wants=network-online.target
[Service]
Type=notify
ExecStart=/usr/bin/redis-server /etc/redis/redis.conf
ExecReload=/bin/kill -HUP $MAINPID
User=redis
Group=redis
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
---
# /etc/systemd/system/api.service
[Unit]
Description=API Server
# After: this service starts after the listed services
# Requires: if these services fail/stop, this service stops
After=database.service cache.service
Requires=database.service cache.service
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/bin/node /opt/api/server.js
User=api
Group=api
Environment=NODE_ENV=production
Restart=on-failure
RestartSec=10
# Don't start this service if dependencies are not running
PartOf=database.service cache.service
[Install]
WantedBy=multi-user.target
# /etc/systemd/system/incoming-processor.service
[Unit]
Description=Process incoming files
After=local-fs.target
[Service]
Type=oneshot
ExecStart=/opt/scripts/process_incoming.sh
User=processor
Group=processor
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/var/spool/incoming /var/spool/processed
StandardOutput=journal
StandardError=journal
PathChanged triggers when a file in the directory is modified or created. Use PathExistsGlob if you want to trigger on a specific file pattern. Claude explains the difference between PathExists, PathChanged, and PathModified in its response — a useful detail that ChatGPT omits.
Drop-In Override Files
One area where Claude demonstrates deeper systemd knowledge is override files. When you ask about modifying a vendor-provided unit without editing the original:
Prompt:
I want to add MemoryMax=1G to the nginx.service unit that came from the distro package,
without editing the original file so package updates don't overwrite my changes.
Claude output:
# Create override directory and file
sudo mkdir -p /etc/systemd/system/nginx.service.d/
sudo tee /etc/systemd/system/nginx.service.d/memory-limit.conf << 'EOF'
[Service]
MemoryMax=1G
EOF
# Reload systemd and restart nginx
sudo systemctl daemon-reload
sudo systemctl restart nginx
# Verify the override is applied
systemctl show nginx --property=MemoryMax
Claude explains that drop-in files in .service.d/ directories merge with the base unit — you only need to include the directives you’re changing. ChatGPT sometimes suggests editing /lib/systemd/system/nginx.service directly, which is overwritten on package updates.
The key distinction: After orders startup but allows independent operation. Requires makes the dependency mandatory — if the database crashes, the API service stops too. PartOf further ties the lifecycle — stopping the database stops the API.
Drop-in Overrides
Prompt:
I have a third-party service (nginx.service) that starts at boot.
I want to override its ExecStart, add environment variables,
and change the restart policy without modifying the original unit file.
Claude output:
# Create a drop-in directory
mkdir -p /etc/systemd/system/nginx.service.d
# Create override file
cat > /etc/systemd/system/nginx.service.d/custom.conf <<'EOF'
[Service]
# Override ExecStart
ExecStart=
ExecStart=/usr/sbin/nginx -g "daemon off; worker_processes auto;"
# Add environment variables
Environment="NGINX_PORT=8080"
Environment="NGINX_LOG_LEVEL=warn"
# Change restart behavior
Restart=always
RestartSec=5
# Add logging
StandardOutput=journal
StandardError=journal
EOF
# Reload systemd
systemctl daemon-reload
# Verify the override
systemctl cat nginx.service
# Restart with new config
systemctl restart nginx
Note: ExecStart= (with no value) clears the original, then the next line sets the new one. Drop-in overrides are packaged better than editing the unit file directly since they survive package updates.
Runtime Service with systemd-run
Prompt:
I want to run a one-off background task (cleanup script) with systemd,
tracking it like a service. Use systemd-run for temporary units.
Claude output:
# Create and start a background service at runtime
systemd-run --unit=cleanup-task \
--working-directory=/opt/app \
--slice=user-tasks.slice \
--remain-after-exit \
/opt/scripts/cleanup.sh
# Monitor it
systemctl status cleanup-task
# View output
journalctl -u cleanup-task -f
# Clean up when done
systemctl stop cleanup-task
systemctl reset-failed cleanup-task
This is useful for one-off tasks that you want tracked by systemd (logs, resource limits, automatic restarts) without writing a permanent unit file.
Debugging Advice Claude Includes
Claude consistently adds debugging commands in its explanations:
# Validate unit syntax
systemd-analyze verify /etc/systemd/system/myapp.service
# Check security score
systemd-analyze security myapp.service
# View logs
journalctl -u myapp -f
# Test resource limits
systemctl show myapp --property=MemoryMax,CPUShares,TasksMax
# Show full unit after merging drop-ins
systemctl cat myapp.service
# Check timer next run time
systemctl list-timers daily-backup.timer
The systemd-analyze security command scores your hardening — Claude mentions this tool and suggests aiming for “MEDIUM” exposure level or better. Scores range from “OK” (best) through “MEDIUM”, “EXPOSED”, and “UNSAFE”. Most default service units from distro packages score “UNSAFE” — adding the hardening directives Claude generates by default typically brings a custom service to “MEDIUM” without additional effort.
Transient Units and systemd-run
For one-off tasks and testing, Claude also generates systemd-run commands that create transient units without writing files:
Prompt:
Show me how to run a one-off command using systemd-run with memory and CPU limits,
and how to check its output.
Claude output:
# Run a command as a transient service with resource limits
systemd-run \
--unit=my-oneoff \
--property=MemoryMax=256M \
--property=CPUShares=100 \
--property=User=myapp \
--wait \
/opt/scripts/heavy-task.sh
# Check status
systemctl status my-oneoff
# View logs
journalctl -u my-oneoff
# Run interactively (stays in foreground)
systemd-run --pty --property=MemoryMax=256M /bin/bash
systemd-run is useful for testing resource limits before encoding them in a unit file — run the command, watch systemctl status, confirm memory doesn’t exceed the limit, then copy those values into the permanent unit. Claude explains this workflow; ChatGPT typically presents systemd-run as a standalone command without the testing workflow context.
When to Use Each Tool
Use Claude when you need production-quality units with complete hardening. The additional directives it includes by default (SystemCallFilter, ProtectKernelModules, RestrictNamespaces) require systemd 232+ but are supported on any modern Linux distribution. Claude also provides better explanations of what each directive does and why — useful when you’re learning systemd.
Use ChatGPT for quick scaffolding when you know you’ll review the hardening separately. Its units are syntactically correct and follow the basic structure, but plan to add hardening directives yourself before deploying to production.
For either tool: always run systemd-analyze verify before deploying, and systemd-analyze security if the service runs with elevated privileges or handles sensitive data.
Related Reading
- Best AI Tools for Writing Makefiles
- How to Use AI for Nix Package Management
-
AI Assistants for Writing Correct AWS IAM Policies
Related Articles
- Best AI Tools for Writing Ansible Playbooks
- AI Tools for Writing Jest Tests for Web Worker and Service
- Best AI Tools for Writing Kubernetes Custom Resource
- Best AI Tools for Technical Documentation Writing in 2026
- AI Tools for Writing Terraform Infrastructure-as-Code
Built by theluckystrike — More at zovo.one ```