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>
18 KiB
vibe > Guidebooks > ERP > Deployment
Deployment
Status: ✅ Active Last Updated: 2026-06-23 Upstream: ERP hub · Applications hub · 01 · factory Downstream: Backup & recovery · Operations Related: tools secrets-and-vso · factory postgres-iac · factory ci-apply-flow · naming-conventions · webapp
This page maps how erp is deployed: the chart that wraps the upstream Dolibarr image, the runtime trick that makes a MySQL-assuming application speak PostgreSQL, the 50Gi document PVC that holds every business record, the Vault CRDs that feed it credentials, and the OpenTofu + CI that declare its Vault objects. It is the most data-critical app in the lab; the iac/ runs through the same tofu apply pipeline as every other app — see factory ci-apply-flow.
1 · App & image
erp is Dolibarr pulled straight from the upstream dolibarr/dolibarr Docker Hub image — there is no repo-built image and no Dockerfile. The chart adapts the upstream container at runtime instead of forking it.
| Field | Value | Source |
|---|---|---|
| Application | Dolibarr ERP/CRM (PHP / Apache) | chart/Chart.yaml |
| Version | 22.0.4 (chart appVersion: "22.0.4") |
chart/Chart.yaml |
| Image | dolibarr/dolibarr:22.0.4 — upstream, pullPolicy: IfNotPresent |
chart/values.yaml |
| Image tag | image.tag empty → defaults to chart appVersion ({{ .Values.image.tag | default .Chart.AppVersion }}) |
chart/templates/deployment.yaml |
| Served at | https://erp.arcodange.lab (internal only) |
chart/templates/config.yaml |
| Container command | ["/bin/bash", "/usr/local/bin/custom-entrypoint.sh", "apache2-foreground"] |
chart/templates/deployment.yaml |
Note
Because erp consumes an upstream image, there is no
docker-build-and-pushworkflow in.gitea/workflows/— unlike webapp, which builds and pushes its own image. erp's only workflow is the OpenTofu/Vault one (see §8).
2 · Postgres, not MySQL
Dolibarr classically assumes MySQL, but erp runs on PostgreSQL. Two pieces make that work, both at startup, both inside the custom entrypoint which wraps the upstream docker-run.sh:
- MySQL → psql rewrite. When
DOLI_DB_TYPE == "pgsql", the entrypointseds the upstream/usr/local/bin/docker-run.shin place, replacing itsmysql -u ... < ${file}SQL invocation withPGPASSWORD=... psql -U ... -h ... -p ... -d ... < ${file}. - Apache
ServerName. It strips the scheme fromDOLI_URL_ROOTand sets the ApacheServerNamein000-default.conf(and appends toapache2.conf) so the vhost matcheserp.arcodange.lab. - It then
execs the originaldocker-run.sh "$@"(i.e.apache2-foreground).
The non-secret database wiring lives in the erp-config ConfigMap, injected via envFrom:
| Env var | Value | Meaning |
|---|---|---|
DOLI_DB_TYPE |
pgsql |
Selects PostgreSQL — triggers the entrypoint rewrite |
DOLI_DB_HOST |
pgbouncer.tools |
Connects through the tools pgbouncer pooler |
DOLI_DB_HOST_PORT |
5432 |
Pooler port |
DOLI_DB_NAME |
erp |
The per-app database (provisioned by factory postgres-iac) |
DOLI_URL_ROOT |
https://erp.arcodange.lab |
Drives the Apache ServerName |
DOLI_ENABLE_MODULES |
Societe,Facture |
Third-parties + invoicing modules |
DOLI_COMPANY_NAME |
Arcodange |
Seeded company name |
DOLI_COMPANY_COUNTRYCODE |
FR |
Seeded country |
PHP_INI_DATE_TIMEZONE |
Europe/Paris |
PHP timezone |
DOLI_AUTH |
dolibarr |
Native Dolibarr auth |
DOLI_CRON |
0 |
In-container cron disabled |
DOLI_DB_USER / DOLI_DB_PASSWORD are not in the ConfigMap — they come from Vault (see §5).
Warning
The psql rewrite is a textual
sedagainst an upstream file. If a future Dolibarr image changes the exactmysql ... < ${file}line indocker-run.sh, the substitution silently stops matching and SQL imports fall back tomysql(which is absent) — startup SQL then fails. Re-verify the entrypoint pattern whenever theappVersionis bumped.
3 · Persistence — the document PVC
A single PVC named erp holds every business record. It is the most important object in the chart.
| Field | Value | Source |
|---|---|---|
| Name | erp (erp.fullname) |
chart/templates/pvc.yaml |
| Access mode | ReadWriteMany (RWX) |
chart/templates/pvc.yaml |
| Size | 50Gi |
chart/templates/pvc.yaml |
| StorageClass | longhorn |
chart/templates/pvc.yaml |
| Retention | annotation helm.sh/resource-policy: keep — survives a helm uninstall |
chart/templates/pvc.yaml |
The Deployment mounts the same PVC at three paths via subPath:
| Mount path | subPath | Holds |
|---|---|---|
/var/www/documents |
documents |
Invoices, attachments, generated PDFs — the critical business data |
/var/www/html/custom |
custom |
Custom/installed Dolibarr modules |
/var/backups |
backups |
In-pod backup landing area |
Caution
Losing this PVC loses all business documents.
/var/www/documentscontains the only copy of uploaded invoices, attachments, and generated PDFs — these are real accounting records, not regenerable cache. Thehelm.sh/resource-policy: keepannotation protects it from a chart uninstall, but it does not protect against a Longhorn-volume loss or a node failure. Treat the PVC as primary data and rely on Backup & recovery for off-volume copies.
4 · Chart shape
| Aspect | Value | Source |
|---|---|---|
replicaCount |
1 (single replica) | chart/values.yaml |
| Autoscaling | disabled (autoscaling.enabled: false; no HPA rendered) |
chart/values.yaml |
| Service | ClusterIP, port 80 → targetPort: http |
chart/templates/service.yaml |
| Ingress host | erp.arcodange.lab, path / (Prefix) |
chart/templates/ingress.yaml |
| Ingress entrypoint | Traefik websecure + router.tls: "true" |
chart/values.yaml |
| TLS cert | certresolver: letsencrypt, domain arcodange.lab / SAN erp.arcodange.lab |
chart/values.yaml |
| Middleware | localIp@file — internal only, no public .fr host |
chart/values.yaml |
| revisionHistoryLimit | 5 |
chart/templates/deployment.yaml |
Warning
Single replica on an RWX PVC.
replicaCount: 1with autoscaling off means erp has no redundancy — a node or pod failure is a full outage until rescheduled. A credential rotation or config change triggers a rollout that briefly takes the only pod down. This is deliberate for a stateful, low-traffic internal app, but do not raise the replica count without first confirming Dolibarr tolerates concurrent writes to the shareddocumentsvolume.
The Deployment carries configmap-hash / configmap2-hash / configmap3-hash annotations (sha256 of the three ConfigMaps) so a change to config or the init scripts forces a pod roll.
5 · Vault CRDs
erp cannot start without VSO-injected credentials. Three CRDs (from the chart) wire it to Vault — see tools secrets-and-vso for the VSO runtime.
| CRD | Name | What it does |
|---|---|---|
VaultAuth |
auth |
Kubernetes auth — mount: kubernetes, role: erp, ServiceAccount erp, audience vault. Every other CRD references it via vaultAuthRef: auth. |
VaultStaticSecret |
vault-kv-app |
type: kv-v2, mount: kvv2, path: erp/config → k8s Secret secretkv, refreshAfter: 24h. Injected via envFrom secretRef. Holds DOLI_ADMIN_LOGIN, DOLI_ADMIN_PASSWORD, DOLI_INSTANCE_UNIQUE_ID. |
VaultDynamicSecret |
vso-db |
mount: postgres, path: creds/erp → k8s Secret vso-db-credentials (rotating DB user/password). rolloutRestartTargets the erp Deployment so a rotation rolls the pod. DOLI_DB_USER / DOLI_DB_PASSWORD are wired into the pod via secretKeyRef. |
Credential delivery in the Deployment:
envFrom: secretRef: secretkv— static admin config + instance UUID.env: DOLI_DB_USER/DOLI_DB_PASSWORD←secretKeyRefonvso-db-credentials(username/password).
| Sources | chart/templates/vaultauth.yaml · vaultsecret.yaml · vaultdynamicsecret.yaml |
|---|
6 · Init scripts (mounted from ConfigMaps)
Three scripts ship in chart/scripts/ and are mounted into the pod via ConfigMaps. The entrypoint runs at container start; the before-starting.d/ scripts run before Apache.
| Script | Mounted at | Role |
|---|---|---|
| custom_entrypoint.sh | /usr/local/bin/custom-entrypoint.sh (ConfigMap dolibarr-custom-entrypoint-script) |
Wraps docker-run.sh: MySQL→psql sed rewrite + Apache ServerName from DOLI_URL_ROOT (see §2) |
| update_conf_db_credentials.sh | /var/www/scripts/before-starting.d/ (ConfigMap dolibarr-before-start-scripts) |
seds the Vault-injected DOLI_DB_USER / DOLI_DB_PASSWORD into Dolibarr's conf.php at startup, so the running app uses the freshly rotated creds |
| update_ownership.sql | /var/www/scripts/before-starting.d/update_table_ownership.sql |
REASSIGN OWNED BY the current public-schema owner → erp_role. Run if you hit read-only-filesystem / permission errors after a credential change |
Caution
The ownership SQL must run after the DB role behind the dynamic creds changes. Because
postgres/creds/erpmints a new Postgres user on each rotation, freshly created tables can end up owned by a transient user. If the in-podupdate_table_ownership.sqlcannot write its temp file (Read-only file system), it is skipped and Dolibarr eventually loses query rights once Vault rotates creds. The fix is to run that SQL by hand against theerpdatabase — see Operations. The script reassigns ownership to the stableerp_rolecreated by factory postgres-iac.
7 · iac/ — Vault objects via the shared module
erp's iac/ declares only its Vault footprint; the Postgres database and erp_role themselves come from factory (postgres-iac).
| Element | Value | Source |
|---|---|---|
| Shared module | app_roles from arcodange-org/tools (hashicorp-vault/iac/modules/app_roles, ref=main), name = "erp" |
iac/main.tf |
| What the module provisions | the postgres/creds/erp dynamic role + a Kubernetes auth role erp + the kvv2 path prefix |
tools secrets-and-vso |
| Admin password | random_password.admin_initial_password (length 32) → DOLI_ADMIN_PASSWORD |
iac/main.tf |
| Instance ID | random_uuid.dolibarr_id with lifecycle { prevent_destroy = true } → DOLI_INSTANCE_UNIQUE_ID (encryption salt + module licensing) |
iac/main.tf |
| KV secret | vault_kv_secret_v2 at <kvv2 prefix>config (i.e. erp/config), data = DOLI_ADMIN_LOGIN + DOLI_ADMIN_PASSWORD + DOLI_INSTANCE_UNIQUE_ID |
iac/main.tf |
| Backend | GCS bucket arcodange-tf, prefix erp/main |
iac/backend.tf |
| Vault provider | address = https://vault.arcodange.lab, auth_login_jwt mount = gitea_jwt, role = gitea_cicd_erp, provider vault 4.4.0 |
iac/providers.tf |
The Postgres dynamic role created here GRANTs erp_role — the stable role created by factory (postgres-iac) — so every rotated DB user inherits the right schema privileges. This is the same KV secret the chart's VaultStaticSecret reads back as secretkv, closing the loop between iac/ (writes config) and chart/ (consumes it).
Warning
The OpenTofu state holds the plaintext admin password.
random_password.admin_initial_passwordis stored unencrypted in the GCS state atarcodange-tf/erp/main. Anyone with read access to that state bucket can readDOLI_ADMIN_PASSWORD. Treat theerp/mainstate prefix as a secret; do not copy it locally unprotected. Therandom_uuidinstance ID is similarly in state but is guarded byprevent_destroybecause losing it breaks decryption of stored data and invalidates purchased modules.
8 · CI — vault.yaml
| Element | Value | Source |
|---|---|---|
| Workflow | Hashicorp Vault (.gitea/workflows/vault.yaml) |
.gitea/workflows/vault.yaml |
| Triggers | workflow_dispatch, plus push / pull_request on iac/*.tf |
.gitea/workflows/vault.yaml |
| Job 1 | gitea_vault_auth — mints a Gitea OIDC JWT for Vault |
.gitea/workflows/vault.yaml |
| Job 2 | tofu — dflook/terraform-apply over iac/, auto_approve: true, OpenTofu 1.8.2 |
.gitea/workflows/vault.yaml |
| Secrets | TERRAFORM_SSH_KEY (SSH key to clone the app_roles module from tools) + HOMELAB_CA_CERT (Vault self-signed CA) + GOOGLE_BACKEND_CREDENTIALS (GCS state) |
.gitea/workflows/vault.yaml |
This tofu apply follows the lab-wide pattern documented in factory ci-apply-flow. There is no application-image build step — the chart is delivered by ArgoCD (01 · factory), and the image is upstream.
9 · <app> convention mapping
erp follows the lab's per-app naming convention — see naming-conventions. With <app> = erp:
<app> slot |
erp value |
|---|---|
| Repo | arcodange-org/erp |
| K8s namespace | erp |
| Internal host | erp.arcodange.lab |
| ServiceAccount | erp |
| Vault Kubernetes auth role | erp |
| Vault KV path | kvv2 erp/config → Secret secretkv |
| Vault dynamic DB path | postgres/creds/erp → Secret vso-db-credentials |
| Postgres database | erp |
| Postgres stable role | erp_role |
| OpenTofu state prefix | GCS arcodange-tf/erp/main |
| Gitea CI Vault role | gitea_cicd_erp |
| Document PVC | erp (50Gi Longhorn RWX) |
Cross-references
- ERP hub — the orientation map for the whole guidebook.
- Backup & recovery — protecting the 50Gi document PVC and the
erpdatabase; cluster-recovery ordering (unseal Vault before scaling erp up). - Operations — day-to-day operational tasks, including running the table-ownership SQL by hand.
- tools secrets-and-vso — the
app_rolesmodule, the VSO runtime that materialisessecretkv+vso-db-credentials, and thepgbouncer.toolspooler. - factory postgres-iac — provisions the
erpdatabase and the stableerp_rolethe dynamic creds inherit. - factory ci-apply-flow — the shared
tofu applyCI pattern erp'svault.yamlfollows. - naming-conventions — the
<app>slots filled in §9. - webapp — the archetype that does build its own image; erp differs by reusing the upstream Dolibarr image.