Guideschevron_rightServers

Servers · 7 min read · Updated

How to Create a systemd Service (and Fix 203/EXEC)

To run your own program as a service, write a unit file in /etc/systemd/system/ with an ExecStart= line, a User=, Restart=on-failure and WantedBy=multi-user.target, then run systemctl daemon-reload and systemctl enable --now. If it fails with status=203/EXEC, systemd could not run the program: the file is missing, not executable, has no #! line, or was saved with Windows line endings. 200/CHDIR means the working directory does not exist and 217/USER that the user does not.

1. Write the Unit File

This runs an app from /opt/myapp as its own unprivileged user, starts it after the network is up, and restarts it five seconds after a crash. Save it as /etc/systemd/system/myapp.service:

[Unit]
Description=My app
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=myapp
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/venv/bin/python /opt/myapp/app.py
EnvironmentFile=-/etc/myapp.env
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

The - before the environment file path means “fine if it does not exist”. Create the user first, with no login shell:

sudo useradd --system --home /opt/myapp --shell /usr/sbin/nologin myapp
sudo chown -R myapp: /opt/myapp

2. Start It and Enable It at Boot

sudo systemctl daemon-reload          # after every change to the unit file
sudo systemctl enable --now myapp     # start now and at every boot
systemctl status myapp
journalctl -u myapp -f                # follow its output

Anything the program prints goes to the journal, so journalctl -u myapp is the first place to look when something is wrong.

When It Will Not Start

systemctl status shows a line such as Main process exited, code=exited, status=203/EXEC. The number says which step failed before your program even ran. We reproduced each of these on Ubuntu 24.04 (systemd 255):

systemd start-up failures and their causes
StatusWhat the journal saysCause and fix
203/EXECOnly status=203/EXECThe file in ExecStart= does not exist, or is not executable. Check the path; chmod +x the file.
203/EXECFailed to execute …: Exec format errorA script with no #! line. Add #!/bin/bash (or the right interpreter) as the first line.
203/EXECFailed to execute …: No such file or directory (but the file exists)Windows line endings: the #! line ends in \r, so the interpreter's name is wrong. Convert the file (below).
200/CHDIRChanging to the requested working directory failedWorkingDirectory= does not exist. Create it or fix the path.
217/USERFailed to determine user credentialsUser= names a user that does not exist. Create it with useradd.

Checks for each 203 cause:

ls -l /opt/myapp/start.sh                 # exists? has x permission?
head -1 /opt/myapp/start.sh               # starts with #! ?
head -1 /opt/myapp/start.sh | od -c       # a \r before \n means Windows line endings
sed -i 's/\r$//' /opt/myapp/start.sh      # remove them

Two rarer causes: the script lives on a file system mounted with noexec (often /tmp), and on AlmaLinux or Rocky Linux an SELinux label that does not allow running the file, which restorecon -v on the file fixes after moving it to a normal location such as /usr/local/bin.

Running It on a Schedule Instead

For a job that should run every night rather than all the time, pair a service with a timer instead of using cron: a .timer unit with OnCalendar= starts the service on schedule and logs every run to the journal. Backing up a database to S3 uses exactly that pattern.

Common Questions

  • Type=simple or Type=forking?

    Use simple (the default) when your program stays in the foreground, which is how most modern programs and anything you start with node, python or a binary behave. Use forking only for old-style daemons that start, fork a background process and exit, and give them a PIDFile so systemd can track the child.

  • Does ExecStart need a full path?

    It is safest to give one. Current systemd versions also accept a bare command name and look it up in a fixed list of system directories, but not in your PATH, so a program in a virtualenv, ~/.local/bin or /opt must be written out in full.

  • Where do I put environment variables?

    Short ones in Environment= lines in the unit. Secrets and longer lists in a separate file referenced with EnvironmentFile=, readable only by root (chmod 600), so they do not show up in systemctl show output or the unit file.

  • How do I make it restart if it crashes?

    Set Restart=on-failure and a short RestartSec. systemd then restarts the service whenever it exits with an error or is killed, but not when you stop it yourself.

Run your own services on a KVM VPS with full root access, in Ashburn or Dallas, ready a few minutes after payment.

See VPS plans

Keep Reading

All guides