Services

UraniumOS supervises services with runit. The kernel starts the initramfs, the initramfs starts busybox init as process 1, and init reads /etc/inittab:

::sysinit:/etc/rc
::respawn:/rod/profile/bin/runsvdir -P /etc/service
::ctrlaltdel:/rod/init/bin/reboot
::shutdown:/etc/rc.shutdown

init runs /etc/rc once, keeps runsvdir running on /etc/service, runs reboot on Ctrl-Alt-Del and runs /etc/rc.shutdown at shutdown. runsvdir, sv and svlogd come from the runit package in /rod/profile/bin. The busybox applets are in /rod/init/bin, which is not on the PATH of a login shell, see system. Jobs that run at fixed times belong in cron.

A process list of a freshly booted system, shortened:

    1     0 root     init
  158     1 root     /rod/profile/bin/sh /etc/rc
  160     1 root     /rod/profile/bin/runsvdir -P /etc/service
  164   160 root     runsv dhcp
  165   160 root     runsv getty-tty1
  166   160 root     runsv getty-ttyS0
  167   160 root     runsv autoframe
  168   160 root     runsv ntpd
  169   160 root     runsv crond
  170   160 root     runsv trial
  182   164 root     udhcpc -f -i eth0 -s /etc/udhcpc.script
  184   169 root     crond -f -c /etc/crontabs

Process 158 is the /etc/rc shell.

What /etc/rc does

/etc/rc is a shell script (bash from the profile) that runs with PATH=/rod/profile/bin:/rod/init/bin. It keeps going when a command fails. It:

  1. mounts /proc, /sys and /dev if the kernel has not, then /dev/pts, /dev/shm (nosuid,nodev,noexec) and /tmp (nosuid,nodev, mode 1777), and sets the kernel log level to 3;
  2. mounts a tmpfs on /run. If the initramfs mounted the btrfs top level at /run/scram/top, it is moved onto the new /run;
  3. remounts / read-write and runs mount -a for /etc/fstab (/home and /boot);
  4. sets the hostname from /etc/hostname;
  5. loads the console keymap named in /etc/keymap, see keyboard;
  6. applies /etc/sysctl.conf and loads the firewall /etc/nftables.conf, see blastdoor;
  7. creates a zram swap device of half the memory. It asks for zstd compression, but the 0.1.0 image ends up with lzo-rle;
  8. brings up lo with 127.0.0.1;
  9. runs /etc/rc.local if it exists. The shell sources the file as root before any service starts;

Shutdown

/etc/rc.shutdown runs sync and umount -a -r, which remounts what it cannot unmount read-only. reboot, poweroff and halt are busybox applets in /rod/init/bin. Call them as /rod/init/bin/reboot or add that directory to your PATH.

Service directories

A service is a directory in /etc/sv. It is enabled when a link to it exists in /etc/service. runsvdir checks /etc/service every five seconds, starts a runsv supervisor for each new link and stops the supervisor of each removed link. The directory can hold:

run
Required, executable. It has to exec a process that stays in the foreground. When the process exits, runsv starts run again after one second.
log/run
Optional. The standard output of run goes through a pipe to this program. The log scripts that ship create /var/log/NAME and exec svlogd -tt /var/log/NAME.
finish
Optional. Runs when run exits.
check
Optional. sv check runs it, and exit status 0 means the service works.
down
An empty file. The supervisor starts but the service does not.
sandbox
Read by hazmat -s, see below.
supervise/
Made by runsv, mode 0700. It holds control, lock, ok, pid, stat and status.

A run script gets the environment that runsvdir inherited from init. SVDIR is only set by /etc/profile, so sv check dhcp inside a script fails and the script has to name the service by path.

The standard error of run is not part of the log pipe. Without exec 2>&1 it goes to the console. The scripts of dhcp, ntpd, crond and sshd use exec 2>&1, and autoframe and trial do not.

Control with sv

In a login shell SVDIR=/etc/service, so sv takes a service name or a path. Anywhere else pass /etc/service/NAME.

# sv status /etc/service/*
run: /etc/service/autoframe: (pid 174) 4s; run: log: (pid 173) 4s
run: /etc/service/crond: (pid 184) 4s; run: log: (pid 183) 4s
run: /etc/service/dhcp: (pid 182) 4s; run: log: (pid 181) 4s
run: /etc/service/getty-tty1: (pid 179) 4s
run: /etc/service/getty-ttyS0: (pid 180) 4s
run: /etc/service/ntpd: (pid 186) 4s; run: log: (pid 171) 4s
run: /etc/service/trial: (pid 172) 4s
sv up NAME, sv down NAME
Start the service and keep it running, or stop it and keep it stopped. A down state does not survive a reboot.
sv once NAME
Start the service once and do not restart it when it exits.
sv restart NAME
Stop and start. It reports the service as running when it is up again.
sv check NAME
Reports that the service is running and exits with status 0 when it is up.
sv status NAME
One line, see the table.
sv exit NAME, sv term NAME, sv hup NAME, sv kill NAME
Stop the supervisor, or send the signal to the service. See sv(8) of runit.
sv alarm NAME
Sends SIGALRM. On a log directory (sv a /etc/service/dhcp/log) it makes svlogd rotate the log.

Status lines:

