Dedicated tree-docs guidebook under vibe/guidebooks/erp/ for the lab's most data-critical app, cross-linked from the applications hub (bidirectional): - README.md : Dolibarr 22.0.4 on Postgres; data-criticality; overview diagram; the Vault-unseal-before-scale recovery ordering (CAUTION). - deployment.md : upstream image + custom entrypoint (MySQL->psql), the 50Gi Longhorn RWX documents PVC, Vault CRDs + the shared app_roles iac, init scripts (conf.php creds, table-ownership), ingress, CI. - backup-and-recovery.md: the Ansible CronJob pg_dump (daily 04:00, 15-day retention) + restore Job (scale-0 -> restore -> scale-1); the cluster recovery ordering (Longhorn -> Vault unseal -> erp scale-up). - operations.md : the read-only bin/arcodange CLI, static/company.json, Deno+Playwright tests, day-2 ops. erp code via full gitea URLs; CLUSTER_RECOVERY.md by name; 2 mermaid diagrams MCP-validated; zero dead links. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
119 lines
9.9 KiB
Markdown
119 lines
9.9 KiB
Markdown
[vibe](../../README.md) > [Guidebooks](../README.md) > **Applications**
|
|
|
|
# Applications
|
|
|
|
> **Status:** ✅ Active
|
|
> **Last Updated:** 2026-06-23
|
|
> **Upstream:** [Lab ecosystem hub](../lab-ecosystem/README.md) · [01 · factory](../lab-ecosystem/01-factory.md)
|
|
> **Downstream:** [webapp](webapp.md) · [url-shortener](url-shortener.md)
|
|
> **Related:** [naming-conventions](../lab-ecosystem/naming-conventions.md) · [tools secrets-and-vso](../tools/secrets-and-vso.md) · [factory postgres-iac](../factory-provisioning/opentofu/postgres-iac.md) · [safe-prod-like-environment ADR](../../ADR/0001-safe-prod-like-environment.md)
|
|
|
|
This guidebook maps the **deployed applications** — the workloads ArgoCD runs in their own `<app>` namespace — and, more importantly, the **single repeatable pattern** every one of them follows. Once you know the pattern, every app reads as a variation on the same skeleton: a Gitea repo whose contents (Dockerfile + Helm chart + optional Vault IaC + CI) and whose `<app>` name fully determine how it builds, deploys, gets its secrets, and is reached from the network.
|
|
|
|
Two apps are presented in depth as the canonical archetypes: [webapp](webapp.md) (Go + external Postgres) and [url-shortener](url-shortener.md) (Rust + embedded SQLite). Other apps in the cluster — `erp`, `dance-lessons-coach`, `telegram-gateway`, `plausible` — instantiate the same pattern; `erp` has its own [ERP guidebook](../erp/README.md) because it carries far more moving parts than the two archetypes.
|
|
|
|
## The common app pattern
|
|
|
|
Every application is a self-contained Gitea repo under the `arcodange-org` (or `arcodange`) org that carries the same four ingredients. The `<app>` name — the repo name — is the join key that threads through all of them (see the [naming-conventions concept](../lab-ecosystem/naming-conventions.md)).
|
|
|
|
| Ingredient | Path in the app repo | What it is | Required? |
|
|
|---|---|---|---|
|
|
| **Dockerfile** | `Dockerfile` | Multi-stage build producing the runtime image, pushed to the Gitea container registry as `gitea.arcodange.lab/<org>/<app>` | ✅ always |
|
|
| **Helm chart** | `chart/` | `Chart.yaml` + `values.yaml` + `templates/` (deployment, service, ingress, serviceaccount, hpa, config, NOTES, optional PVC, optional Vault CRDs) — the unit ArgoCD syncs | ✅ always |
|
|
| **Vault IaC** | `iac/` | OpenTofu that declares the app's Vault objects: a Postgres dynamic-secret role keyed on `<app>` + a Kubernetes auth role bound to the `<app>` ServiceAccount. The canonical form pulls the [`app_roles` module](../tools/secrets-and-vso.md) from `tools`; the privileged `app_policy` half is declared centrally so the app repo never holds it | 🟡 only apps needing Postgres / Vault KV |
|
|
| **CI workflows** | `.gitea/workflows/` | A `dockerimage` job that builds + pushes the image on every `main` push, and (when `iac/` exists) a `vault` job that runs `tofu apply` against Vault, gated to changes under `iac/*.tf` | ✅ image build · 🟡 vault apply |
|
|
|
|
### How a chart becomes a running app
|
|
|
|
Factory's [ArgoCD app-of-apps](../lab-ecosystem/01-factory.md) emits **one `Application` CRD per app**, and every field is derived mechanically from the `<app>` name:
|
|
|
|
| Application field | Value | Source |
|
|
|---|---|---|
|
|
| `repoURL` | `https://gitea.arcodange.lab/<org>/<app>` | `<app>` + optional org override |
|
|
| `path` | `chart` | fixed convention |
|
|
| `namespace` | `<app>` (`CreateNamespace=true`) | `<app>` |
|
|
| `syncPolicy` | `automated` with `prune: true` + `selfHeal: true` | app-of-apps default |
|
|
|
|
The same `<app>` name is also the Postgres database/role name, the Vault role name, the KV path prefix, and the ServiceAccount name — one string keying the whole stack. See [naming-conventions](../lab-ecosystem/naming-conventions.md).
|
|
|
|
### Ingress convention — `.fr` public vs `.lab` internal
|
|
|
|
Every app that serves HTTP exposes itself through two Traefik ingresses with a fixed split by domain suffix:
|
|
|
|
| Ingress | Domain | Traefik entrypoint | Middlewares | TLS / cert | Reached via |
|
|
|---|---|---|---|---|---|
|
|
| **Public** | `<app>.arcodange.fr` | `web` | `kube-system-crowdsec@kubernetescrd` (CrowdSec bouncer) | terminated at the edge | the **Cloudflared tunnel** — the public web entrypoint |
|
|
| **Internal** | `<app>.arcodange.lab` | `websecure` | `localIp@file` (LAN-only allow-list) | a cert from **either** the Traefik `letsencrypt` resolver **or** cert-manager's `step-issuer` (`StepClusterIssuer`) | the LAN directly |
|
|
|
|
> [!NOTE]
|
|
> The two archetypes differ only in cert mechanism, not in the convention: webapp's internal ingress carries the `letsencrypt` certresolver annotations, while url-shortener's internal ingress requests its cert from cert-manager's `step-issuer`. Both still ride `websecure` + `localIp@file`; both still expose a `.fr` twin behind the CrowdSec middleware.
|
|
|
|
## Two archetypes compared
|
|
|
|
The deployed apps fall into two shapes. Pick the matching archetype's page when adding or modifying an app.
|
|
|
|
| Aspect | [webapp](webapp.md) | [url-shortener](url-shortener.md) |
|
|
|---|---|---|
|
|
| Language / build | **Go** (golang:1.23 → alpine runtime) | **Rust** (cargo-chef → `scratch` runtime) |
|
|
| State | **External Postgres**, reached through the `tools` **pgbouncer** pooler with credentials delivered by **VSO** | **Embedded SQLite** on a `/data` file |
|
|
| Persistence | none in-cluster (DB lives on `pi2`) | a **Longhorn RWO PVC** (`storageClassName: longhorn`, `helm.sh/resource-policy: keep`) mounted at `/data` |
|
|
| Replicas | **scalable** (stateless pods; HPA-ready) | **single** — RWO volume cannot be shared across pods |
|
|
| `iac/` + Vault | **yes** — declares a Postgres dynamic-secret role + a k8s auth role; pod consumes **dynamic, rotating** DB creds via `VaultAuth` + `VaultDynamicSecret` + `VaultStaticSecret` CRDs | **none** — no Vault objects, no DB role |
|
|
| Recovery | restore from the **PostgreSQL backup** (factory `05_backup` → `/mnt/backups`) | **Longhorn block-device recovery** of the PVC (raw replica `.img` files) — see [ansible recover](../factory-provisioning/ansible/06-recover.md) |
|
|
|
|
The choice is essentially *"shared/scalable state that survives a single node"* (Postgres, webapp shape) versus *"self-contained single-writer state co-located with the pod"* (SQLite-on-Longhorn, url-shortener shape). The trade-off and why both are kept prod-like is recorded in the [safe-prod-like-environment ADR](../../ADR/0001-safe-prod-like-environment.md).
|
|
|
|
## Generic app lifecycle
|
|
|
|
```mermaid
|
|
%%{init: {'theme': 'base'}}%%
|
|
flowchart LR
|
|
classDef src fill:#2563eb,stroke:#1e40af,color:#fff
|
|
classDef proc fill:#059669,stroke:#047857,color:#fff
|
|
classDef store fill:#7c3aed,stroke:#6d28d9,color:#fff
|
|
classDef net fill:#b45309,stroke:#92400e,color:#fff
|
|
|
|
REPO["app repo<br>Dockerfile + chart/ + iac/ + .gitea/workflows"]:::src
|
|
IMG["image pushed<br>gitea registry <org>/<app>"]:::store
|
|
VAULT["tofu apply<br>Vault role + Postgres role"]:::store
|
|
ARGO["ArgoCD<br>deploys chart (ns <app>)"]:::proc
|
|
POD["pod<br>Postgres via pgbouncer + VSO<br>OR SQLite on Longhorn PVC"]:::proc
|
|
TR["Traefik ingress<br>.fr public / .lab internal"]:::net
|
|
|
|
REPO -- "dockerimage CI" --> IMG
|
|
REPO -- "vault CI (iac apps only)" --> VAULT
|
|
IMG --> ARGO
|
|
VAULT -. "creds for DB apps" .- POD
|
|
ARGO --> POD
|
|
POD --> TR
|
|
```
|
|
|
|
1. The **app repo** holds the four ingredients: a Dockerfile, a `chart/`, an optional `iac/`, and `.gitea/workflows`.
|
|
2. On a push to `main`, the **dockerimage** workflow builds the image and pushes it to the Gitea container registry as `<org>/<app>`.
|
|
3. For apps with an `iac/`, the **vault** workflow runs `tofu apply` to declare the app's Postgres dynamic-secret role and Kubernetes auth role in Vault.
|
|
4. **ArgoCD** (factory's app-of-apps) syncs the chart into the `<app>` namespace, deriving `repoURL`/`path`/`namespace` from the `<app>` name.
|
|
5. The **pod** comes up; a Postgres-backed app receives rotating DB credentials through pgbouncer + VSO, while a SQLite-backed app mounts its Longhorn PVC at `/data`.
|
|
6. **Traefik** publishes the pod through two ingresses: the `.fr` public route (CrowdSec middleware, via the Cloudflared tunnel) and the `.lab` internal route (`websecure` + `localIp` + a letsencrypt/step-issuer cert).
|
|
|
|
## Index
|
|
|
|
| Page | Archetype | Status |
|
|
|---|---|---|
|
|
| [webapp](webapp.md) | Canonical **Go + external Postgres** exemplar — `iac/` + Vault dynamic creds, scalable stateless pods | ✅ Active |
|
|
| [url-shortener](url-shortener.md) | **Rust + embedded SQLite** counterpart — single replica on a Longhorn RWO PVC, no Vault | ✅ Active |
|
|
|
|
`erp` and the other apps (`dance-lessons-coach`, `telegram-gateway`, `plausible`) follow the same pattern; `erp` is documented in depth in its own [ERP guidebook](../erp/README.md).
|
|
|
|
## Maintenance rule
|
|
|
|
> [!IMPORTANT]
|
|
> **When an app's repo changes shape, its page here changes in the same PR.** If you alter the chart structure, the ingress convention, the Vault wiring, the persistence model, or the CI workflows of a deployed app, update this hub and the relevant archetype page in the same change. A reference map that drifts from the real `chart/` and `iac/` sends agents confidently down dead paths.
|
|
|
|
## Cross-references
|
|
|
|
- [01 · factory](../lab-ecosystem/01-factory.md) — the ArgoCD app-of-apps that emits one `Application` per app on this page.
|
|
- [tools secrets-and-vso](../tools/secrets-and-vso.md) — the `app_policy` + `app_roles` module pair that turns `<app>` into Vault policies, roles, and CI identities; the VSO runtime path the Postgres archetype rides.
|
|
- [factory postgres-iac](../factory-provisioning/opentofu/postgres-iac.md) — the per-app PostgreSQL database + `<app>_role` the webapp archetype depends on.
|
|
- [naming-conventions](../lab-ecosystem/naming-conventions.md) — the `<app>` join key that threads through repo, image, namespace, DB, Vault role, and ServiceAccount.
|
|
- [safe-prod-like-environment ADR](../../ADR/0001-safe-prod-like-environment.md) — why the lab keeps apps deployed prod-like and the state/recovery trade-offs behind the two archetypes.
|