unitdore
A door you open and close for container units.
Unitdore bridges your container runtime (Podman, Docker) and your init system (systemd, OpenRC, runit). It discovers running containers and podman-compose stacks, stores them in a config file, and generates + manages a service for each one.
Install
Arch Linux (AUR)
yay -S unitdore
Debian / Ubuntu
Download the .deb from the releases page and install:
sudo dpkg -i unitdore_*.deb
CentOS / AlmaLinux / Rocky Linux
Download the .rpm from the releases page and install:
sudo rpm -i unitdore-*.rpm
# or with dnf:
sudo dnf install unitdore-*.rpm
Gentoo (OpenRC / systemd)
Ebuild in pkg/gentoo/app-containers/unitdore/. Copy it into a local overlay, then:
ebuild unitdore-<ver>.ebuild manifest # once per version
sudo emerge app-containers/unitdore
Void Linux (runit)
Template in pkg/void/srcpkgs/unitdore/. Copy it into a void-packages checkout, then:
./xbps-src pkg unitdore
sudo xbps-install --repository=hostdir/binpkgs unitdore
Set init: runit / init: openrc in the config if autodetect picks the wrong one.
From source
make build
sudo make install
Or manually:
go build -o unitdore .
sudo cp unitdore /usr/local/bin/
Usage
Discover running containers
sudo unitdore syncup
Queries Podman and Docker for running containers, adds new ones to the config, and reconciles any that have disappeared (marking them disabled).
- Podman first; Docker only for names Podman doesn't have
- podman-compose containers → one
kind: stackunit per project; existing member units disabled (part of stack <project>) - Root: root's containers + rootless Podman containers of each user in
users: - Normal user: own Podman containers, config
~/.config/unitdore/units.yaml, user units only
Edit the config
sudo unitdore edit
Opens /etc/unitdore/units.yaml in $EDITOR (falls back to nvim, nano, vi; creates the config dir if missing).
Install service files
sudo unitdore install <name>
Generates the service file for one enabled unit (error if unknown or disabled). Does not enable or start it.
sudo unitdore install <name> --dry-run # preview, write nothing
sudo unitdore install --all # all enabled units; installed + disabled ones are stopped and removed
sudo unitdore installall # same as install --all
Start / stop / enable / uninstall
sudo unitdore start <name> # start only
sudo unitdore enable <name> # enable at boot + start
sudo unitdore stop <name>
sudo unitdore uninstall <name> # stop, disable, remove service file
Each takes --all instead of a name to act on every unit (shortcuts: startall, enableall, stopall, uninstallall, installall).
start/enable --all: installed, enabled units inorder(lowest first); sameorder→ parallelstop/uninstall --all: all installed units, reverseorder, one at a time
Order and delay at boot
order/delay are written into the generated service files, so they also apply at boot.
| Backend | order |
delay |
|---|---|---|
| systemd | After= units of the previous group |
ExecStartPre=sleep N, TimeoutStartSec=N+90 |
| OpenRC | depend() { after … } |
start_pre() { sleep N } |
| runit | sv -w 30 check <prev> in run |
sleep N in run |
- Previous group = nearest lower
orderamong enabled units of the same user delayis the unit's own, ignored in the first group- Changing
order/enabledviaupdateregenerates all installed, enabled units - Delay also applies to manual
startand to restarts delaymust be a valid non-negative duration (5s,1m); an invalid value fails config load
Check status
sudo unitdore status
Prints a summary table:
NAME RUNTIME USER ENABLED INSTALLED STATE REASON
nginx podman root yes yes active
myapp docker hein yes yes active
oldthing podman hein no no — container not found
Config file
Location: /etc/unitdore/units.yaml
init: "" # optional: systemd | openrc | runit (empty = autodetect)
service_dir: "" # optional: runit supervised dir (empty = /var/service or /etc/service)
prefix: "" # optional: prepended to all service names, e.g. "prod-"
suffix: "" # optional: appended to all service names, e.g. "-svc"
users: [hein] # root only: rootless users scanned by syncup
units:
- name: nginx
runtime: podman # podman | docker
user: "" # empty = root/system unit
command: "" # optional: override ExecStart entirely
order: 1 # startup order (lower = earlier)
delay: 0s # wait before start (own delay, not first group)
enabled: true
disabled_reason: "" # auto-set by syncup reconciliation
- name: myapp
runtime: docker
user: hein # rootless: generates user unit for this user
order: 2
delay: 5s
enabled: true
- name: shop # = compose project name
kind: stack # container (default) | stack
runtime: podman
user: hein
project: shop
working_dir: /srv/shop # from compose labels (refreshed by syncup)
compose_files: [compose.yaml]
compose_bin: /usr/bin/podman-compose
order: 3
enabled: true
- name: backup # shell script unit
runtime: script
script: /opt/scripts/backup.sh # required
stop_script: /opt/scripts/stop.sh # optional → ExecStop
shell: /bin/bash # optional, default /bin/bash
service_type: simple # simple (default) | oneshot
working_dir: /opt/scripts # optional
order: 4
enabled: true
- name: agent # executable unit
runtime: exec
executable: /opt/agent/bin/agent # required
args: [--config, /etc/agent.toml] # optional
stop_executable: /opt/agent/bin/stop # optional → ExecStop
order: 5
enabled: true
Init systems
Set init: in the config, or leave empty to autodetect (/run/systemd/system → systemd, /run/openrc → openrc, /etc/runit → runit, else systemd).
| systemd | openrc | runit | |
|---|---|---|---|
| Service file | /etc/systemd/system/unitdore-<n>.service |
/etc/init.d/unitdore-<n> |
/etc/sv/unitdore-<n>/{run,finish} + symlink in service_dir |
| Start / stop | systemctl |
rc-service |
sv |
| Enable at boot | systemctl enable |
rc-update add … default |
remove down file |
| Installed state | disabled until enabled | not in runlevel | symlinked with down file (no autostart) |
Rootless user: |
systemctl --user unit (needs linger) |
system service, command_user |
system service, chpst -u |
| Non-root use | yes (own user units) | root only | root only |
restart* |
Restart=on-failure, StartLimitBurst |
supervise-daemon respawn_* (always respawns) |
finish: on-failure, RestartSec; no retry limit |
docker dependency |
Requires=docker.service |
need docker |
none |
oneshot |
RemainAfterExit |
start()/stop() functions |
run once, then tail -f /dev/null |
- Rootless units on openrc/runit set
HOMEandXDG_RUNTIME_DIR=/run/user/<uid>and create that dir if missing. command/argsuse systemd quoting; they are re-quoted for sh on openrc/runit.- No log redirection is generated on openrc; use the runtime's own logs.
docs/generated-units.mdshows systemd output only.
Script / executable units
Added manually (unitdore edit); syncup never creates or disables them.
| Key | Runtime | Meaning |
|---|---|---|
script |
script | Required. ExecStart=<shell> <script> |
stop_script |
script | Optional. ExecStop=-<shell> <stop_script> |
shell |
script | Interpreter, default /bin/bash |
executable |
exec | Required. ExecStart=<executable> <args> |
args |
exec | Optional list, quoted as needed |
stop_executable |
exec | Optional. ExecStop=-<stop_executable> |
service_type |
both | simple (default) or oneshot (RemainAfterExit=yes) |
working_dir |
both | Optional WorkingDirectory |
command |
both | If set, overrides ExecStart |
user |
both | Set for a user unit |
Stacks (podman-compose)
| Condition | Unit |
|---|---|
compose_bin, working_dir, compose_files exist |
podman-compose -p <proj> -f <files> up / stop, Type=simple |
| any missing | podman pod start/stop pod_<proj>, Type=oneshot |
| compose missing and no pod | syncup disables: stack not found |
Users
| Run as | Config | Discovers | Units |
|---|---|---|---|
| root | /etc/unitdore/units.yaml |
root + users: (Podman only for users) |
system + user units |
| user | ~/.config/unitdore/units.yaml |
own Podman | own user units |
User units need linger to start at boot: sudo loginctl enable-linger <user> (install warns).
Prefix and suffix
Set prefix and/or suffix to namespace your service files:
prefix: prod-
suffix: ""
Generates: unitdore-prod-nginx.service, unitdore-prod-myapp.service, etc.
Prefix/suffix apply to the service file name only — container names are unchanged.
Unnamed containers
If a container has no name, syncup uses its short container ID (first 12 chars) as the unit name.
You can rename it later with unitdore edit.
Generated unit files
See docs/generated-units.md for full examples covering:
- System units (root)
- User units (rootless)
- Docker vs Podman
- Custom commands
- Compose stacks (compose + pod fallback)
- Prefix/suffix naming
- Startup ordering
- Unnamed containers (short ID fallback)
Quick example:
# /etc/systemd/system/unitdore-nginx.service
# Generated by unitdore — do not edit manually
[Unit]
Description=Unitdore: nginx (podman)
After=network.target
[Service]
Type=simple
ExecStart=/usr/bin/podman start -a nginx
ExecStop=/usr/bin/podman stop nginx
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
Flags
--config string path to units config file (default "/etc/unitdore/units.yaml"; non-root: ~/.config/unitdore/units.yaml)
Requirements
- Go 1.21+
- systemd
- Podman and/or Docker (only what you have installed is used)
- podman-compose for compose-mode stacks (pod fallback without it)
- Root for system units; the relevant user for rootless units