run: ntpd: (pid 219) 1s; run: log: (pid 171) 6sRunning, with its log process.
down: ntpd: 1s, normally up; run: log: (pid 171) 5sStopped with sv down.
run: crond: (pid 222) 3s, want downStarted with sv once.
down: web: 5sHas a down file and has not been started.
down: bad1: 1s, normally up, want upThe run script keeps exiting and is being restarted.
fail: nosuch: unable to change to service directory: file does not existNo such name in SVDIR.

Enable and disable

# ln -s /etc/sv/sshd /etc/service/sshd
# rm /etc/service/sshd

A new link starts the service within five seconds and at every later boot. Removing the link stops the supervisor and the service within about seven seconds. A link can also be made from a chroot, so a service can be enabled before the first boot.

Try a service before it starts at boot

# touch /etc/sv/web/down
# ln -s /etc/sv/web /etc/service/web
# sv once /etc/service/web
# sv status /etc/service/web
run: /etc/service/web: (pid 241) 1s, normally down, want down

Remove the down file and run sv up when it works. geiger skips services that have a down file.

Logs

svlogd -tt writes /var/log/NAME/current with UTC timestamps, whatever the time zone is:

# cat /var/log/dhcp/current
2026-11-02_16:53:09.65038 udhcpc: started, v1.37.0
2026-11-02_16:53:09.66001 udhcpc: broadcasting discover
2026-11-02_16:53:09.68126 udhcpc: broadcasting select for 10.0.2.15, server 10.0.2.2
2026-11-02_16:53:09.70119 udhcpc: lease of 10.0.2.15 obtained from 10.0.2.2, lease time 86400

When current reaches the size limit, or on SIGALRM, svlogd renames it to @TAI64N.s and starts a new file. The limits are the runit defaults, 1,000,000 bytes and 10 files.

A log directory added to a service that is already supervised is not used, and no /var/log/NAME/current appears. Create the log before the first start, or remove the link, wait for the supervisor to exit and link it again.

The logs are on the root subvolume, so every frame has them.

The services of the system

The stage links the seven services in the first block. setup --ssh links sshd, and uranium-desktop creates the last two.

getty-tty1getty -l /rod/init/bin/login 38400 tty1. The login prompt of the first virtual console. No log.
getty-ttyS0getty -l /rod/init/bin/login -L 115200 ttyS0 vt100. The serial console. No log.
dhcpWaits for a network device with a device entry in /sys/class/net, brings the first one up and runs udhcpc -f on it. See network. Log: /var/log/dhcp.
ntpdntpd -n -p pool.ntp.org -p time.cloudflare.com. See time. Log: /var/log/ntpd.
crondcrond -f -c /etc/crontabs. See cron. Log: /var/log/crond.
autoframeSleeps 3600 seconds, runs scram auto 48 and exits, and runsv starts it again. The first frame comes an hour after boot. scram auto takes a frame labelled auto and deletes the older automatic frames beyond the newest 48. Log: /var/log/autoframe, which stays empty.
trialRuns scram verify, sleeps 3600 seconds and repeats. scram verify returns at once unless an update is on trial. On trial it waits 300 seconds, runs geiger up to three times 30 seconds apart, confirms the update on success and reboots on failure. See scram. No log.
sshdCreates /var/empty, creates the ed25519 host key if it is missing and runs sshd -D -e. See ssh. Log: /var/log/sshd.
udevd, seatdAdded and linked by uranium-desktop USER for the desktop profile.

Write a service

This service serves /srv/www with the busybox web server. It is called by full path because busybox is not on the PATH of the scripts.

# mkdir -p /etc/sv/web/log /srv/www
# cat > /etc/sv/web/run <<'EOF'
#!/rod/profile/bin/sh
exec 2>&1
exec /rod/init/bin/httpd -f -p 8080 -h /srv/www
EOF
# cat > /etc/sv/web/log/run <<'EOF'
#!/rod/profile/bin/sh
mkdir -p /var/log/web
exec svlogd -tt /var/log/web
EOF
# chmod +x /etc/sv/web/run /etc/sv/web/log/run
# ln -s /etc/sv/web /etc/service/web

Open the port for other machines with blastdoor allow 8080. A run script that is not executable fails once a second, with an error on the console. A script that runs a program that does not exist fails the same way.

Start order

All services start together. To wait for another one, test it in run and exit while it is down. runsv starts the script again a second later:

sv check /etc/service/dhcp > /dev/null || exit 1

The path form is needed because SVDIR is not set in a script.

Sandbox a service

hazmat -s COMMAND in a run script reads the file sandbox in the current directory. runsv starts run in the service directory, and HAZMAT_FILE changes the name. Without the file the command runs without a sandbox. Each line is one of:

ro PATH
Bind the path read-only.
rw PATH
Bind the path writable. It is created if missing.
net, nonet
Share the host network, or not. The default is no network.

Blank lines and lines starting with # are ignored, and any other word stops hazmat with unknown line. hazmat then runs bwrap --unshare-all, with --share-net for net, and binds /nix, /rod/profile, /bin, /usr, /lib, /lib64, /etc/ssl and /etc/resolv.conf read-only, with a fresh /proc, /dev and /tmp. /rod/init is not bound, so use programs from the profile. The service keeps it's user id. See hazmat.