Files
factory/vibe/guidebooks/erp/deployment.md
Gabriel Radureau 7bf83e75ed docs(vibe): add erp/ guidebook (Dolibarr deployment + backup/recovery + ops)
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>
2026-06-23 22:12:11 +02:00

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-push workflow 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:

  1. MySQL → psql rewrite. When DOLI_DB_TYPE == "pgsql", the entrypoint seds the upstream /usr/local/bin/docker-run.sh in place, replacing its mysql -u ... < ${file} SQL invocation with PGPASSWORD=... psql -U ... -h ... -p ... -d ... < ${file}.
  2. Apache ServerName. It strips the scheme from DOLI_URL_ROOT and sets the Apache ServerName in 000-default.conf (and appends to apache2.conf) so the vhost matches erp.arcodange.lab.
  3. It then execs the original docker-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 sed against an upstream file. If a future Dolibarr image changes the exact mysql ... < ${file} line in docker-run.sh, the substitution silently stops matching and SQL imports fall back to mysql (which is absent) — startup SQL then fails. Re-verify the entrypoint pattern whenever the appVersion is 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/documents contains the only copy of uploaded invoices, attachments, and generated PDFs — these are real accounting records, not regenerable cache. The helm.sh/resource-policy: keep annotation 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 80targetPort: 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@fileinternal only, no public .fr host chart/values.yaml
revisionHistoryLimit 5 chart/templates/deployment.yaml

Warning

Single replica on an RWX PVC. replicaCount: 1 with 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 shared documents volume.

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_PASSWORDsecretKeyRef on vso-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/erp mints a new Postgres user on each rotation, freshly created tables can end up owned by a transient user. If the in-pod update_table_ownership.sql cannot 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 the erp database — see Operations. The script reassigns ownership to the stable erp_role created 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_password is stored unencrypted in the GCS state at arcodange-tf/erp/main. Anyone with read access to that state bucket can read DOLI_ADMIN_PASSWORD. Treat the erp/main state prefix as a secret; do not copy it locally unprotected. The random_uuid instance ID is similarly in state but is guarded by prevent_destroy because 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 tofudflook/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 erp database; 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_roles module, the VSO runtime that materialises secretkv + vso-db-credentials, and the pgbouncer.tools pooler.
  • factory postgres-iac — provisions the erp database and the stable erp_role the dynamic creds inherit.
  • factory ci-apply-flow — the shared tofu apply CI pattern erp's vault.yaml follows.
  • 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.