[vibe](../../README.md) > [Guidebooks](../README.md) > **Lab ecosystem**
# Lab ecosystem
> **Status:** ✅ Active
> **Last Updated:** 2026-06-23
> **Related:** [ADR-0001 · safe prod-like environment](../../ADR/0001-safe-prod-like-environment.md) · [PRD · safe prod-like environment](../../PRD/safe-prod-like-environment/README.md) · [INV-001 · prod blast-radius couplings](../../investigations/INV-001-prod-blast-radius-couplings.md)
## What this is
This guidebook is the **end-to-end map of the Arcodange home lab** — how the three repos (`factory`, `tools`, `cms`), the three Raspberry Pis, and the cloud edge wire together into one running system. It is a *descriptive reference map*, not a procedure: it answers *"how does this fit together right now?"*. For *"how do I add a new app step by step?"* see the [new-web-app runbook](../../../doc/runbooks/new-web-app/README.md); for *"why was it built this way?"* see the [factory ADRs](../../../doc/adr/README.md).
The lab is run from **one control node** — a MacBook Pro M4 — driving everything via Ansible (imperative host setup) and OpenTofu (declarative cloud/Gitea/Vault/Postgres state). The three Pis (`pi1`/`pi2`/`pi3` = `192.168.1.201-203`) sit behind a home Livebox. `pi1` is the k3s server; `pi2`/`pi3` are agents. Gitea + PostgreSQL run as Docker Compose **outside** k3s on `pi2`'s disk; everything else runs **inside** k3s on Longhorn distributed block storage. The public edge is a Cloudflared Zero-Trust tunnel into the internal Traefik, with Cloudflare DNS and Zoho email fronting `arcodange.fr`.
## The whole lab, end to end
```mermaid
%%{init: {'theme': 'base'}}%%
flowchart TB
classDef ctrl fill:#2563eb,stroke:#1e40af,color:#fff
classDef host fill:#0891b2,stroke:#0e7490,color:#fff
classDef proc fill:#059669,stroke:#047857,color:#fff
classDef store fill:#7c3aed,stroke:#6d28d9,color:#fff
classDef edge fill:#d97706,stroke:#b45309,color:#fff
classDef dead fill:#6b7280,stroke:#4b5563,color:#fff
MAC["Control node (MacBook Pro M4)
Ansible + OpenTofu"]:::ctrl
subgraph LAN["Home LAN (Livebox) — 192.168.1.0/24"]
subgraph PI2["pi2 · 192.168.1.202 (docker-compose, outside k3s)"]
GITEA["Gitea
arcodange-org/*"]:::host
PG[("PostgreSQL")]:::store
end
subgraph K3S["k3s cluster — pi1 server, pi2/pi3 agents"]
ARGO["ArgoCD app-of-apps
/argocd"]:::proc
LH[("Longhorn
block storage")]:::store
VAULT["Vault + VSO
secrets"]:::store
TRAEFIK["Traefik
ingress"]:::proc
TOOLS["tools namespace
(Vault, Grafana, CrowdSec, …)"]:::host
APPS["app namespaces
(webapp, erp, cms, …)"]:::host
end
OLLAMA["pi3 · ollama"]:::host
end
subgraph CLOUD["Cloud edge"]
CF["Cloudflare DNS
+ Cloudflared tunnel"]:::edge
ZOHO["Zoho
email (arcodange.fr)"]:::edge
GCS[("GCS gs://arcodange-tf
OpenTofu state + Longhorn backup")]:::store
end
INTERNET(["Internet"]):::edge
MAC -- "Ansible: provision hosts, k3s, docker-compose" --> PI2
MAC -- "Ansible: k3s, Longhorn, Traefik" --> K3S
MAC -- "OpenTofu: Gitea/Vault/PG/Cloudflare/OVH state" --> GITEA
MAC -- "OpenTofu state" --> GCS
GITEA -- "repoURL chart/" --> ARGO
ARGO -- "Application CRDs (prune+selfHeal)" --> TOOLS
ARGO -- "Application CRDs (prune+selfHeal)" --> APPS
VAULT -- "VSO injects secrets into pods" --> TOOLS
VAULT -- "VSO injects secrets into pods" --> APPS
APPS -- "dynamic creds" --> PG
LH -. "PVCs" .- TOOLS
LH -. "PVCs" .- APPS
LH -- "backup target" --> GCS
INTERNET --> CF -- "tunnel" --> TRAEFIK --> APPS
INTERNET --> ZOHO
```
1. The **control node** (MacBook) provisions the three Pis with Ansible (OS, disks, Docker, k3s, Longhorn, Traefik) and manages all SaaS/Gitea/Vault/Postgres state with OpenTofu.
2. On **pi2**, Gitea and PostgreSQL run as Docker Compose *outside* k3s, on the local disk — they are the source-of-truth services the cluster depends on.
3. OpenTofu keeps its **state in GCS** (`gs://arcodange-tf`), and Longhorn pushes volume **backups** to the same GCS project.
4. **Gitea** hosts every app repo; each repo's `chart/` directory is the deployable Helm chart.
5. **ArgoCD's app-of-apps** turns each Gitea repo into an `Application` CRD (automated `prune` + `selfHeal`) that deploys into the `tools` namespace and the per-app namespaces.
6. **Vault** is the single source of truth for secrets; the **Vault Secrets Operator (VSO)** injects them into pods via Kubernetes auth, and apps draw dynamic PostgreSQL credentials from Vault against `pi2`.
7. **Longhorn** provides the PVCs the in-cluster workloads mount, and backs up to GCS.
8. The **public edge** routes Internet traffic through Cloudflare DNS and a Cloudflared Zero-Trust **tunnel** into the internal **Traefik**, which fronts the app namespaces; **Zoho** handles `arcodange.fr` email.
> [!NOTE]
> The ArgoCD Helm chart under [`argocd/`](../../../argocd/) is defined and templated, but **ArgoCD itself is not currently deployed in-cluster** (its install step is commented out in the `03_cicd` provisioning). The app-of-apps wiring documented here is the intended steady state; see [01 · factory](01-factory.md) for the caveat.
## Deploy / secrets / DNS flows
- **Deploy flow.** Push to a Gitea repo → CI builds an image into the Gitea registry → ArgoCD (via the app-of-apps and, for some apps, the Image Updater) syncs the `chart/` directory into the matching namespace with `prune` + `selfHeal`. The whole chain keys off one `` identifier — see [naming-conventions.md](naming-conventions.md).
- **Secrets flow.** Vault is the **single source of truth** (no sops/age). CI authenticates to Vault via **Gitea OIDC JWT** (role `gitea_cicd_`); pods receive secrets at runtime via **VSO** (Kubernetes auth + `VaultDynamicSecret` CRDs). Detail in [secrets-and-vault.md](secrets-and-vault.md).
- **DNS / edge flow.** Internal names resolve under `*.arcodange.lab` (Pi-hole + Step-CA-issued TLS). Public traffic for `arcodange.fr` enters through Cloudflare and a Cloudflared tunnel to internal Traefik; public TLS is Let's Encrypt via Traefik's DNS-challenge (DuckDNS). Email runs through Zoho. Edge detail in [03 · cms](03-cms.md).
## Master index
| Page | What it maps | Status |
|---|---|---|
| [01 · factory](01-factory.md) | The cornerstone admin repo: Ansible host/cluster provisioning, ArgoCD app-of-apps, OpenTofu (`iac/`), and per-app PostgreSQL (`postgres/iac/`) | ✅ Active |
| [02 · tools](02-tools.md) | The `tools` namespace: Vault, VSO, Prometheus, Grafana, CrowdSec, poolers, Redis/KeyDB, Plausible + ClickHouse, the `tool` library chart | ✅ Active |
| [03 · cms](03-cms.md) | The public-facing site: Nuxt static site, Cloudflare zone + tunnel + Turnstile, Zoho email (MX/SPF/DKIM/DMARC/BIMI + aliases) | ✅ Active |
| [naming-conventions.md](naming-conventions.md) | The `` join key — one kebab-case name reused identically across Gitea, PG, Vault, k8s, ArgoCD, GCS, DNS | ✅ Active |
| [secrets-and-vault.md](secrets-and-vault.md) | How Vault is the single source of truth: Gitea OIDC JWT for CI, VSO injection for pods, dynamic PostgreSQL creds | ✅ Active |
| [storage-and-recovery.md](storage-and-recovery.md) | Longhorn block storage, GCS backup target, and the tested power-cut recovery sequence | ✅ Active |
## Status legend
✅ done · 🟡 beta · 🔴 critical · ⚠️ known issue · ❌ disabled · ⬜ not started.
## Maintenance rule
> [!IMPORTANT]
> **If you alter a component documented here, update its page in the same change.** A reference map that drifts from reality sends readers (and agents) confidently down dead paths. The PR that changes the component is the PR that updates its guidebook page — treat the doc edit as part of the diff, not a follow-up.
## Cross-references
- [ADR-0001 · safe prod-like environment](../../ADR/0001-safe-prod-like-environment.md) — the decision this map supports.
- [PRD · safe prod-like environment](../../PRD/safe-prod-like-environment/README.md) — the product framing of an isolated, prod-like sandbox.
- [INV-001 · prod blast-radius couplings](../../investigations/INV-001-prod-blast-radius-couplings.md) — the couplings (the `` join key, shared Vault/PG/Longhorn) that make blast radius real.
- [doc/adr](../../../doc/adr/README.md) — the canonical infrastructure ADRs (FRENCH).
- [new-web-app conventions](../../../doc/runbooks/new-web-app/conventions.md) — the authoritative source for the `` naming convention.