# OpenPanel `prod.docker-compose.yaml` — right-sizing low-volume

**Date:** 2026-06-08
**Cible:** `openpanel/prod.docker-compose.yaml`
**Plateforme:** Dokploy (docker compose v2 standalone, pas Swarm)
**Host:** VPS Ubuntu 24.04, 16,7 Go RAM / 8 cœurs, **partagé** (~7,3 Go déjà utilisés par d'autres apps), disque 241 Go.

## Contexte & problème

4 sites trackés, volume réel observé sur 24 h : Acieb 17 + ADVMFR 21 + Abc Guepes 28 + Elevaseo 4 = **~70 events/jour** (< 0,001 event/s). Croissance attendue : stable, **pics ponctuels** (campagnes/lancements).

Le compose actuel applique le tuning de la doc *high-volume* (milliers d'events/s, dizaines de workers) à ce déploiement minuscule. Diagnostic vérifié contre le **code source** (`Openpanel-dev/openpanel`) et la doc officielle — pas d'extrapolation.

### Constats vérifiés (source / doc)

| Réglage actuel | Verdict | Preuve |
| --- | --- | --- |
| `SESSION_JOB_CONCURRENCY: 50` | **Variable fantôme** — ignorée | 0 match dans le repo. Worker `sessions` lit `SESSIONS_CONCURRENCY` (défaut **1**) via `getConcurrencyFor('sessions')` — `apps/worker/src/boot-workers.ts:194` + helper l.92-97 |
| `PROFILE_JOB_CONCURRENCY: 20` | **Variable fantôme** — ignorée, **aucun worker `profile`** | 0 match ; absent de la liste des workers de `boot-workers.ts` (profils gérés par buffer, pas par queue) |
| `EVENT_JOB_CONCURRENCY: 50` | Réelle mais 100× surdimensionnée | `boot-workers.ts:142-145` : `EVENT_JOB_CONCURRENCY || '10'`. Défaut 10 = ~10 events en parallèle, déjà très au-dessus d'un pic réaliste |
| `EVENT/SESSION/PROFILE_BUFFER_BATCH_SIZE` (10000/5000/500) | Inutile (pas un bug) | Flush piloté par **cron** ; le batch est un *plafond d'events lus par flush*, pas un seuil. À 70/j, les defaults (4000…) suffisent. Doc env-vars + high-volume |
| `op-api deploy.replicas: 2` | Actif sous compose v2 → **2 conteneurs API pour rien** | compose v2 `up` honore `deploy.replicas`. À ce volume, 1 suffit |
| `op-db command:` (`max_connections=200`, `shared_buffers=256MB`…) | Justifié par « 50 workers » inexistants | Connexions réelles ≈ 15-20 (concurrence 10 + api). Defaults Postgres (100) suffisent |
| `op-ch` **sans** limite mémoire | **Seul vrai risque** | ClickHouse dimensionne par défaut `max_server_memory_usage_to_ram_ratio=0.9` sur la RAM **host** (16 Go), pas la RAM libre. Sur host partagé → risque swap/OOM |
| `DISABLE_BULLBOARD: "false"` | Redondant | Défaut déjà `false` (doc env-vars) |
| Pas de `start_period` ni rotation logs | Hygiène | API migre au boot (risque `unhealthy` prématuré) ; logs containers non bornés |

### À conserver (déjà correct)
Redis `maxmemory-policy noeviction` + AOF `everysec` (durabilité queue BullMQ), tous les healthchecks, secrets `${...}`, réseaux `internal` + `dokploy-network`, volumes nommés.

## Approche retenue : A + B — right-size vers les defaults, avec une marge de pic explicite

Retirer tout le sur-tuning high-volume (la conf maigrit), borner ClickHouse, ajouter `start_period` + rotation logs (**A**), **mais** garder `EVENT_JOB_CONCURRENCY=20` explicite et lisible comme marge de pic documentée (**B**, pic réel ~28 events/24h, défaut source = 10). Plus les opti révélées par la lecture doc/source (cf. Redis ci-dessous). Dokploy permet de scaler les workers en 1 clic pour une vraie campagne.

Approche écartée : **C** (diff minimal — laisse une conf qui ment au prochain lecteur).

## Changements détaillés

### `op-worker`
- Supprimer `SESSION_JOB_CONCURRENCY`, `PROFILE_JOB_CONCURRENCY` (fantômes), les 3 `*_BUFFER_BATCH_SIZE` (defaults), `DISABLE_BULLBOARD` (redondant ; BullBoard reste actif).
- **Garder `EVENT_JOB_CONCURRENCY=20`** (B) : marge de pic explicite (défaut source 10).
- `deploy.resources.limits.memory` : 1G → **512M** ; `reservations.memory` : 256M.
- Ajouter `start_period: 20s` au healthcheck.
- Résultat : le bloc `environment` du worker = `<<: *x-common` + `EVENT_JOB_CONCURRENCY: 20`.

### `op-kv` (opti doc/source ajoutée)
- Ajouter `--notify-keyspace-events Ex` à la commande Redis. L'API fait déjà `CONFIG SET notify-keyspace-events Ex` au boot (`apps/api/src/index.ts`) pour la feature « visiteurs actifs », mais c'est runtime-only (perdu au restart de Redis). Le figer dans la commande → explicite + persistant. Reste : `noeviction` + AOF inchangés.

### `op-api`
- Supprimer `deploy.replicas: 2` (défaut 1). Le commentaire sur le lock advisory multi-replica devient sans objet → le retirer.
- Ajouter `start_period: 60s` au healthcheck (migrations Prisma au boot). Garder la commande d'attente db/ch + migrate.

### `op-db`
- Supprimer le bloc `command:` custom → image stock `postgres:14-alpine` (max_connections=100).
- `deploy.resources.limits.memory` : 1G → **512M** ; `reservations.memory` : 256M. Garder healthcheck.

### `op-ch` (priorité) — ⚠️ corrigé après incident
- **PAS de `deploy.resources.limits.memory` dure.** Première tentative (limit 3G)
  → ClickHouse **OOM-kill en crash-loop (exit 137)** au redeploy. Cause : un mur
  cgroup compte aussi le **page cache** (qu'un moteur SQL remplit), et le
  `op-config.xml` du template **désactive l'observateur cgroup**
  (`cgroups_memory_usage_observer_wait_time=0`) → CH ne voit pas la limite,
  dimensionne ses caches pour la RAM host (16 Go), dépasse 3G → kill.
- **Bornage SOFT via `files/clickhouse_config/low-memory.xml`** (la vraie protection,
  déterministe, ne tue jamais le process) :
  - `max_server_memory_usage = 3221225472` (3 GiB)
  - `mark_cache_size = 536870912` (512 MiB ; défaut 5 GiB)
  - `uncompressed_cache_size = 0`
- `start_period: 30s` au healthcheck conservé.
- Host à ~9 Go libres → aucun risque d'OOM host à ce volume sans mur dur.
- **Leçon** : sur ClickHouse, ne pas mettre de limite mémoire Docker dure ; borner
  par `max_server_memory_usage` + caches. Copie pratique du XML à côté du compose
  (`openpanel/low-memory.xml`) ; fichier monté réel = `files/clickhouse_config/low-memory.xml`.

### `op-dashboard`
- Ajouter `deploy.resources.limits.memory: 768M` + `reservations.memory: 256M` (aucune limite actuellement).
- Ajouter `start_period: 30s` au healthcheck.

### Transverse
- Ajouter une ancre `x-logging` (`driver: json-file`, `max-size: 10m`, `max-file: 3`) et l'appliquer via `logging: *x-logging` à chaque service → logs bornés.

## Audit de l'env Dokploy (`variables-env.env`)

Vérifié contre le code source — 3 trouvailles, dont une sécurité.

1. **🔴 SÉCURITÉ — `ALLOW_REGISTRATION=fasle` (typo) → inscriptions OUVERTES.**
   `auth.ts:107` : `return process.env.ALLOW_REGISTRATION !== 'false'`. Tout sauf
   exactement `'false'` autorise l'inscription. `'fasle' !== 'false'` → `true` →
   `https://analytics.elevaseo.app` accepte les inscriptions publiques.
   **Fix : `false`.** À corriger AUSSI dans l'UI Dokploy (le fichier repo = miroir).
2. **⚠️ Tuning mort + conflit.** `EVENT_JOB_CONCURRENCY=10`, `EVENT_BUFFER_BATCH_SIZE`,
   `EVENT_BUFFER_CHUNK_SIZE` n'étaient référencés nulle part dans le compose → ignorés.
   Résolution : **env câblé** — le compose référence `${VAR:-default}`, l'env Dokploy
   devient la source de vérité unique. `EVENT_JOB_CONCURRENCY` fixé à **20** (marge de pic).
3. **⚠️ `MAXMIND_LICENSE_KEY` mort → géoloc inactive.** Consommé par
   `packages/geo/scripts/download.ts` mais jamais passé aux conteneurs. Câblé via
   `x-common` (`MAXMIND_LICENSE_KEY: ${MAXMIND_LICENSE_KEY:-}`) → pays/ville activés.
4. **ℹ️** `NODE_ENV` / `SELF_HOSTED` dans l'env = redondants (déjà en dur). Inoffensif.

Modèle adopté : **env Dokploy = source de vérité du tuning + secrets + géoloc** ;
compose = structure + mémoire + sécu + defaults de fallback.

## Plafond mémoire résultant
512 (db) + 640 (kv, inchangé) + 3072 (ch) + 1024 (api) + 768 (dashboard) + 512 (worker) ≈ **6,5 Go** de limites hautes, sous les ~9 Go libres du host partagé.

## Non-objectifs (YAGNI)
PGBouncer (inutile < milliers conn.), Kafka/Redpanda, sharding de queue, replicas worker, read-replica PG. Tout ça relève du high-volume.

## Vérification post-édition
- `docker compose -f openpanel/prod.docker-compose.yaml config` valide (syntaxe + interpolation).
- Relecture : plus aucune variable absente de la doc/source ; aucun réglage justifié par une charge qui n'existe pas.

## Versioning / revert
- Backup du compose original : `openpanel/optimisations/prod.docker-compose.yaml.2026-06-08-pre-rightsizing.bak`.
- Revert = recopier ce backup sur `prod.docker-compose.yaml` et réimporter dans Dokploy.

## Livraison (Dokploy)
1. Reporter le nouveau `prod.docker-compose.yaml` dans Dokploy → service **Compose** → **Advanced** (coller / réimporter). L'édition locale seule ne déploie rien (miroir partiel).
2. Poser `files/clickhouse_config/low-memory.xml` dans le `files/clickhouse_config` du déploiement réel côté serveur (pour que `max_server_memory_usage` prenne effet).
3. `docker compose pull && docker compose up -d` (ou via l'UI Dokploy).

## Notes
- Repo = **miroir partiel** : seul `prod.docker-compose.yaml` est édité ici ; `files/` et `.env` vivent côté serveur Dokploy.
- Pas de dépôt git ici → le spec n'est pas commité (rules git flow non applicables).
