Architecture decision records (homelab)
ADRs capture decisions that are stable enough to reference later, with context and trade-offs. They supplement the narrative Decisions page.
ADRs capture decisions that are stable enough to reference later, with context and trade-offs. They supplement the narrative Decisions page.
Rejected. The homelab will not split Prod/Test/Dev into separate tailnets. Environment separation stays as it is today: a single tailnet with tag-based grants.
The investigation is retained in full because the rejection is the useful part — it documents why an appealing structural-isolation story doesn’t survive contact with an already-working Services deployment.
Superseded in direction by ADR-0007: Workload isolation in a single tailnet — access pools, which keeps the workload/homelab separation inside the tailnet as ACL-defined access pools, with a separate tailnet retained only as a deferred off-ramp. Its core finding — that a tailnet boundary pays off where paths should be closed anyway (non-homelab workload), not between homelab environments — still stands.
Today the homelab runs a single tailnet with tag-based environment separation. Services are already the primary exposure mechanism for containers across Prod/Test/Dev — advertised via Docktail labels and ScaleTail sidecars, with per-service MagicDNS names, per tailscale.md. That part of the design is settled and this ADR does not revisit it.
What is not settled is the environment boundary. Per tailscale-grants.md, isolation between Prod/Test/Dev is enforced entirely by grant rules:
That works, but the isolation is one policy-file edit deep. A bad merge, a mis-tagged node, or a fat-fingered tag:* destination collapses Prod/Test/Dev into one flat network. The blast radius of a policy mistake is the entire homelab.
Two Tailscale capabilities change the calculus:
POST /api/v2/organizations/-/tailnets provisions a fully isolated tailnet under the same account, no console clicks. The response carries a tailnet-scoped OAuth client — creation is org-scoped, but administration and deletion require that per-tailnet credential (worked example).externalTailnets plus grants referencing group://<alias>/<name> or tag://<alias>/<name>. It is explicitly designed for machine-to-machine access between trusted tailnets, which is exactly the homelab’s shape.The question this ADR frames: is a tailnet boundary the right isolation primitive for the homelab’s environments, and is the cross-tailnet seam cheap enough to live with?
Because Services are already the exposure model, the split’s real cost lands here rather than on greenfield design:
plex.<prod-tailnet>.ts.net replace the current flat convention, or does an alias layer hide the split?httpsEnabled is per-tailnet and off by default. Any new tailnet needs the setting turned on before TLS-terminating services work at all — a provisioning step that doesn’t exist today because the current tailnet was configured once, long ago.externalTailnets grant.OPEN and worth answering early: whether a Service can be published across a tailnet boundary. If yes, the boundary contract is service-level and clean. If not, cross-environment access is node-and-port level, which is coarser than the exposure model already in use — a genuine regression against current practice, and arguably the strongest argument for staying on one tailnet.
An earlier draft of this ADR analyzed the seam using link-based sharing (sharing docs) and concluded the model was largely unworkable — tags stripped, shares user-scoped so tagged machines can’t accept them, quarantine by default. Those constraints are real but belong to link-based sharing, not declarative node sharing. Declarative sharing operates at the policy level and supports tag references and machine-to-machine access directly. The two mechanisms coexist; picking the wrong one produces the wrong conclusion, which is why they’re separated explicitly below.
| Driver | Weight | Notes |
|---|---|---|
| Blast radius of a policy mistake | High | Current model: one file protects everything |
| Prod stability during Dev experiments | High | Dev is where breakage is expected |
| Operational overhead (per-tailnet admin, keys, DNS) | Medium | Lower than assumed — API-drivable |
| Dependence on an alpha feature | High | Declarative sharing is alpha + waitlist |
| Komodo GitOps flow across environments | Medium | Controller currently reaches all three |
| Observability / audit clarity | Medium | Per-tailnet audit is cleaner than tag filters |
| Churn to the existing Services layer | Medium | DNS suffixes, sidecar auth, per-tailnet httpsEnabled |
| Partner / shared access model | Medium | Adversarial model already documented |
| Cost | Low–Medium | OPEN: plan implications of additional tailnets |
Getting this distinction right is most of the decision.
| Link-based sharing | Declarative node sharing | |
|---|---|---|
| Status | GA (beta flag in docs), v1.4+ | Alpha, waitlist-gated |
| Unit of sharing | One machine → one user | Policy-level, resource classes |
| Tags across the boundary | Stripped | Supported via tag://<alias>/<tag> |
| Machine-to-machine | Not supported — only users accept shares | Explicitly supported |
| Quarantine | Inbound-only by default | Governed by allowIncomingConnections; OPEN whether quarantine semantics still apply |
| Setup | Invite link, out-of-band, manual accept | Double opt-in policy edits on both tailnets |
| Automatable | Poorly | Fully — policy file is GET/POST /api/v2/tailnet/-/acl |
| Group sync (SCIM/Google) | n/a | Unsupported — can’t reference synced groups |
Consequence: declarative sharing is the mechanism this design should be built on. Link-based sharing remains the right tool only for the partner/guest case — handing one machine to one outside human.
Double opt-in, per the declarative node sharing docs. Receiving tailnet declares what may be referenced:
Sharing tailnet accepts the relationship and writes the grant:
A grant referencing something absent from the other side’s allowExternalReferencesTo is silently ignored — no error, just no connectivity. That failure mode needs a verification step in any runbook.
Retained as the investigation record. Outcome noted on each.
httpsEnabled, policy push, scoped auth keys); tags and m2m survive the boundary; double opt-in means neither side can unilaterally widen access.Policy-file CI with ACL tests in GitHub Actions, mandatory review on the policy file, ban tag:* destinations, and tighten per-service grants on the Services already in place.
Rejected — keep the current single-tailnet structure with tag-based environment separation.
The structural-isolation argument is real, and declarative node sharing does make tailnet-per-environment technically achievable in a way link-based sharing never did. It is still the wrong trade for this homelab. Four reasons, in order of weight:
Services are already the exposure mechanism for most containers, and access works today — admin tiers, partner access, per-service MagicDNS names. A tailnet split rewrites that working surface: tailnet-qualified DNS suffixes, tailnet-scoped Docktail/ScaleTail sidecar auth, per-tailnet httpsEnabled, and per-tailnet auth keys and tagOwners. The failure it prevents — a fat-fingered grant flattening environments — has not actually occurred. Trading working access for hypothetical blast-radius reduction is the wrong direction.
The unresolved question from the investigation is decisive: if a Service cannot be published across a tailnet boundary, then cross-environment access degrades to node-and-port grants. That is coarser than the per-service exposure model already running. And if the shareable app has to physically live in the tailnet it’s shared from, containers move off the Prod Docker host — which drags in Komodo stack placement and the promotion path from ADR-0001. Either answer is worse than the status quo for a boundary nobody asked for.
Three tailnets means three policy files, three consoles, three sets of tailnet-scoped OAuth credentials, three auth-key rotations, and per-tailnet provisioning steps that don’t exist today. Add the silent-ignore failure mode — a grant referencing something absent from the other side’s allowExternalReferencesTo produces no error, just no connectivity — and routine debugging gets materially harder. Isolation that is expensive to operate gets worked around, and a worked-around boundary is worse than an honest tag.
Declarative node sharing is alpha and waitlist-gated. Under this ADR it would sit on the path between the Komodo controller and Prod periphery agents — meaning homelab manageability depends on an alpha feature. That inverts the risk this ADR set out to reduce.
tag:prod / tag:test / tag:dev) in one tailnet. No grants rewrite, no tag taxonomy change, no Services churn.Noted, not decided: the Prod/Test/Dev split could later collapse to Test and Prod only. Under ADR-0007, Dev’s remaining purpose is being reconsidered — if non-homelab work moves to a workload environment, a third homelab environment may not be earning its keep. That is a separate decision and deliberately out of scope here.
The investigation’s real finding is that the boundary axis was wrong. Splitting environments imposes a boundary across paths that need to stay open (admin, Komodo, observability, backups). Splitting workload from homelab puts the boundary where the paths should be closed anyway. See ADR-0007.
Each of these works today only because everything is one tailnet. Each would have needed an explicit externalTailnets/grant design — collectively, this list is the concrete form of reason #1 above:
tag:admin) → Prod/Test/Dev periphery agents — the canonical m2m case; technically workable via tag:// references, but it puts homelab manageability behind an alpha featureallowIncomingConnectionstag:storage NAS (UnRaid = Prod, Synology = Test)httpsEnabled PATCH, tagOwners, auth keys, policy push.Because the split is rejected, most of the original follow-on work is moot. What remains:
tag:* destinations, require review on the policy file, run tests in GitHub Actions.The rejection closes most of these. Recorded so the option isn’t re-litigated from scratch:
allowIncomingConnections for declaratively shared nodes — unresolved; relocated to ADR-0007, where it actually matters.externalTailnets, double opt-in, tag:///group:// external reference syntax, allowExternalReferencesTo, allowIncomingConnections. Also the source for the SCIM/Google synced-group limitation.01-tailnet-sandboxes — OAuth token exchange, POST /organizations/-/tailnets, and the tailnet-scoped OAuth credential that the org token cannot substitute for on delete02-tailnet-membership — tsnet embedded membership; relevant if services join tailnets directly rather than via host daemons03-declarative-sharing — externalTailnets config walkthrough and scoping external access by group/tag instead of blanket dst: ["*"]04-api-is-the-way — policy file as data via GET/POST /tailnet/-/acl, the httpsEnabled PATCH, scoped/tagged/short-lived auth keys, plus end-to-end.shProposed. Direction settled: workload/dev traffic stays in the current tailnet, separated by ACLs organized as access pools. The separate-tailnet option is deferred to a named trigger, not discarded.
Extends ADR-0006, which rejected splitting Prod/Test/Dev into separate tailnets. That investigation’s useful finding was that the boundary axis was wrong, not the boundary idea. This ADR takes the next step of that finding: separation of workload from homelab is real, but the mechanism is pools within one boundary, with structural separation preserved as an off-ramp.
Two distinct things are currently conflated inside one tailnet:
The earlier draft of this ADR weighed two mechanisms: a separate tailnet for workload (structural isolation, independent policy, clean audit boundary) against staying in the shared tailnet with tag separation (zero migration, isolation only advisory). The pivotal consideration — that a boundary’s alpha dependency sits on a path that is allowed to degrade, unlike ADR-0006’s admin/Komodo paths — is retained.
The judgment has since shifted on the trigger. The conditions that would justify a boundary — outside resources managing elements, sustained workload or external-party growth — are not real today. Building the structural split now pays complexity for a hypothetical, and it puts reachability that does not yet need to exist on an alpha, waitlist-gated feature (declarative node sharing). The boundary is kept as the off-ramp, priced and designed, but not built.
Single tailnet, workload separated by access pools. The structural split is deferred, not discarded.
Workload/Dev elements stay in the current tailnet, protected by ACLs rather than a boundary. Nothing new to provision, no per-tailnet settings (httpsEnabled, tagOwners, auth keys), and the Services/Docktail/ScaleTail exposure layer works exactly as today. External sharing, when it happens, stays link-based — one machine handed to one invited user — which covers ad-hoc guest access without committing to a separate tailnet.
The tailnet is no longer scoped as the homelab with everything else grafted on. It is the environment housing purpose-defined pools — provided services as one pool, development/workloads as another. The pool set is open: additional pools are named when a need clears the bar; none are pre-declared here (see Open Questions). This reframing drives everything that follows: nodes are organized by purpose AND access, and the ACL question becomes “which pools can which users and services reach.”
The access model follows the pools: grants are written in terms of who may reach which pool, in which direction. Two concept principles, without enumerating the pools themselves:
The pool taxonomy itself — names, membership, concrete grants — is deliberately unspecified in this ADR. It is a follow-on design task (see Consequences).
If the environment grows and outside resources need to manage elements, the workload moves to a new set of devices in a separate tailnet — the homelab tailnet untouched. The earlier draft’s separate-tailnet analysis is retained as the off-ramp design: structural isolation, independent policy file, clean audit boundary, external-group sharing without exposing private applications. Its alpha dependency (declarative node sharing) is acceptable once the boundary is real, because it then sits on a workload path that is allowed to degrade — the same risk posture, just correctly timed.
The pool taxonomy chosen now is the seam that cut will follow: pools map cleanly to future tailnet membership, and a split becomes a placement/policy exercise rather than a redesign. Policy-file CI should be in place first — a split that is mostly policy movement is only cheap if the policy is already tested.
| Concern | ADR-0006 (environment split) | This ADR (pools) |
|---|---|---|
| Admin/Komodo paths | Cut by the boundary — homelab manageability behind an alpha feature | Untouched; all stay in the homelab tailnet |
| Existing Services layer | Rewritten across three tailnets | Untouched |
| Partner/guest access | Complicated by tailnet-qualified DNS | Link-based sharing, unchanged from today |
| Alpha dependency | On the critical management path | On the deferred off-ramp only |
| Migration cost | Three tailnets, full grants rewrite | Zero now; the split is a future, triggered exercise |
| Blast radius | Hypothetical (bad grant) | Contained within pool grants — and CI-testable |
Noted, not built. When the trigger fires — outside resources managing elements, or sustained external-party growth — execute the off-ramp: new device set in a separate tailnet, provisioned via the API chain (OAuth exchange → create tailnet → httpsEnabled → tagOwners/policy → scoped auth keys), homelab tailnet untouched, cross-boundary access via declarative node sharing. Joining the waitlist for declarative sharing is only needed when the trigger becomes real; doing it now starts a clock for a feature nothing active depends on.
tag:* destinations, require review on the policy file, map pool grants to tests, run in GitHub Actions.The homelab currently standardizes on Docker (rootful, on Fedora Server baremetal hosts) managed through Komodo for GitOps. The prior narrative decision (“Komodo over Kubernetes (K3s + ArgoCD)” in planning/decisions.md) chose Docker-native Komodo over a K3s + ArgoCD stack on the grounds of complexity, risk tolerance, and concept alignment with existing familiarity with compose files.
This ADR reopens that axis. Two things have shifted the picture:
So the earlier “Kubernetes is overkill for a homelab” conclusion is being stress-tested against a simpler question: is the thing we’re optimizing for simplicity of the runtime or simplicity of declarative management? Those pull in different directions.
| Axis | Docker + Komodo | Kubernetes + ArgoCD |
|---|---|---|
| Runtime complexity | Low; Compose, one socket, familiar | High; control plane, CRDs, more failure modes |
| Declarative spin up/down | Possible, but needs branch/merge hygiene (ADR 0001) | Similar; kubectl apply / Git commit, controller reconciles, but follows a similar git architecture |
| Single pane of glass | Komodo dashboard (Docker-only) | ArgoCD dashboard (K8s-only) |
| Ecosystem / docs | Smaller (Komodo) | Ubiquitous (ArgoCD, CNCF) |
| Operational burden | Lower set-up complexity, higher per-change ceremony | Higher standing complexity, lower per-change friction |
| Skills transfer to work with OCP | Dev OCP cluster already covers this separately | Direct overlap with Red Hat stack |
The salient counterpoint to the original decision: Docker is less complex to run, but it can mean more steps to manage compared to native GitOps. The “overkill” argument was about runtime footprint; it does not directly address declarative management ergonomics, which is now the priority.
Standardize everything on Docker + Komodo ResourceSync.
Replace Komodo with a K3s or K8s control plane and ArgoCD as the GitOps controller for every workload.
Use Docker for “prod apps” and Kubernetes/ArgoCD for “workload apps.”
No conclusion yet, this ADR is intentionally left open.
The leading direction is Option C (split model), driven by the desire for easy declarative management without forcing the entire homelab onto a K8s control plane it may not need. The single strongest argument for abandoning the split and going all-in on Kubernetes (Option B) is the unified ArgoCD dashboard; that has not yet been judged worth the operational cost.
What would move this from Draft to a decision:
If Option C is adopted:
If Option B is adopted instead:
decisions.md — that section would need revisiting.The homelab needs a strategy for managing container images between the local environment and production. Initial considerations included running a local container registry (Artifactory/Lightwell analog) or relying on Docker Hub with version pinning. The homelab already uses Komodo GitOps, GitHub Actions for rendering, and Ansible validation playbooks.
Goals:
Skip running a local Docker registry as a stop-gap. Instead:
nginx:1.25-alpine instead of latest) — this is the primary guardrail.| Approach | Security Evaluation | Version Pinning | Offline | Maintenance |
|---|---|---|---|---|
| Local Docker registry | Manual (ad-hoc scanning) | ✅ Config-based | ✅ Yes | Low-Medium |
| Docker Hub + pinning | Manual (ad-hoc scanning) | ✅ Config-based | ❌ No | Low |
| Git pipeline + image scanning | ✅ Automated (PR gate) | ✅ PR-approved tag | ✅ CI cache | Medium |
| Artifactory/Lightwell | ✅ Scanning + promotion | ✅ Pinning | ✅ Yes | High |
Positive
Negative / risks
Mitigations
latestAccepted — proceed with extending the Git pipeline for image governance instead of running a local registry.
trivy image-scan --exit-code 1 --severity HIGH,CRITICALThe homelab needs a host OS for baremetal Docker nodes (LenovoMini 1 for Prod, LenovoMini 2 for Test, and the DevDocker VM on ProxMox). Requirements:
| Option | Release Cadence | Support Window | Notes |
|---|---|---|---|
| Fedora Server | ~6mo | ~13mo | Latest kernel + tooling, frequent upgrades |
| Rocky / Alma Linux | Major (9.x) | ~10yr | RHEL-compatible, boring & stable |
| Fedora CoreOS | Auto-updating streams | Per stream | Immutable, no package management at runtime |
| CentOS Stream | Rolling between RHEL releases | Continuous | Between Fedora and RHEL |
| Factor | Docker | Podman |
|---|---|---|
| Komodo Periphery support | First-class (native socket or API) | Rough — community workarounds only (DOCKER_HOST to socket emulation) |
| Rootless | Available (opt-in via dockerd-rootless-setuptool.sh) | Built-in, rootless by default |
| Compose | Docker Compose v2 | podman-compose or podman play kube |
After investigation, Podman support in Komodo relies on workarounds:
DOCKER_HOST env pointing at Podman’s Docker API socket (/run/user/$UID/podman/podman.sock)Rootless Docker was initially preferred for security, but three factors drove the switch to rootful:
dnf install + daemon enable. No dockerd-rootless-setuptool.sh, no loginctl enable-linger, no environment variable wiring./var/run/docker.sock mounted for API access. Updates via image pull, not manual binary replacement.Periphery runs as a Docker container, managed by the same Ansible playbook that provisions the host. The Komodo maintainer’s filesystem semantics concern (discussion #220) applies to rootless Docker where user namespaces cause path remapping — with rootful Docker, container and host paths are the same, so the concern doesn’t apply.
The host provisioning playbook will include a role komodo-periphery that handles Periphery as a container:
roles/
komodo-periphery/
tasks/
main.yml # pull, create dirs, run container
templates/
periphery.config.toml.j2 # config template
vars/
main.yml # defaults (core URL, port, etc.)Key choices:
network_mode: host — Periphery needs to reach containers by their host-level ports for monitoring. Host networking avoids port mapping conflicts and simplifies the network topology./var/run/docker.sock mount — gives Periphery the standard Docker API access. With rootful Docker this is the native socket, no indirection needed./:/mnt/host:ro — bind mounts the host root so Periphery can read filesystem stats for disk monitoring.restart_policy: unless-stopped — survives reboots and daemon restarts.periphery.config.toml.j2)The config template is rendered by Ansible with host-specific variables (core address, hostname, onboarding key), sourced from Ansible vault or group vars.
SELinux may block container socket access. On Fedora Server with SELinux enforcing, the Periphery container needs the :z flag on volume mounts or a dedicated SELinux policy. If Periphery fails to connect, check ausearch -m avc and add :z to the socket mount: "/var/run/docker.sock:/var/run/docker.sock:z".
Host networking limits to one Periphery per host. With network_mode: host, port 8120 is consumed directly on the host. Only one Periphery instance can run per machine. This is the expected topology — one agent per host.
Periphery container image tag strategy. Use a specific version tag (e.g., :0.2.5) not :latest for reproducible deployments. Update by bumping the version in Ansible vars and re-running the playbook.
Onboarding key rotates. The onboarding_key in [[connections]] is a one-time use key from the Komodo Core UI. After first connection, Periphery persists its session — the key is no longer needed. If re-deploying from scratch, generate a new key.
Config changes require container restart. If periphery.config.toml changes, Ansible must trigger a container restart. The docker_container module handles this automatically when config changes are detected via template diff.
Ansible vault for secrets. The onboarding key is sensitive and should be stored in Ansible vault, not in plaintext group vars.
Positive:
docker pull; Ansible handles version pinningNegative / risks:
:latest during bring-up, pin to a specific version once stable.Selecting a container runtime for baremetal Docker hosts involves balancing security, operational convenience, and tooling compatibility. This ADR breaks down three approaches and the fundamental tension between central-socket management and daemonless isolation.
The host OS decision is handled in ADR 0002: Baremetal Docker Host OS Selection.
The classic model: dockerd runs as root, exposes /var/run/docker.sock.
| Aspect | Detail |
|---|---|
| Socket | /var/run/docker.sock — single, system-wide, owned by root:docker |
| Daemon | dockerd running as root — always-on, manages all containers |
| Root model | Container processes run as root by default (can drop to user with USER in Dockerfile) |
| Komodo support | First-class — native socket access, all features work |
| Setup complexity | Minimal — dnf install docker, start daemon, add user to docker group |
| Compose | Native docker compose v2 support |
dockerd runs under a user namespace, exposes a user-scoped socket.
| Aspect | Detail |
|---|---|
| Socket | /run/user/$UID/docker.sock — per-user, still a single management endpoint |
| Daemon | dockerd-rootless running as the user — still a central daemon |
| Root model | Containers run in a user namespace — real root is not exposed, but root within the namespace |
| Komodo support | Viable with configuration — DOCKER_HOST env pointed at rootless socket |
| Setup complexity | Moderate — dockerd-rootless-setuptool.sh, loginctl enable-linger, env setup |
| Compose | Native docker compose v2 — works within user namespace |
No central daemon. Each container is launched directly by the user’s Podman client.
| Aspect | Detail |
|---|---|
| Socket | None by default — containers are child processes of the user’s shell |
| Daemon | No daemon — podman system service can emulate a Docker API socket on demand |
| Root model | Rootless by default — no user namespace needed, containers run as the calling user |
| Komodo support | Workarounds only — DOCKER_HOST to emulated socket, known bugs (stack status, network labels) |
| Setup complexity | Minimal for CLI — dnf install podman. Complex for management tooling (need podman system service as a systemd unit) |
| Compose | podman-compose or podman play kube — not drop-in compatible |
A Docker socket is equivalent to root access on the host. Any process that can write to the socket can:
Rootful Docker places this power on a single, well-known path (/var/run/docker.sock). The docker group is effectively root. The blast radius of a compromised process with socket access is the entire host.
Rootless Docker mitigates this through user namespaces. The daemon and all containers run within the user’s namespace — root inside a container is the user outside. A compromised socket can’t escape the user namespace, so the blast radius is that user’s resources only. However, it’s still a central daemon — if the rootless dockerd process is exploited, all that user’s containers are compromised.
Podman eliminates the central socket entirely. No process management attachment, no daemon to target. Each container is a child of its launching process. The blast radius is per container — no socket to poison. To use management tooling, you must explicitly start a podman system service socket, which reintroduces a central endpoint — but it’s opt-in, not the default.
This is the inverse of the security tradeoff:
| Runtime | Management model | Tooling friction |
|---|---|---|
| Docker (rootful) | One socket, one daemon, everything works | Zero — every tool speaks Docker API natively |
| Docker (rootless) | One socket per user, one daemon per user | Low — set DOCKER_HOST, same API |
| Podman | No socket, no daemon, containers are child processes | Higher — no standard API without emulation layer |
Without a central socket, management tooling faces a discovery problem:
podman system service socket, which must be running as a systemd user service.docker ps / podman ps enumerate running containers across a session? Podman uses a per-user boltdb database, but there’s no persistent daemon registering container state.podman-compose) uses a different project model than Docker Compose — Komodo’s stack status detection breaks (issue #504).With a central socket, management is trivial: connect to the well-known path, issue standard API calls, get predictable results. The cost is the security risk described above.
| Criterion | Docker (rootful) | Docker (rootless) | Podman (daemonless) |
|---|---|---|---|
| Komodo compatibility | Native | Configurable | Workarounds |
| Socket security | Dangerous (root equiv.) | Safer (user namespaced) | None by default |
| Privileged containers | Full support | Blocked | Blocked (rootless) |
| Port <1024 | Native | Workaround needed | Workaround needed |
| Systemd integration | Manual | Manual | Native (podman generate systemd) |
| cgroup management | Full | Limited | Limited (rootless) |
| Builds | Native | Native (BuildKit) | Native (Buildah) |
| Swarm mode | Supported | Blocked | N/A |
| Docker Compose | Native | Native | Partial (podman-compose) |
| Multi-arch builds | Native | Native | Native (Buildah) |
This ADR does not make a standalone decision — it informs ADR 0002. The current direction:
Docker (rootful) for maximum simplicity and tooling compatibility. The socket-security risk is accepted as an acceptable tradeoff for a homelab with trusted workloads.
Podman is the more secure architecture by design, and rootless Docker is a middle ground. Rootful Docker optimizes for operational simplicity — no setup ceremony, no port restrictions, no privileged container limitations. For a homelab where all workloads are user-deployed and trusted, the central-socket risk is manageable.
If the homelab expands to include untrusted or multi-tenant workloads, this should be revisited — rootless Docker or Podman would contain blast radius in a way rootful Docker cannot.
If Docker (rootful) is adopted (current direction):
dnf install docker, enable daemon, run containersIf Docker (rootless) or Podman is reconsidered later:
The homelab uses Komodo with ResourceSync to declare servers, stacks, deployments, and related resources from Git (an “app-of-apps” style flow: Git remains the audit trail; Komodo reconciles).
Goals:
main shows the whole picture — dev/, test/, and prod/ (or equivalent komodo/{dev,test,prod}/) layouts exist together on main so reviews and long-lived docs match reality after merges.main or follow a named branch (for example to soak a stack change in Dev before it lands on main).An earlier sketch of this approach lived in wip/komodo-artifact-templates/scratch/branch_management_ideas.md.
Single infrastructure repo; main is the default line of truth
After work merges, main holds the canonical layout for all managed environments (folder-separated configs, per existing repo structure docs).
One ResourceSync configuration per Komodo-managed environment (Dev, Test, Prod — or whichever environments are GitOps-managed)
Each [[resource_sync]] (or equivalent Komodo sync configuration) sets, at minimum:
komodo/dev/** for Dev), so a branch used for experiments does not accidentally apply Prod definitions.Branch-testing workflow
feature/xyz while Test and Prod continue to follow main).main.main so environments that should track main again have their branch/ref set back to main (or another agreed default).Known quirk (accepted): the ResourceSync TOML that sets branch = "feature/xyz" lives in Git. On the feature branch that file typically must reference the same branch so the sync pulls a self-consistent tree. After merge to main, that same file’s branch field must be updated to main (or removed to default) for environments that should no longer track the feature branch. This is intentional small, explicit edits on main, not hidden automation, until/unless you add tooling.
Releases / tags as a pin for Prod (optional, not required now)
Using tags or GitHub Releases so Prod tracks refs/tags/v… while main moves forward is a valid pattern when you want Prod immutability without branch gymnastics. With separate per-environment directories and per-sync branch control, the urgency is lower than in a single-shared-path model. We defer mandatory release-based promotion; revisit if main churn becomes painful for Prod.
Positive
main.Negative / risks
Mitigations (recommended, non-blocking)
komodo/sync/ or similar) so diffs are obvious.| Alternative | Why not chosen (for now) |
|---|---|
| Prod pinned only by releases/tags | Valuable later; deferred to reduce moving parts until branch steering is proven in daily use. |
| Separate repos per environment | Duplicates structure; conflicts with single-repo promotion story already documented. |
Only ever sync from main | Forces feature testing via copy/paste or temporary files outside Git flow; rejected. |
wip/komodo-artifact-templates/scratch/branch_management_ideas.md