systemd Unit Generator
Service and timer units with restart policies, users and hardening options.
● Runs 100% in your browser — nothing you type leaves this page.
# Generated by OhSudo — https://ohsudo.com/tools/systemd-unit-generator/[Unit]Description=My application# Start after these units (and pull them in); network-online waits for a configured networkWants=network-online.targetAfter=network-online.target[Service]# How systemd knows the service has startedType=simple# Run as this user, not rootUser=myappGroup=myappWorkingDirectory=/opt/myapp# The program and its arguments (full path; this is not a shell)ExecStart=/opt/myapp/bin/server --port 8080# Environment variables for the programEnvironment=NODE_ENV=production# Restart automatically, after a pauseRestart=on-failureRestartSec=5# Basic hardening: no privilege escalation, a private /tmp, /usr /boot /etc read-onlyNoNewPrivileges=yesPrivateTmp=yesProtectSystem=full# No access to /home, /root and /run/userProtectHome=yes# Start at boot (systemctl enable)[Install]WantedBy=multi-user.target
Install it
$ sudo useradd --system --no-create-home --shell /usr/sbin/nologin myapp$ sudo nano /etc/systemd/system/myapp.service$ sudo systemctl daemon-reload$ sudo systemctl enable --now myapp.service$ systemctl status myapp.service$ journalctl -u myapp.service -fPaste each file, save, then reload systemd and start it. Logs: journalctl -u <name>.
The address bar updates as you go, so you can bookmark or share the result.
How to use it
- Give the service a name and the command to run — with the full path of the program (
/usr/bin/node, notnode). - Choose the user it runs as. A dedicated user (the install steps create it) is safer than root.
- Decide how it runs: all the time, restarted if it crashes — or on a schedule with a
.timer, the systemd alternative to cron. - Copy the file(s) to
/etc/systemd/system/, then run the commands under the result.
What you get
- Restart on failure, with a pause between tries, so a crash doesn’t take your app down for good.
- Start order: after the network is really up, and after the services it needs (
postgresql.service…). - Limits that matter in production:
MemoryMax=makes the kernel stop this service when it uses too much memory, instead of a random process (see Linux process “Killed”);LimitNOFILE=raises the open files limit, whichlimits.confdoesn’t do for services (see Too many open files). - Basic hardening: no privilege escalation, a private
/tmp, system folders read-only. - Timers with catch-up after downtime (
Persistent=true) and a random delay, logged in the journal like every service.
Common commands
sudo systemctl daemon-reload # after creating or editing a unit filesudo systemctl enable --now myapp # start now and at bootsystemctl status myapp # state and last log linesjournalctl -u myapp -f # follow the logssudo systemctl edit myapp # change settings in a drop-in, without touching the filesystemctl list-timers # when every timer runs nextsystemd-analyze calendar "Mon..Fri 08:30" # check an OnCalendar expressionsystemctl list-unit-files 'myapp*' # is the unit installed, under which nameWarning
Secrets don’t belong in the unit file. Every user on the machine can read it (systemctl cat, systemctl show). Put passwords and tokens in an EnvironmentFile owned by root with mode 600.
FAQ
Why does my command work in the shell but not in ExecStart?
ExecStart= is not a shell. There is no PATH from your profile, no cd, no &&, | or > redirections, no ~. Use full paths and WorkingDirectory=; if you really need shell syntax, run it through /bin/sh -c '…' — or better, put it in a script. If the service fails to start, see Job for … failed because the control process exited with error code.
simple, exec, notify or oneshot?
simple for most apps that run in the foreground. exec is the same, but systemctl start already fails if the program can’t even be started (wrong path, permissions). notify only if the app tells systemd it’s ready (many daemons can: nginx, PostgreSQL, some app servers). oneshot for scripts that do something and exit — that’s what timers run.
Timer or cron?
Both work. A timer logs every run in the journal, can catch up after the machine was off, doesn’t run twice if the previous run is still going, and uses the same limits and users as services. Cron is simpler for a quick one-liner — see the Cron Builder and Cron job not running.
Which commands do I need to manage the service?
They’re in the systemctl and journalctl Cheat Sheet.
Where can I practice managing services?
In the lesson Services: systemctl and journalctl.
Articles
Learn more
Nginx for Laravel with PHP-FPM: production config, permissions and queues
Deploy Laravel on Nginx and PHP-FPM: a complete HTTPS config pointing at public, permissions for storage, the optimize and queue steps, and the errors you may see.
Intermediate · 3 min read
Nginx and Gunicorn for Django: production config, static files and HTTPS
Deploy Django with Gunicorn behind Nginx: a complete HTTPS config, a socket with systemd, static and media files, the settings you need behind a proxy, and common errors.
Intermediate · 3 min read
Nginx reverse proxy for FastAPI and Uvicorn, with HTTPS and WebSockets
Run FastAPI behind Nginx: a complete HTTPS config, Uvicorn in a systemd service, proxy headers for the real client IP, a subpath, WebSockets, and common errors.
Intermediate · 3 min read
Nginx for Next.js: a production config with HTTPS, caching and standalone builds
Run Next.js behind Nginx: a complete config with HTTPS and long caching for _next/static, the standalone build, a systemd service, and the errors people run into.
Intermediate · 3 min read
Nginx reverse proxy for Node.js and Express, with HTTPS and WebSockets
Set up Nginx in front of a Node.js or Express app: a complete config with HTTPS and WebSockets, a systemd service, trust proxy for real client IPs, and common errors.
Beginner · 3 min read
Cron job not running? Find the reason in the log, PATH and environment
Fix a cron job that does not run or fails silently: check the log, PATH and environment, % signs, /etc/cron.d rules, permissions and lost output.
Beginner · 4 min read
"Job for nginx.service failed because the control process exited with error code": how to find the cause
Fix "Job for X.service failed because the control process exited with error code": read systemctl status and journalctl, test the config, and fix the usual causes.
Beginner · 5 min read
Linux process "Killed" with no error: find the OOM killer and stop it happening again
A process that just prints "Killed" (exit code 137) was usually stopped by the Linux OOM killer. Find it in the kernel log, see what used the memory, and fix it.
Intermediate · 4 min read
"Too many open files" on Linux: raise the right limit, or find the leak
Fix "Too many open files" (EMFILE): check a process's real limit in /proc, raise LimitNOFILE for systemd services, and tell a low limit from a file descriptor leak.
Intermediate · 3 min read