Generate a properly-formatted systemd unit file from a command, restart policy, env vars, and user — with security-hardening defaults baked in
Security hardening (recommended on)
Generated unit file
Save as /etc/systemd/system/myapp.service
After saving: sudo systemctl daemon-reload && sudo systemctl enable --now myapp.service
systemd Unit File Generator produces a complete, properly-formatted .service file from a form — no manual lookup of directives, no copy-paste from outdated StackOverflow answers. Security-hardening directives are baked in by default. Output includes inline comments explaining each directive. Copy, install, enable, and run.
on-failure (default), always, no, or on-abnormal.KEY=VALUE pairs or an EnvironmentFile path.Install and enable:
sudo cp myservice.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable myservice
sudo systemctl start myservice
Check status:
systemctl status myservice
journalctl -u myservice -f
The gap between “running a process” and “running a production service” is exactly the gap between nohup ./app & and a proper systemd unit file.
nohup ./app & puts a process in the background. It doesn’t restart if the process crashes. It doesn’t start at boot. It runs as root (unless you manually sudo -u) . The process has access to your entire filesystem, all your devices, and can load kernel modules. If the process is compromised or buggy, it has maximum blast radius.
A systemd unit with proper security hardening:
/home, can’t write to /usr or /etc/tmp — can’t read files other services left in shared tmpThe security directives aren’t optional hardening — they’re the correct default for any service you didn’t explicitly design to need root or broad filesystem access.
These are baked in by default and togglable with reason:
| Directive | Effect | Attack surface closed |
|---|---|---|
NoNewPrivileges=true | Prevents setuid binary execution | Privilege escalation via compromised binary |
ProtectSystem=strict | Mounts /usr, /boot, /etc read-only | Persistent filesystem modification |
ProtectHome=true | Hides /home, /root, /run/user | Credential theft from user home directories |
PrivateTmp=true | Per-service /tmp — not shared | Tmp file snooping and symlink attacks across services |
PrivateDevices=true | No /dev except null, zero, random | Raw device access, disk reads from other partitions |
ProtectKernelTunables=true | /proc/sys, /sys read-only | Kernel parameter manipulation |
ProtectKernelModules=true | Cannot load/unload kernel modules | Rootkit-via-module insertion |
ProtectControlGroups=true | /sys/fs/cgroup read-only | Container escape via cgroup manipulation |
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX | Blocks AF_NETLINK, AF_PACKET, etc. | Raw network packet capture, low-level netlink operations |
RestrictNamespaces=true | Cannot create new namespaces | Container escape vectors |
LockPersonality=true | Cannot change exec personality | ABI compatibility attack vectors |
MemoryDenyWriteExecute=true | No writable+executable memory | JIT-based shellcode injection |
The generator includes all of these with comments explaining each one. You can toggle off any directive that your specific service legitimately requires — for example, PrivateDevices needs to be off for services that access hardware devices directly.
systemd-analyze security OutputAfter installing a unit, systemd-analyze security myservice.service gives it a security score and a table of each directive’s status. The score ranges from 0 (fully exposed) to 10 (fully hardened). The table shows which directives are applied and which are missing.
A typical unprotected service scores 4–5. A service generated by this tool with all hardening directives scores 8–9. Scores above 9 require additional hardening that most services don’t need (system call filtering via SystemCallFilter, capability dropping beyond NoNewPrivileges, MemoryAccounting).
The output looks like:
→ Overall exposure level for myservice.service: 2.4 SAFE
✓ PrivateTmp= Service has private tmp namespace
✓ NoNewPrivileges= Service cannot gain new privileges
✗ SystemCallFilter= System call allow list not defined
Use systemd-analyze security after every new unit file to confirm the hardening is taking effect.
Running an LLM inference server (llama.cpp, Ollama, vLLM lite) on a Raspberry Pi 5 or similar edge hardware as a persistent service is a real use case. The requirements are specific:
Restart=on-failure with RestartSec=10s handles this automatically.MemoryMax=3G on a 4GB Pi prevents total system lockup.CPUQuota=80% leaves headroom for system tasks and SSH.WorkingDirectory=/opt/models makes this explicit./etc/myservice/env) with restricted permissions (chmod 600), not hardcoded in the unit file.adduser --system --no-create-home mlserve) and run the inference server as that user. It doesn’t need root for any inference task.The generated unit file for an inference service pairs naturally with the Docker Compose Visualizer approach — use Docker if you need container isolation; use systemd if you want native performance and simpler resource accounting on constrained hardware.
journalctl -u myservice -f immediately after starting the service. The first 30 seconds often reveal permission issues that the security directives caused — a service trying to write to a path that’s now read-only, for example. Fix these by removing only the specific directives your service needs.EnvironmentFile= for secrets. Inline Environment=API_KEY=secret in the unit file is readable by systemctl show. Use EnvironmentFile=/etc/myservice/env with the file permissions set to 600 and owned by the service user.After=network.target for any service that makes network connections on startup. Without this, the service may start before the network interface is configured, fail to connect, and exit.oneshot with RemainAfterExit=yes is the right pattern for initialization scripts — tasks that run once at boot and don’t keep a process running. The service appears as “active” even after the script exits.MemoryMax=3G triggers OOM kill when the service exceeds 3 GB. It doesn’t prevent the service from accumulating memory up to that point. Watch systemctl status and journalctl for OOM kill events..service only, not .socket companions for socket activation/etc/systemd/system/); user services (~/.config/systemd/user/) have the same format but different install paths and no network access by default.timer unit is left as an exercise; systemd-cron handles the translation.d/ directories) let you extend a unit without modifying the original, which is useful for distribution-managed servicesMemoryDenyWriteExecute breaks JIT compilers (Node.js V8, JVM JIT). Toggle off for services that use JIT compilation.For informational purposes only. Not financial, medical, or legal advice. You are solely responsible for how you use these tools.