Module 6 · 6.3
English
Your own services and timers
Module 6: Services and logs · Reading time: 11 minutes
Learning objective: After this lesson you can run your own program as a systemd service under its own user, with a few security options, and schedule tasks with a systemd timer or cron.
Key concepts: unit file, [Unit], [Service], [Install], ExecStart, User=, WorkingDirectory, EnvironmentFile, Restart=, WantedBy, NoNewPrivileges, ProtectSystem, ProtectHome, PrivateTmp, systemd-analyze security, timer, OnCalendar, Persistent, crontab
Why not just "nohup" or "screen"?
When you put your own program on a server (a small web application, a bot, a script that does something every night), you often start it by hand the first time: node server.js &, or in a screen session. That works until you log out, the server restarts or the program crashes. Then it is gone, and nobody notices.
A program that must always run should be a systemd service. Then:
- it starts automatically at boot;
- systemd restarts it if it crashes;
- its output goes to the journal (lesson 6.2);
- it runs under a user you choose, with the permissions you allow.
Your own unit file
Say you have a small Node.js application in /opt/factuurapp that listens on 127.0.0.1:3000. Later, nginx or Caddy will expose it to the outside world (module 7).
Step 1: a dedicated user without a shell (lesson 3.2). The application does not need to be root and does not need to log in:
anna@web1:~$ sudo adduser --system --group --home /opt/factuurapp --no-create-home factuurapp
anna@web1:~$ sudo chown -R factuurapp:factuurapp /opt/factuurapp/dataThe code itself can stay owned by root (the application does not need to change itself); only the directory it writes to belongs to the service user.
Step 2: the unit file. You put your own units in /etc/systemd/system/:
# /etc/systemd/system/factuurapp.service
[Unit]
Description=Factuurapp (Node.js)
After=network-online.target
Wants=network-online.target
[Service]
User=factuurapp
Group=factuurapp
WorkingDirectory=/opt/factuurapp
EnvironmentFile=/etc/factuurapp/env
ExecStart=/usr/bin/node /opt/factuurapp/server.js
Restart=on-failure
RestartSec=5s
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/opt/factuurapp/data
[Install]
WantedBy=multi-user.targetThe three sections:
- [Unit]: description and order.
After=network-online.targettogether withWants=makes sure the service only starts once the network is up. - [Service]: how the program runs.
ExecStartwith the full path (systemd does not use a search path like your shell does).User=andGroup=decide who it runs as.EnvironmentFilereads settings such as a database password from a separate file, so they are not in the unit. - [Install]: what
enabledoes.WantedBy=multi-user.targetmeans: start along with a normal boot.
Step 3: secrets kept separate, with strict permissions.
anna@web1:~$ sudo mkdir /etc/factuurapp
anna@web1:~$ sudo install -m 600 -o root -g root /dev/null /etc/factuurapp/env
anna@web1:~$ sudoedit /etc/factuurapp/envThe content could be, for example, DATABASE_URL=postgres://factuur:...@127.0.0.1/factuur. systemd reads the file as root before switching to the service user, so 600 owned by root is enough.
Step 4: load, start, check.
anna@web1:~$ sudo systemctl daemon-reload
anna@web1:~$ sudo systemctl enable --now factuurapp
anna@web1:~$ systemctl status factuurapp
anna@web1:~$ journalctl -u factuurapp -fSecurity options: little effort, big difference
The four lines under RestartSec in the example limit what the service can do, even if someone finds a vulnerability in your application:
| Option | Effect |
|---|---|
NoNewPrivileges=true | the process and its children can never gain more privileges (not even via sudo or setuid programs) |
ProtectSystem=strict | the entire file system is read-only for the service, except what you allow with ReadWritePaths= |
ProtectHome=true | /home, /root and /run/user are invisible |
PrivateTmp=true | its own empty /tmp that other programs cannot see |
systemd can assess your unit:
anna@web1:~$ systemd-analyze security factuurapp
...
→ Overall exposure level for factuurapp.service: 4.9 OK 🙂The lower the number, the better. A service without any options scores around 9 ("UNSAFE"). You do not have to reach zero: every option you add can break something the application needs. Add them one at a time and test after each step. If something stops working, the reason is usually in the journal ("Read-only file system", "Permission denied").
Scheduling tasks: timers
For something that has to happen at fixed times (a backup, a clean-up script, a report) you need a timer. A systemd timer consists of two units with the same name: a .service that says what should happen, and a .timer that says when.
# /etc/systemd/system/opruimen.service
[Unit]
Description=Oude exportbestanden opruimen
[Service]
Type=oneshot
User=factuurapp
ExecStart=/usr/bin/find /opt/factuurapp/data/export -type f -mtime +30 -delete# /etc/systemd/system/opruimen.timer
[Unit]
Description=Dagelijks oude exportbestanden opruimen
[Timer]
OnCalendar=*-*-* 03:15:00
RandomizedDelaySec=10m
Persistent=true
[Install]
WantedBy=timers.targetType=oneshot means: the program runs and is then finished. OnCalendar is the schedule: every day at 03:15. You can also write daily, weekly or Mon *-*-* 02:00; systemd-analyze calendar "Mon *-*-* 02:00" shows when the next run will be. Persistent=true is important: if the server was off at 03:15, the task still runs straight after boot. RandomizedDelaySec spreads the start time a little.
You enable the timer, not the service:
anna@web1:~$ sudo systemctl daemon-reload
anna@web1:~$ sudo systemctl enable --now opruimen.timer
anna@web1:~$ systemctl list-timers opruimen
NEXT LEFT LAST PASSED UNIT ACTIVATES
Tue 2026-11-10 03:19:42 CET 15h left - - opruimen.timer opruimen.service
anna@web1:~$ sudo systemctl start opruimen.service # test once by hand now
anna@web1:~$ journalctl -u opruimen.serviceWhat about cron?
cron is the older, simpler tool and still works fine. A line in a user's crontab (crontab -e):
15 3 * * * /usr/local/bin/opruimen.shThe five fields are minute, hour, day of the month, month and day of the week. The pros and cons side by side:
| cron | systemd timer | |
|---|---|---|
| Setting up | one line | two files |
| Missed run (server off) | skipped | still runs with Persistent=true |
| Output and errors | by email, if the server can send mail; otherwise lost | in the journal |
| Security options, user | via the script itself | everything systemd offers (see above) |
| Seeing when it ran | difficult | systemctl list-timers and the journal |
For a server you manage seriously, timers are the better choice, especially for backups (lesson 8.2): you want to be sure they ran and what they reported. A cron line that fails silently only gets noticed when you need the backup.
On AlmaLinux and Rocky Linux
Units and timers work exactly the same. One difference: SELinux can block your own service if it reads or writes files in a place SELinux does not expect, or listens on an unusual port. The message is in /var/log/audit/audit.log. Do not switch SELinux off to fix it; find the right label or rule (for example with semanage).
Summary
- A program that must always run should be a systemd service: it starts automatically, restarts after a crash and logs to the journal.
- Your own units go in
/etc/systemd/system/, withUser=a dedicated user without a shell, and secrets in anEnvironmentFilewith permissions 600. NoNewPrivileges,ProtectSystem=strictwithReadWritePaths,ProtectHomeandPrivateTmplimit the damage of a vulnerability;systemd-analyze securityassesses the unit.- A timer is a
.serviceplus a.timerwithOnCalendar;Persistent=truecatches up on missed runs. Enable the timer, not the service. - cron still works, but timers give better logs and do not miss runs.
Quiz
Exercise
Exercise O6.3: Your own service with a timer. Create a small service of your own with a timer on your server, for example a script that writes the free disk space and systemctl --failed to a file or the journal every night. Use a dedicated user, Type=oneshot, at least two security options and Persistent=true. Test with systemctl start, check the output in the journal and note the score from systemd-analyze security. Paste your .service and .timer and answer the questions in the checklist.
Exercise O6.3: Your own service with a timer
You do this exercise yourself; no one checks it. Tick off what you've done and mark the exercise as done.
With Persistent=true the task still runs after boot if it was missed. A score below 6 is good for a first service; write access only through ReadWritePaths.
Your ticks and notes only stay in this browser; we don't send them anywhere. If you're signed in, we only save in My courses that you've done the exercise.
Further reading
- DirectAdmin guide 15.1: Cron Jobs: running tasks automatically
- Manual page
systemd.exec(5)(User=, ProtectSystem=, NoNewPrivileges=): https://manpages.debian.org/trixie/systemd/systemd.exec.5.en.html - Manual page
systemd.timer(5)(OnCalendar=, Persistent=): https://manpages.debian.org/trixie/systemd/systemd.timer.5.en.html - Manual page
systemd.time(7)(schedules): https://manpages.debian.org/trixie/systemd/systemd.time.7.en.html
