Virteche
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.

Plans and prices

VPS Plans

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

See all plans
  • Bronze

    3 vCPU · 6 GB RAM · 75 GB SSD

    2 TB transfer

    $37.50/mo

    Configure inAshburnDallas
  • Silver

    4 vCPU · 8 GB RAM · 100 GB SSD

    4 TB transfer

    $46.50/mo

    Configure inAshburnDallas
  • Gold

    6 vCPU · 12 GB RAM · 150 GB SSD

    6 TB transfer

    $60.00/mo

    Configure inAshburnDallas
  • Diamond

    8 vCPU · 16 GB RAM · 200 GB SSD

    8 TB transfer

    $73.50/mo

    Configure inAshburnDallas

Network port: 10 Gbps in Ashburn, VA, 50 Gbps in Dallas, TX. Pick the location when you configure.

Keep Reading

All guides