Build-time systemd schedule
The System and Service Manager systemd provides the building blocks for Linux systems, and it’s the default in NixOS — though, there are a few alternatives such as finix and sixos that currently explore this design space.
As someone currently self-hosting a few services on my NixOS servers, it’s almost a daily command by now.
Current status of my servers, as generated by nix-topology (with a few custom patches) ❄️
Runtime Schedule
Recently, I’ve been doing some upgrades and maintenance in one of my servers running Immich, and I thought I would check the current job schedule to ensure they are configured correctly in my timezone.
[admin@metropolis:~]$ systemctl list-timers --all
NEXT LEFT LAST PASSED UNIT ACTIVATES
Sat 2026-08-15 10:44:47 CEST 15min Sat 2026-08-15 10:14:47 CEST 14min ago disk-monitor.timer disk-monitor.service
Sat 2026-08-15 11:00:00 CEST 30min Sat 2026-08-15 10:00:08 CEST 29min ago logrotate.timer logrotate.service
Sat 2026-08-15 15:21:37 CEST 4h 52min Fri 2026-08-14 15:21:40 CEST 19h ago acme-renew-immich.gvolpe.com.timer acme-order-renew-immich.gvolpe.com.service
Sat 2026-08-15 22:52:43 CEST 12h Fri 2026-08-14 22:52:43 CEST 11h ago systemd-tmpfiles-clean.timer systemd-tmpfiles-clean.service
Sun 2026-08-16 00:00:00 CEST 13h Sat 2026-08-15 00:00:08 CEST 10h ago custom-immich-stop-trigger.timer custom-immich-stop-trigger.service
Sun 2026-08-16 00:00:01 CEST 13h Sat 2026-08-15 00:00:08 CEST 10h ago postgresql-backup.timer postgresql-backup.service
Sun 2026-08-16 00:00:05 CEST 13h Sat 2026-08-15 00:00:08 CEST 10h ago borgbackup-job-immich-s3-media.timer borgbackup-job-immich-s3-media.service
Sun 2026-08-16 00:00:05 CEST 13h Sat 2026-08-15 00:00:08 CEST 10h ago borgbackup-job-postgres.timer borgbackup-job-postgres.service
Sun 2026-08-16 00:00:30 CEST 13h Sat 2026-08-15 00:00:35 CEST 10h ago s3-replica.timer s3-replica.service
Sun 2026-08-16 03:00:00 CEST 16h Sat 2026-08-15 03:00:08 CEST 7h ago s3ql-fsck.timer s3ql-fsck.service
Sun 2026-08-16 03:30:00 CEST 17h Sat 2026-08-15 03:30:06 CEST 6h ago custom-immich-start-trigger.timer custom-immich-start-trigger.service
Mon 2026-08-17 00:00:00 CEST 1 day 13h Mon 2026-08-10 02:00:08 CEST 5 days ago nix-gc.timer nix-gc.service
Mon 2026-08-17 01:39:34 CEST 1 day 15h Mon 2026-08-10 02:52:22 CEST 5 days ago fstrim.timer fstrim.service
13 timers listed.
As the number of jobs grows, it’s easy to lose track.
However, looking at this information has got me wondering whether it was possible to get this information directly from my NixOS configuration instead of having to deploy before checking — i.e. build-time checks instead of runtime checks.
Build-Time Schedule
A nice property of NixOS is that it produces a derivation at build time, which is a build recipe of the entire system that can be easily inspected; and the systemd configuration that’s part of it is no exception.
nix-repl> nixosConfigurations.metropolis.config.systemd.timers
{
"acme-renew-immich.gvolpe.com" = { ... };
borgbackup-job-immich-s3-media = { ... };
borgbackup-job-postgres = { ... };
custom-immich-start-trigger = { ... };
custom-immich-stop-trigger = { ... };
disk-monitor = { ... };
fstrim = { ... };
logrotate = { ... };
nix-gc = { ... };
postgresql-backup = { ... };
s3-replica = { ... };
s3ql-fsck = { ... };
}
nix-repl> nixosConfigurations.metropolis.config.systemd.timers.s3-replica.timerConfig
{
OnCalendar = "*-*-* 0:00:30";
Persistent = false;
}
So the information is available and we can process it however we see fit. In my case, I wanted to create a Mermaid flow diagram with each server’s schedule, and here’s the result (see it on Mermaid Live).
This was hacked-up together with a Python script that fetches the core information with the following command:
nix eval --json .#nixosConfigurations.metropolis.config.systemd.timers \
--apply 'builtins.mapAttrs (name: timer: timer.timerConfig)' | jq
Which produces the following JSON data:
{
"acme-renew-immich.gvolpe.com": {
"AccuracySec": "86400s",
"FixedRandomDelay": true,
"OnCalendar": "daily",
"Persistent": "yes",
"RandomizedDelaySec": "24h",
"Unit": "acme-order-renew-immich.gvolpe.com.service"
},
"borgbackup-job-immich-s3-media": {
"OnCalendar": "*-*-* 0:00:05",
"Persistent": false
},
"borgbackup-job-postgres": {
"OnCalendar": "*-*-* 0:00:05",
"Persistent": false
},
"custom-immich-start-trigger": {
"OnCalendar": "*-*-* 03:30:00",
"Unit": "custom-immich-start-trigger.service"
},
"custom-immich-stop-trigger": {
"OnCalendar": "*-*-* 00:00:00",
"Unit": "custom-immich-stop-trigger.service"
},
"disk-monitor": {
"OnBootSec": "5m",
"OnUnitActiveSec": "30m"
},
"fstrim": {
"OnCalendar": [
"",
"weekly"
]
},
"logrotate": {
"OnCalendar": [
"hourly"
]
},
"nix-gc": {
"OnCalendar": [
"weekly"
],
"Persistent": true,
"RandomizedDelaySec": "0"
},
"postgresql-backup": {
"OnCalendar": "*-*-* 0:00:01",
"Unit": "postgresql-backup.service"
},
"s3-replica": {
"OnCalendar": "*-*-* 0:00:30",
"Persistent": false
},
"s3ql-fsck": {
"OnCalendar": "*-*-* 03:00:00",
"Persistent": false,
"Unit": "s3ql-fsck.service"
}
}
Then it’s all about processing this data to generate the Mermaid diagram in the desired format.
Timezone
The script follows this order of evaluation, and it defaults to UTC if both values are absent.
nix-repl> nixosConfigurations.metropolis.config.systemd.globalEnvironment.TZ
"Europe/Warsaw"
nix-repl> nixosConfigurations.metropolis.config.time.timeZone
"Europe/Warsaw"
Multiple timezones can be set individually on each service unit, but this is not a feature I need right now.
Runtime Dependencies
On a running server, we can generate a diagram of systemd dependencies with the following command:
systemd-analyze dot --require --from-pattern="*.service" --to-pattern="*.service" \
| dot -Tsvg > services.svg
Which renders something like this for this NixOS server:
That can quickly get pretty unreadable, so in general it’s more useful to analyze specific units and not all of them, e.g.
[admin@metropolis:~]$ systemctl show immich-server.service -p Requires -p Wants
Requires=postgresql.target s3ql-mount.service -.mount system-immich.slice
Wants=-.mount
These tools are useful for our daily server maintenance tasks, but can we verify dependencies without actually deploying first?
Build-Time Dependencies
It is no surprise that this information is available in our NixOS configuration blueprint as well.
nix-repl> nixosConfigurations.metropolis.config.systemd.services.s3 <TAB>
nixosConfigurations.metropolis.config.systemd.services.s3-aliases
nixosConfigurations.metropolis.config.systemd.services.s3-replica
nixosConfigurations.metropolis.config.systemd.services.s3ql-auth
nixosConfigurations.metropolis.config.systemd.services.s3ql-fs
nixosConfigurations.metropolis.config.systemd.services.s3ql-fsck
nixosConfigurations.metropolis.config.systemd.services.s3ql-mount
The following one-liner command shows how to get all systemd dependencies directly from our NixOS configuration:
nix eval --json .#nixosConfigurations.metropolis.config.systemd.services \
--apply 'builtins.mapAttrs (name: service: { inherit (service) requires wants wantedBy requiredBy; })' \
| jq 'with_entries(.value |= with_entries(select(.value != []))) | with_entries(select(.value != {}))'
To keep it brief, here’s part of the JSON data that’s produced by it:
{
"immich-machine-learning": {
"requires": [
"postgresql.target",
"s3ql-mount.service"
],
"wantedBy": [
"multi-user.target"
]
},
"immich-server": {
"requires": [
"postgresql.target",
"s3ql-mount.service"
],
"wantedBy": [
"multi-user.target"
]
},
"nginx": {
"wantedBy": [
"multi-user.target"
],
"wants": [
"acme-immich.gvolpe.com.service"
]
}
}
I didn’t create any diagrams out of this, but we have the necessary data if that’s something we want to do.
Final thoughts
I thought this was worth sharing, as NixOS doesn’t cease to amaze me 🤩. Safe to say I’m always learning new things, and more importantly, having tons of fun running these servers! Can’t recommend it enough.
What do you think? Let me know in the comments below 👇
Best, Gabriel.
Discussion
Comments