SELFHOST.COMPUTER

systemd Service & Timer Generator

Fill in the form and copy a unit file, an optional timer and the commands to enable it. Use it to keep your own program running after a reboot, or to replace a cron job with a timer that logs to the journal. Everything runs in your browser; nothing is sent anywhere.

Timer (run on a schedule instead of at boot):

Hardening (each one removes something the service can do):

myapp.service


  
  

Enable it


How it works

  • Where files go. Units you write yourself live in /etc/systemd/system/. After saving or changing one, run sudo systemctl daemon-reload.
  • Service with a timer. When you add a timer, the service has no [Install] section and you enable the timer. systemd starts the service of the same name at the scheduled time. Use Type=oneshot for jobs that run and exit.
  • OnCalendar uses systemd's own format (DayOfWeek Year-Month-Day Hour:Minute:Second), not cron's. Test an expression with systemd-analyze calendar "..."; it prints the normalised form and the next run. For the crontab equivalent, use the cron builder.
  • Persistent=true makes a timer catch up after downtime, which cron can't do. RandomizedDelaySec spreads the start so many servers don't hit a backup target at the same second.
  • Restart policy. on-failure restarts after a crash or non-zero exit, not after a clean stop. always also restarts after a clean exit. Repeated fast failures eventually hit systemd's start limit and it stops trying; see systemctl status.
  • ExecStart is not a shell. Use an absolute path, no pipes or redirects. For those, run /bin/sh -c '...'. A literal % is written as %%, which the generator does for you.

Hardening, and its limits

The ticked options are a reasonable starting point for a plain program running as its own user. Check what's left with systemd-analyze security <name>.service and tighten from there. Some options will break things: ProtectSystem=strict needs every directory the program writes to listed (or use the data directory), ProtectHome hides /home, and dropping all capabilities stops it binding ports below 1024. Turn on one at a time if something fails, and read the errors in journalctl -u <name>.

This is aimed at ordinary programs. A unit that just runs docker compose needs the Docker socket, so most of these options don't apply. Containers have their own limits; the Compose generator sets memory ceilings per container.

Related

Create the user and lock down SSH first: the first 15 minutes on a fresh VPS. A backup timer pairs well with restic, and the backup planner sizes the repository. Open the right ports with the firewall generator.