coldwa.st
All guidesProgrammingWebDataToolsDatabasesHaskellConceptsCabal & buildsToolchainCompilerPerformanceEditor & HLS

Coldwast

systemd service file generator

Fill in the command that starts your app and get a .service unit you can drop into /etc/systemd/system/, with the install commands to match. The generator also reads what you typed and flags the mistakes that make a unit fail on the first start. Everything runs in your browser.

Front of a tower server with a perforated metal grille and small power buttons, lit in blue against a dark background
The front panel of a tower server, lit blue. Once a unit is enabled, systemd starts your app on a machine like this at every boot, with nobody logged in.
Start from

Your unit file

 

What the generator noticed

    Install it

     

    The five lines that decide whether it works

    ExecStart is not a shell. systemd splits the line into arguments itself and runs the program directly. A pipe, a > redirection or && is handed to your program as a literal argument. systemd also expands $VAR on its own, so a literal dollar sign has to be written $$. When the generator sees shell syntax it wraps the command in /bin/sh -c and does that escaping for you.

    Use an absolute path. A path such as ./server is rejected outright. A bare name such as node is accepted, but systemd searches only its own fixed directories, not the PATH of your login shell, so a Node installed with nvm or a tool in ~/.local/bin is not found.

    Type=simple or exec for almost everything. A web server, a worker or a bot that stays in the foreground is simple. exec differs in one useful way: the start is only reported as successful once the binary has actually been executed, so a wrong path fails systemctl start instead of failing silently a moment later. forking is for old daemons that put themselves in the background, and notify only works if the program calls sd_notify.

    Restart=on-failure with a limit. on-failure restarts after a crash or a non-zero exit, but not after a clean stop; always restarts even then. By default systemd allows 5 starts in 10 seconds, and with a 5-second delay a crashing app never reaches that limit, so it loops forever. StartLimitIntervalSec=300 with StartLimitBurst=5 makes it give up and leave a clear failed state.

    Wants= and After= together. After=network-online.target on its own only sets an order; it does not pull the target in. Without Wants=network-online.target your service can still start before the network is up, which is the classic "works after a manual restart, fails at boot" bug.

    The sandbox, and what was left out on purpose

    With ProtectSystem=strict the whole filesystem is read-only for the service, except the paths you list in ReadWritePaths=. PrivateTmp gives it its own /tmp, NoNewPrivileges stops it from gaining rights through setuid binaries, and the kernel and cgroup settings stop it from changing system-wide state. Run systemd-analyze security yourapp.service before and after: it prints an exposure score and lists each setting still open.

    Two common recommendations are deliberately absent. MemoryDenyWriteExecute=true breaks just-in-time compilers, which means Node.js, the JVM and PyPy. PrivateDevices=true is safe for a web app but breaks anything that talks to a GPU or a serial port. Add them yourself if you know your program does not need them.

    A pair of eyeglasses on a desk in front of a blurred monitor showing coloured source code in an editor
    Glasses resting in front of a screen full of code. When a unit fails, the answer is on screen too: systemctl status gives the exit code, journalctl gives your program's own output.

    When it will not start: the exit codes

    systemctl status yourapp shows a status like status=203/EXEC. The number tells you which step failed before your program even ran:

    StatusMeaningUsual fix
    203/EXECThe program could not be executedWrong path, missing execute bit, or a script without a shebang line
    200/CHDIRWorkingDirectory is missing or not accessibleCreate it, or check that ProtectHome is not hiding it
    217/USERThe User= account does not existRun the useradd line above
    226/NAMESPACEThe sandbox could not be set upA path in ReadWritePaths does not exist; create it or prefix it with -

    Any other failure is your own program exiting, and its output is in journalctl -u yourapp -b.

    Changing it later

    After editing the file in /etc/systemd/system/, run sudo systemctl daemon-reload before restarting, or systemd keeps using the old version. For a unit installed by a package, do not edit it in place: sudo systemctl edit yourapp creates an override file that survives upgrades and reloads systemd for you.

    Related: what a VPS is, putting a reverse proxy in front of the app, SSH basics, and for scheduled jobs the cron expression explainer.

    Directives follow the systemd.service(5) and systemd.exec(5) manual pages. Type=exec needs systemd 240 or later; every current Debian, Ubuntu, Fedora and RHEL release qualifies.