feat: OpenRC and runit init backends behind an Init interface

Extract init-neutral unit resolution into service/, add initsys.Init with systemd, openrc and runit backends, selected by 'init:' config or autodetect. OpenRC and runit are root only; rootless units run as system services as that user. Add docs and generated examples for all three.
This commit is contained in:
2026-10-12 20:19:58 +02:00
parent 95143dd998
commit df9b226c7e
32 changed files with 2329 additions and 322 deletions
+323 -1
View File
@@ -1,4 +1,4 @@
# Generated Systemd Unit Files
# Generated Unit Files (systemd, OpenRC, runit)
Unitdore generates `.service` files from your `units.yaml` config.
This document shows every combination with annotated examples.
@@ -330,6 +330,326 @@ ExecStop=-/usr/bin/podman pod stop pod_shop
---
## Config Behind the Init System Examples
```yaml
init: systemd # systemd | openrc | runit
units:
- {name: nginx, runtime: podman, order: 1, enabled: true}
- {name: redis, runtime: docker, order: 2, enabled: true, restart: true, restart_sec: 5, restart_retries: 3}
- {name: myapp, runtime: podman, user: alice, order: 3, enabled: true}
- {name: backup, runtime: script, script: /opt/scripts/backup.sh, stop_script: /opt/scripts/stop.sh, shell: /bin/sh, working_dir: /opt/scripts, order: 4, enabled: true}
- {name: agent, runtime: exec, executable: /opt/agent/bin/agent, args: [--config, /etc/agent.toml], service_type: oneshot, order: 5, enabled: true}
- {name: shop, runtime: podman, kind: stack, project: shop, order: 6, enabled: true}
```
---
## Script and Executable Units (systemd)
`script` runs `<shell> <script>` (default shell `/bin/bash`); `exec` runs the binary directly.
```ini
# /etc/systemd/system/unitdore-backup.service
# Generated by unitdore — do not edit manually
[Unit]
Description=Unitdore: backup (sh script)
After=network.target
[Service]
Type=simple
WorkingDirectory=/opt/scripts
ExecStart=/bin/sh /opt/scripts/backup.sh
ExecStop=-/bin/sh /opt/scripts/stop.sh
[Install]
WantedBy=multi-user.target
```
```ini
# /etc/systemd/system/unitdore-agent.service
# Generated by unitdore — do not edit manually
[Unit]
Description=Unitdore: agent (executable)
After=network.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/opt/agent/bin/agent --config /etc/agent.toml
ExecStop=
[Install]
WantedBy=multi-user.target
```
---
## OpenRC
Root only. Files in `/etc/init.d/`. Rootless `user:` units run as a system service via `command_user`.
### nginx
```sh
#!/sbin/openrc-run
# /etc/init.d/unitdore-nginx
# Generated by unitdore — do not edit manually
description="Unitdore: nginx (podman)"
command_background=yes
pidfile="/run/${RC_SVCNAME}.pid"
command="/usr/bin/podman"
command_args="start -a nginx"
depend() {
need net
}
stop_pre() {
/usr/bin/podman stop nginx || true
}
```
### redis
```sh
#!/sbin/openrc-run
# /etc/init.d/unitdore-redis
# Generated by unitdore — do not edit manually
description="Unitdore: redis (docker)"
supervisor=supervise-daemon
respawn_delay=5
respawn_max=3
command="/usr/bin/docker"
command_args="start -a redis"
depend() {
need net
need docker
}
stop_pre() {
/usr/bin/docker stop redis || true
}
```
### myapp
```sh
#!/sbin/openrc-run
# /etc/init.d/unitdore-myapp
# Generated by unitdore — do not edit manually
description="Unitdore: myapp (podman)"
export HOME=/home/alice
export XDG_RUNTIME_DIR=/run/user/1000
command_background=yes
pidfile="/run/${RC_SVCNAME}.pid"
command="/usr/bin/podman"
command_args="start -a myapp"
command_user="alice"
depend() {
need net
}
start_pre() {
checkpath -d -m 0700 -o alice /run/user/1000
}
stop_pre() {
su -m -s /bin/sh -c '/usr/bin/podman stop myapp' alice || true
}
```
### backup
```sh
#!/sbin/openrc-run
# /etc/init.d/unitdore-backup
# Generated by unitdore — do not edit manually
description="Unitdore: backup (sh script)"
command_background=yes
pidfile="/run/${RC_SVCNAME}.pid"
command="/bin/sh"
command_args="/opt/scripts/backup.sh"
directory="/opt/scripts"
depend() {
need net
}
stop_pre() {
/bin/sh /opt/scripts/stop.sh || true
}
```
### agent
```sh
#!/sbin/openrc-run
# /etc/init.d/unitdore-agent
# Generated by unitdore — do not edit manually
description="Unitdore: agent (executable)"
start() {
ebegin "Starting $RC_SVCNAME"
/opt/agent/bin/agent --config /etc/agent.toml
eend $?
}
depend() {
need net
}
```
### shop
```sh
#!/sbin/openrc-run
# /etc/init.d/unitdore-shop
# Generated by unitdore — do not edit manually
description="Unitdore: shop (podman pod)"
start() {
ebegin "Starting $RC_SVCNAME"
/usr/bin/podman pod start pod_shop
eend $?
}
stop() {
ebegin "Stopping $RC_SVCNAME"
/usr/bin/podman pod stop pod_shop
eend $?
}
depend() {
need net
}
```
---
## runit
Root only. Each unit is `/etc/sv/unitdore-<name>/{run,finish}` symlinked into the service dir. Install adds a `down` file (no autostart until started/enabled). Both files are shown per unit.
### nginx
```sh
# ===== /etc/sv/unitdore-nginx/run =====
#!/bin/sh
# unitdore-nginx — generated by unitdore, do not edit manually
exec 2>&1
exec /usr/bin/podman start -a nginx
# ===== /etc/sv/unitdore-nginx/finish =====
#!/bin/sh
# unitdore-nginx — generated by unitdore, do not edit manually
/usr/bin/podman stop nginx || true
exec sv down .
```
### redis
```sh
# ===== /etc/sv/unitdore-redis/run =====
#!/bin/sh
# unitdore-redis — generated by unitdore, do not edit manually
exec 2>&1
exec /usr/bin/docker start -a redis
# ===== /etc/sv/unitdore-redis/finish =====
#!/bin/sh
# unitdore-redis — generated by unitdore, do not edit manually
/usr/bin/docker stop redis || true
[ "$1" = 0 ] && exec sv down .
sleep 5
```
### myapp
```sh
# ===== /etc/sv/unitdore-myapp/run =====
#!/bin/sh
# unitdore-myapp — generated by unitdore, do not edit manually
exec 2>&1
[ -d /run/user/1000 ] || install -d -m 0700 -o alice /run/user/1000
export HOME=/home/alice
export XDG_RUNTIME_DIR=/run/user/1000
exec chpst -u alice /usr/bin/podman start -a myapp
# ===== /etc/sv/unitdore-myapp/finish =====
#!/bin/sh
# unitdore-myapp — generated by unitdore, do not edit manually
chpst -u alice /usr/bin/podman stop myapp || true
exec sv down .
```
### backup
```sh
# ===== /etc/sv/unitdore-backup/run =====
#!/bin/sh
# unitdore-backup — generated by unitdore, do not edit manually
exec 2>&1
cd /opt/scripts || exit 1
exec /bin/sh /opt/scripts/backup.sh
# ===== /etc/sv/unitdore-backup/finish =====
#!/bin/sh
# unitdore-backup — generated by unitdore, do not edit manually
cd /opt/scripts 2>/dev/null
/bin/sh /opt/scripts/stop.sh || true
exec sv down .
```
### agent
```sh
# ===== /etc/sv/unitdore-agent/run =====
#!/bin/sh
# unitdore-agent — generated by unitdore, do not edit manually
exec 2>&1
/opt/agent/bin/agent --config /etc/agent.toml || exit 1
exec tail -f /dev/null
# ===== /etc/sv/unitdore-agent/finish =====
#!/bin/sh
# unitdore-agent — generated by unitdore, do not edit manually
exec sv down .
```
### shop
```sh
# ===== /etc/sv/unitdore-shop/run =====
#!/bin/sh
# unitdore-shop — generated by unitdore, do not edit manually
exec 2>&1
/usr/bin/podman pod start pod_shop || exit 1
exec tail -f /dev/null
# ===== /etc/sv/unitdore-shop/finish =====
#!/bin/sh
# unitdore-shop — generated by unitdore, do not edit manually
/usr/bin/podman pod stop pod_shop || true
exec sv down .
```
---
## Quick Reference: Service File Locations
| Scenario | Path |
@@ -339,3 +659,5 @@ ExecStop=-/usr/bin/podman pod stop pod_shop
| With prefix `prod-` | `/etc/systemd/system/unitdore-prod-<name>.service` |
| With suffix `-svc` | `/etc/systemd/system/unitdore-<name>-svc.service` |
| With both | `/etc/systemd/system/unitdore-prod-<name>-svc.service` |
| OpenRC | `/etc/init.d/unitdore-<name>` |
| runit | `/etc/sv/unitdore-<name>/` (+ symlink in `/var/service` or `/etc/service`) |
+9 -2
View File
@@ -1,6 +1,6 @@
.TH UNITDORE 1 "2026-04-08" "0.1.0" "User Commands"
.SH NAME
unitdore \- manage container units via systemd
unitdore \- manage container units via systemd, OpenRC or runit
.SH SYNOPSIS
.B unitdore
[\fB\-\-config\fR \fIpath\fR]
@@ -106,6 +106,13 @@ units:
Stack units run \fBpodman\-compose up\fR/\fBstop\fR when compose_bin,
working_dir and compose_files exist; otherwise \fBpodman pod start\fR/\fBstop
pod_\fR\fIproject\fR.
.SH INIT SYSTEMS
The \fBinit:\fR config key selects \fBsystemd\fR, \fBopenrc\fR or \fBrunit\fR.
Empty means autodetect, falling back to systemd. OpenRC and runit are root only;
rootless units run as system services as that user. runit service links go in
\fBservice_dir:\fR (default \fI/var/service\fR or \fI/etc/service\fR).
OpenRC scripts are written to \fI/etc/init.d/unitdore\-*\fR and runit services to
\fI/etc/sv/unitdore\-*\fR.
.SH GENERATED SERVICE FILES
System units are written to \fI/etc/systemd/system/\fR and user units to
\fI~/.config/systemd/user/\fR.
@@ -156,7 +163,7 @@ Generated system unit files.
.I ~/.config/systemd/user/unitdore-*.service
Generated rootless user unit files.
.SH REQUIREMENTS
systemd; Podman and/or Docker (only installed runtimes are used);
systemd, OpenRC or runit; Podman and/or Docker (only installed runtimes are used);
podman\-compose for compose\-mode stacks.
User units need linger to start at boot: \fBloginctl enable\-linger\fR \fIuser\fR.
Root is required for system units; the relevant user for rootless units.