Skip to content
AZ Tools

systemd Service File Generator

Build a .service unit file for systemd — Type, Restart, User, Environment, sandboxing — plus the install and journalctl commands.

Scope:
example.service
[Unit]
Description=Example API server
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/usr/local/bin/example-api --port 8080
WorkingDirectory=/srv/example
User=example
Environment=NODE_ENV=production
Environment=PORT=8080
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal

# Sandboxing — remove any line that stops the service working.
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectControlGroups=true
RestrictSUIDSGID=true
ReadWritePaths=/srv/example

[Install]
WantedBy=multi-user.target
Install commands
# /etc/systemd/system/example.service
sudo systemctl daemon-reload
sudo systemctl enable --now example.service
sudo systemctl status example.service
sudo journalctl -u example.service -f

How to use

  1. Choose the scope: a system unit in /etc/systemd/system that starts at boot, or a --user unit in ~/.config/systemd/user that starts when you log in.
  2. Enter the absolute path to your program in ExecStart, together with any arguments. systemd does not consult $PATH, so /usr/bin/node rather than node.
  3. Pick Type= and Restart=. Type=simple suits a program that stays in the foreground; Type=oneshot suits a script that does one job and exits.
  4. Add environment variables one KEY=value pair per line, or point EnvironmentFile= at a file if they contain secrets you do not want in a world-readable unit.
  5. Copy the unit into the path shown, then run the daemon-reload, enable and journalctl commands from the second box.

About this tool

Show

Writes a systemd unit file from a form, so you do not have to remember which directive lives in which section. Fill in the command, the account it runs as and the restart policy, and the tool emits a complete [Unit] / [Service] / [Install] file along with the commands to install, enable and follow it. It also flags four mistakes that systemd will not tell you about, because it silently does something other than what you meant: an ExecStart that is not an absolute path (systemd does not search $PATH, so the unit fails to start with a status 203/EXEC that says nothing useful), a command containing a pipe, redirect or && (systemd does not run a shell, so those characters are handed to your program as literal arguments), a User= line in a --user unit (which the user manager ignores, since it already runs as you), and Restart=always on a Type=oneshot service (a process that is supposed to exit, restarted forever). The sandboxing block is optional and deliberately conservative: NoNewPrivileges, PrivateTmp, ProtectSystem=strict and friends will break a service that writes outside its working directory, which is why the generated file says so in a comment rather than pretending the settings are free. Nothing is uploaded — the file is assembled in your browser.

Frequently asked questions

Why does my service fail with status=203/EXEC?

Almost always because ExecStart is not an absolute path. systemd executes the binary directly rather than through a shell, so it never searches $PATH; a bare node server.js means "run a file literally named node in the root of the filesystem". Use the output of which node. The same code appears when the file exists but is not executable, or when its interpreter line points at a missing interpreter, so check chmod +x and the shebang if the path is already absolute.

Can I use pipes, redirects or && in ExecStart?

No, and systemd will not complain — it just passes them to your program as ordinary arguments, so myapp > /var/log/app.log starts myapp with three arguments and no redirect. If you genuinely need shell syntax, invoke a shell explicitly with ExecStart=/bin/sh -c 'myapp | tee /var/log/app.log'. In most cases you do not need it: stdout already goes to the journal, so read it with journalctl -u yourapp instead of redirecting to a file.

What is the difference between After= and Wants=?

After= is ordering only: it says "if both units start in the same transaction, start mine second". It does not pull the other unit in, so After=network-online.target alone on a machine where nothing else wants that target means the target is never activated and you wait for nothing. Wants= is the dependency that actually pulls it in. That is why this generator emits Wants=network-online.target alongside the After= line — it is the pair the systemd documentation recommends, and the most common cause of a service starting before the network is up.

Should I use a system unit or a --user unit?

Use a system unit for anything that must be running before or without a login — a web server, a database, a scheduled job. Use a --user unit for something tied to your session, such as a syncing agent or a personal bot; it runs as you, needs no sudo, and lives in ~/.config/systemd/user. The catch is that a user manager normally stops when your last session ends, so a user service that should survive logout needs loginctl enable-linger, which the generated commands include. User units also cannot set User= or Group=, and system targets such as network-online.target do not exist in the user manager.

Is the hardening block safe to keep?

It is safe to try, but not safe to assume. ProtectSystem=strict makes the whole filesystem read-only apart from /dev, /proc and /sys, so a service that writes a PID file, a SQLite database or a log outside its working directory will fail once you enable it. The generator adds ReadWritePaths for the working directory precisely so the common case still works; anything else the service writes to has to be listed there as well. If the service breaks the moment you add the block, remove lines one at a time rather than dropping all of them — systemd-analyze security yourapp.service will score the result and tell you what each directive buys.

Why does the unit not change anything after I edit it?

systemd caches unit files, so an edit on disk is invisible until you run systemctl daemon-reload (or systemctl --user daemon-reload for a user unit). After the reload you still need systemctl restart yourapp for a running service to pick up the new configuration — daemon-reload only reloads systemd's own view of the file. If a change to Environment= appears to be ignored even then, check whether an EnvironmentFile= is setting the same variable, since the file is read after the inline directives and wins.

Related tools