Files
unitdore/PLAN.md
T
warkanum f0827fcdd7 feat: podman-compose stack units and root/user execution scopes
- syncup: podman first, docker only as fallback (podman wins on name clash;
  existing docker units found in podman switch to podman)
- syncup: group podman-compose containers into one `kind: stack` unit per
  project (project, working_dir, compose_files, compose_bin from labels);
  disable existing member units ("part of stack <project>")
- generator: compose mode (podman-compose up/stop, WorkingDirectory) with
  pod start/stop fallback when compose binary/dir/files are missing
- reconcile: stacks disabled with "stack not found" when neither compose
  files nor pod exist
- root: scan root + rootless users listed in `users:` (podman only)
- non-root: config ~/.config/unitdore/units.yaml, own user units only
- sysuser package: runuser with XDG_RUNTIME_DIR/DBUS env, home/linger lookup
- install: warn when linger is not enabled for user units
- testdata/stack: podman-compose test stack and captured podman ps fixture
- docs: README, generated-units, man page, AGENTS, PLAN
2026-10-12 19:05:51 +02:00

5.6 KiB

Unitdore — Project Plan

A door you open and close for container units. Unitdore bridges your container runtime and systemd.


What It Is

A Go CLI tool that bridges your container runtime (Podman, Docker, or custom) and systemd. It discovers running containers, stores them in a config file, and generates + manages systemd .service units for each one.

Think of it as: config → systemd units → running services.


Goals

  • Unified config for containers across runtimes and users
  • Auto-generate systemd service files (no manual unit writing)
  • Reconcile: disable units whose containers no longer exist
  • Simple CLI — no daemon, no background process, no magic

Non-Goals

  • Not a container runtime (doesn't replace Podman/Docker)
  • Not Kubernetes / Quadlet
  • Compose: only wraps existing podman-compose stacks (one unit per project); docker compose planned
  • Not a daemon — runs on demand

Config File

Location: /etc/unitdore/units.yaml Owned by: root Modified by: unitdore edit (opens in $EDITOR) or automatically by syncup

units:
  - name: nginx
    runtime: podman          # podman | docker | exec
    user: hein               # empty = root/system unit; set = user unit for this user
    command: ""              # optional: override the ExecStart command entirely
    order: 1                 # startup order (lower = earlier)
    delay: 0s                # delay after previous order group finishes
    enabled: true            # false = unit exists but is not installed/started
    disabled_reason: ""      # auto-set by reconcile: "container not found", etc.

Commands

unitdore syncup

Discovers all currently running containers across configured runtimes and adds any new ones to the config. Does not remove existing entries.

  • Runs podman ps --format json and/or docker ps --format json
  • Compares against existing config entries by name
  • Appends new units with enabled: true
  • Writes updated config back to disk
  • Also reconciles: checks if containers for existing units still exist. If not → sets enabled: false + disabled_reason

unitdore edit

Opens /etc/unitdore/units.yaml in $EDITOR (fallback: vi).

unitdore install

Generates and installs systemd .service files for all enabled units.

  • For units with user: "" → writes to /etc/systemd/system/unitdore-<name>.service then runs systemctl daemon-reload
  • For units with user: hein → writes to /home/hein/.config/systemd/user/unitdore-<name>.service then runs systemctl --user daemon-reload as that user
  • Does not enable or start — just installs the unit files
  • Removes unit files for disabled units and reloads

unitdore active

Enables and starts all installed, enabled units.

  • Runs systemctl enable --now unitdore-<name>.service for system units
  • Runs systemctl --user enable --now unitdore-<name>.service for user units
  • Reports success/failure per unit

unitdore status

Prints a summary table of all managed units.

NAME       RUNTIME   USER    ENABLED   SYSTEMD STATUS   REASON
nginx      podman    —       yes       active (running)
myapp      docker    hein    yes       active (running)
oldthing   podman    hein    no        inactive         container not found

Generated Service File (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

For user units, User= and Group= are set and it targets default.target.


Project Structure

unitdore/
├── main.go
├── go.mod
├── cmd/
│   ├── root.go          # cobra root, version, config path flag
│   ├── syncup.go        # discover + reconcile
│   ├── edit.go          # open $EDITOR
│   ├── install.go       # generate + write unit files
│   ├── active.go        # enable + start units
│   └── status.go        # status table
├── config/
│   └── config.go        # load, save, Unit struct, YAML marshal/unmarshal
├── runtime/
│   ├── runtime.go       # interface: ListContainers() []Container
│   ├── podman.go        # podman ps --format json
│   └── docker.go        # docker ps --format json
└── systemd/
    ├── generator.go     # build .service file content from a Unit
    └── manager.go       # install, reload, enable, start, status via systemctl

Dependencies

  • github.com/spf13/cobra — CLI framework
  • gopkg.in/yaml.v3 — config file
  • Standard library for everything else (exec, os, text/template)

Phased Build

Phase Deliverable
1 Scaffold: project, go.mod, cobra root, config load/save
2 syncup — discover containers, update config
3 install — generate + write service files
4 active + status
5 Reconcile (auto-disable missing units) in syncup
6 User unit support (rootless containers)
7 Polish: edit, error handling, --dry-run flag
8 podman-compose stacks (compose + pod fallback), root/user scopes
9 docker compose stacks (com.docker.compose.* labels)

Open Questions

  • Should syncup also accept a --runtime flag to limit discovery to one runtime?
  • Should install imply a syncup --reconcile first, or be kept separate?
  • Packaging: just a binary, or also a Makefile / install script?