Deployment and Updates
Declarative layers
flowchart LR
git[("Gitea repositories")] --> checks["Restricted CI<br/>validation · build · scan"]
checks --> registry[("Gitea registry<br/>commit-tagged images")]
registry --> trusted["Repository-scoped runners<br/>deploy · synchronize"]
subgraph converge["DECLARATIVE CONVERGENCE"]
direction TB
tofu["OpenTofu<br/>Proxmox VMs"]
ansible["Ansible<br/>hosts · mounts · timers"]
tofu --> ansible
end
trusted --> tofu
ansible --> compose["Docker Compose<br/>household services"]
trusted --> k3s["K3s<br/>personal web apps"]
trusted --> knowledge["Open WebUI<br/>knowledge collections"]
compose <--> state[("TrueNAS + volumes<br/>mutable state")]
k3s <--> state
classDef source fill:#282416,stroke:#c6a95b,color:#f2e4b8;
classDef automation fill:#2b1d15,stroke:#c47d4a,color:#f3d1ba;
classDef compute fill:#211b32,stroke:#9c82d4,color:#e3d9fa;
classDef storage fill:#10271e,stroke:#58b184,color:#c7efda;
class git source;
class checks,trusted,tofu,ansible automation;
class compose,k3s,knowledge compute;
class registry,state storage;
Infrastructure definitions make the intended shape inspectable and repeatable without pretending every runtime byte belongs in Git. Applications receive secrets at deployment time; plaintext credentials do not belong in definitions or documentation.
Validation and deployment
Strict repository checks validate inventory consistency, suspicious tracked files, Ansible syntax, the rendered inventory, and every Compose project. CI uses placeholder values, so routine validation does not require production decryption keys.
Operations-repository deployment is manually dispatched through Gitea Actions. A concurrency guard permits one deployment at a time, and the operator can target the complete active environment or a single VM role. Ansible converges the selected hosts from the reviewed Git revision. Application repositories deploy automatically only after their commit-tagged image build succeeds.
Gitea Actions trust boundaries
Actions run on ward-vm-cicd-01, not on an application VM. General jobs use
rootless Docker-in-Docker runners with no host Docker socket. The instance-wide
gitea-shared-ci runner exposes only the shared-ci label, runs one job at a
time, and is used for application builds. The repository-scoped home-ops-ci
runner handles validation, secret scanning, and Renovate for the operations
repository.
Both restricted runners have fixed outer-container addresses. Host firewall rules allow them to reach Gitea and DNS while rejecting other RFC1918 networks. They can check out source and push images, but they cannot contact Kubernetes, Open WebUI, or other private services.
Production jobs use separate repository-scoped runners:
| Repository | Trusted label | Responsibility |
|---|---|---|
home-ops | home-ops-deploy | Manual Ansible convergence |
home-ops-docs | home-ops-docs-deploy | Documentation rollout to K3s |
personal-website | personal-website-deploy | Website rollout to K3s |
recipe-book | recipe-book-deploy | Recipe rollout and knowledge synchronization |
web-log | web-log-deploy | Web log and files-service rollout |
knowledge-base | knowledge-base-sync | Open WebUI knowledge synchronization |
These runners execute directly inside hardened runner containers. They have no Docker socket or host filesystem mounts, and each registration identity can accept jobs only from its own repository. Kubernetes configuration and Open WebUI API keys are supplied only to the corresponding trusted job.
Application workflows separate build from deployment. A build runs on
shared-ci, pushes an immutable image tagged with the Git commit, and must
succeed before the trusted deployment job starts. This keeps untrusted build
tools away from production credentials and private network access.
Update policy
Container versions are pinned rather than allowed to float silently. Renovate runs daily, maintains a dependency dashboard, and manages an explicit allowlist of public images. An update must be approved before Renovate opens a pull request.
Release notes are reviewed before merge, then the normal validation and deployment path applies. App-owned databases and caches are upgraded with the parent application’s guidance rather than as unrelated image changes.
Renovate currently covers selected Compose images. Operating-system packages, Ansible collections, Dockerfiles, language dependencies, firmware, and software outside the allowlist still require deliberate maintenance.
Compose and K3s
Compose is the default because it is direct and well suited to stateful household services on known hosts. K3s runs on a dedicated single-node VM for personal web applications and Kubernetes experimentation. It is not a migration target for every mature Compose workload.