Skip to content

Cookie settings

Choose which kinds of cookies you allow. You can change your choice at any time via Cookie settings at the bottom of every page. Privacy statement

Necessary

Always on

Needed to log in, keep your cart and remember your choices. These are always on.

Which cookies?
  • weboke_sessie, weboke_klant, weboke_remember: your session and staying logged in
  • weboke_basket: your cart
  • wkBtwIncl: showing prices with or without VAT
  • wok_cookies: your cookie choice, 12 months

Measures which ad leads to a visit or order, so we do not pay for ads that do nothing.

Which cookies?
  • Google Ads via Google Tag Manager (_gcl_au, _gcl_aw), up to 90 days
  • OpenAI Ads, ads in ChatGPT
  • wok_aff: which partner referred you to us (affiliate programme), 60 days

4.6

Country and language

Each country has its own site, with prices and VAT for that country.

View

Module 6 · 6.3

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/data

The 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.target

The three sections:

  • [Unit]: description and order. After=network-online.target together with Wants= makes sure the service only starts once the network is up.
  • [Service]: how the program runs. ExecStart with the full path (systemd does not use a search path like your shell does). User= and Group= decide who it runs as. EnvironmentFile reads settings such as a database password from a separate file, so they are not in the unit.
  • [Install]: what enable does. WantedBy=multi-user.target means: 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/env

The 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 -f

Security 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:

OptionEffect
NoNewPrivileges=truethe process and its children can never gain more privileges (not even via sudo or setuid programs)
ProtectSystem=strictthe 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=trueits 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.target

Type=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.service

What 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.sh

The five fields are minute, hour, day of the month, month and day of the week. The pros and cons side by side:

cronsystemd timer
Setting upone linetwo files
Missed run (server off)skippedstill runs with Persistent=true
Output and errorsby email, if the server can send mail; otherwise lostin the journal
Security options, uservia the script itselfeverything systemd offers (see above)
Seeing when it randifficultsystemctl 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/, with User= a dedicated user without a shell, and secrets in an EnvironmentFile with permissions 600.
  • NoNewPrivileges, ProtectSystem=strict with ReadWritePaths, ProtectHome and PrivateTmp limit the damage of a vulnerability; systemd-analyze security assesses the unit.
  • A timer is a .service plus a .timer with OnCalendar; Persistent=true catches up on missed runs. Enable the timer, not the service.
  • cron still works, but timers give better logs and do not miss runs.

Quiz

1. Your application runs as a service with User=root. Why is a dedicated user better?



2. You have created a .service and a .timer. What do you enable with enable --now?



3. What does Persistent=true do in a timer?



4. You add ProtectSystem=strict and your application then reports "Read-only file system" when saving uploads. What is the right fix?



5. Where do you put the database password for your own service?



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.




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

One moment…

Set up your domains

For each domain, arrange the nameservers, the link to your hosting and who becomes the owner.

Loading your domains…