rubis/.claude/deploy-memory.md
ordinarthur 985e817289 docs(deploy-memory): refonte split web/api 2 services
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 03:04:22 +02:00

185 lines
7.8 KiB
Markdown

# Deploy memory — rubis
Ce repo contient **deux déploiements distincts** sur la même infra K3s,
namespace `rubis` :
1. **Landing** statique (`rubis.arthurbarre.fr`) — image nginx-alpine.
2. **App SaaS** (`app.rubis.arthurbarre.fr`) — image AdonisJS + React (V1).
Chacun a sa propre image, son propre Dockerfile, son propre workflow CI.
---
## 1. Landing (`rubis.arthurbarre.fr`)
### Infra
- Deployment : `rubis` · Container : `rubis` · NodePort : `30109`
- Image : `git.arthurbarre.fr/ordinarthur/rubis`
- Domaine : https://rubis.arthurbarre.fr (sous-domaine **temporaire**
domaine définitif `rubis-sur-l-ongle.fr` pas encore acheté)
- Manifests : `k3s/{namespace,deployment,service}.yml`
- Route Traefik : `~/dev/perso/proxmox/ansible/roles/traefik/templates/rubis.yml.j2`
- Workflow CI : `.gitea/workflows/deploy.yml`
- Source : `landing/` (servi par nginx)
### Mise à jour
Push git, le CI build + rollout auto (filter sur `landing/**`, `Dockerfile`,
`k3s/{namespace,deployment,service}.yml`).
---
## 2. App SaaS (`app.rubis.arthurbarre.fr`)
**Architecture en 2 services** : nginx en frontal (web) + API Node interne.
```
Traefik :443 → app.rubis.arthurbarre.fr:30110 → rubis-web (nginx)
├─ / → SPA static (try_files)
└─ /api/* → rubis-api (ClusterIP :3333)
```
### rubis-web — frontend (NodePort 30110, exposé)
- Image : `git.arthurbarre.fr/ordinarthur/rubis-web`
- Container : `web` (nginx-alpine + SPA Vite dist)
- Sert /assets/* (cache 1y immutable), / (SPA fallback try_files)
- Reverse-proxy /api/* → rubis-api.rubis.svc.cluster.local:3333
- Manifest : `k3s/app/web.yml`
- Workflow CI : `.gitea/workflows/deploy-web.yml`
- Source : `apps/web`, `packages/shared` + `apps/web/nginx.conf`
### rubis-api — backend (ClusterIP, interne uniquement)
- Image : `git.arthurbarre.fr/ordinarthur/rubis-api`
- Container : `api` (Node 22 + AdonisJS V7)
- Workers BullMQ dans le même process (cf. `start/queue.ts`)
- Init container `migrate` : `node ace.js migration:run --force` depuis
`/app/apps/api/build` (idempotent)
- Probes K3s sur `/api/v1/health`
- Manifest : `k3s/app/api.yml` (Deployment + Service ClusterIP + ConfigMap)
- Workflow CI : `.gitea/workflows/deploy-api.yml`
- Source : `apps/api`, `packages/shared`
### rubis-redis — backend BullMQ + cache (ClusterIP)
- Image : `redis:7.4-alpine`
- PVC 1Gi local-path, AOF on, maxmemory 256mb allkeys-lru
- Manifest : `k3s/app/redis.yml`
- Re-déployé par le workflow API (path filter inclut `redis.yml`)
### Infra commune
- Domaine : https://app.rubis.arthurbarre.fr (DNS A 5413305619)
- Route Traefik : `~/dev/perso/proxmox/ansible/roles/traefik/templates/rubis-app.yml.j2`
### Dépendances externes (déjà déployées)
- **Postgres** : 10.10.10.3:5432, base `rubis_prod`, user `rubis`
- **MinIO** : `minio.minio.svc.cluster.local:9000`, bucket
`rubis-prod-invoices`, creds = root MinIO
- **Redis** : interne au namespace `rubis`, pas de dépendance externe
### Secrets K3s (`rubis-app-secrets`)
Posés une fois manuellement via `kubectl create secret generic` (jamais
dans les manifests). Re-pose si rotation ou ajout :
```bash
kubectl --kubeconfig ~/dev/perso/proxmox/k3s/kubeconfig.yaml \
-n rubis create secret generic rubis-app-secrets \
--from-literal=APP_KEY=... \
--from-literal=PG_PASSWORD=... \
--from-literal=S3_ACCESS_KEY=... \
--from-literal=S3_SECRET_KEY=... \
--from-literal=RESEND_API_KEY=... \
--from-literal=MISTRAL_API_KEY=... \
--from-literal=REDIS_PASSWORD="" \
--dry-run=client -o yaml | kubectl apply -f -
```
### Mise à jour
Push git → un (ou les deux) workflow(s) CI se déclenchent selon les paths
modifiés. Build+rollout indépendants.
| Path modifié | Workflow déclenché |
|---|---|
| `apps/web/**`, `Dockerfile.web`, `k3s/app/web.yml` | deploy-web.yml |
| `apps/api/**`, `Dockerfile.api`, `k3s/app/api.yml`, `k3s/app/redis.yml` | deploy-api.yml |
| `packages/shared/**`, `pnpm-lock.yaml`, `tsconfig.base.json`, … | les deux |
### Particularités du Dockerfile.api
- `node ace build` plante avec `ERR_UNKNOWN_FILE_EXTENSION` car
`@poppinss/ts-exec` ne s'enregistre pas à temps avant l'import de
`bin/console.ts`. **Solution** : `pnpm exec tsx ace.js build
--ignore-ts-errors` (tsx = esbuild, gère .ts nativement).
- `--ignore-ts-errors` car `tests/bootstrap.ts` référence un type généré
tardivement (`.adonisjs/client/registry/schema.d.ts`). Le typecheck
strict est exécuté en CI séparée (`pnpm typecheck`).
### Particularités du Dockerfile.web
- Vite build appelé directement (`pnpm exec vite build`) au lieu du
script `tsc -b && vite build` qui plante sans cache `.tsbuildinfo` à
cause de @tanstack/router-core.
- nginx.conf inclut le upstream `rubis-api.rubis.svc.cluster.local:3333`.
Si on renomme le service API, c'est ici qu'il faut mettre à jour
(en plus du manifest).
---
## Voie manuelle (debug, hors CI)
```bash
TAG=$(git rev-parse --short HEAD)
# Landing (rubis.arthurbarre.fr)
docker build --platform linux/amd64 -t git.arthurbarre.fr/ordinarthur/rubis:$TAG .
docker push git.arthurbarre.fr/ordinarthur/rubis:$TAG
kubectl --kubeconfig ~/dev/perso/proxmox/k3s/kubeconfig.yaml \
-n rubis set image deploy/rubis rubis=git.arthurbarre.fr/ordinarthur/rubis:$TAG
# App — web (frontend)
docker build --platform linux/amd64 -f Dockerfile.web \
-t git.arthurbarre.fr/ordinarthur/rubis-web:$TAG .
docker push git.arthurbarre.fr/ordinarthur/rubis-web:$TAG
kubectl --kubeconfig ~/dev/perso/proxmox/k3s/kubeconfig.yaml \
-n rubis set image deploy/rubis-web web=git.arthurbarre.fr/ordinarthur/rubis-web:$TAG
# App — api (backend)
docker build --platform linux/amd64 -f Dockerfile.api \
-t git.arthurbarre.fr/ordinarthur/rubis-api:$TAG .
docker push git.arthurbarre.fr/ordinarthur/rubis-api:$TAG
kubectl --kubeconfig ~/dev/perso/proxmox/k3s/kubeconfig.yaml \
-n rubis set image deploy/rubis-api api=git.arthurbarre.fr/ordinarthur/rubis-api:$TAG \
&& kubectl --kubeconfig ~/dev/perso/proxmox/k3s/kubeconfig.yaml \
-n rubis patch deploy/rubis-api --type=json \
-p="[{\"op\":\"replace\",\"path\":\"/spec/template/spec/initContainers/0/image\",\"value\":\"git.arthurbarre.fr/ordinarthur/rubis-api:$TAG\"}]"
```
⚠️ Cross-compile ARM Mac → linux/amd64 plante sur `@swc/core` au build de
l'app. Préférer pousser et laisser le CI Gitea (linux/amd64 natif).
Si changement route/domaine : `cd ~/dev/perso/proxmox/ansible && ansible-playbook playbooks/gateway.yml`. Penser à `sudo systemctl restart traefik`
sur la gateway si un nouveau domaine ne récupère pas son cert ACME tout
seul (le hot-reload de la dynamic config ne déclenche pas toujours le
challenge LE).
---
## Quand le domaine définitif sera acheté (`rubis-sur-l-ongle.fr`)
1. Records DNS chez OVH : A `@`, A `app`, MX/SPF/DKIM pour Resend
2. Modifier les rules dans `rubis.yml.j2` et `rubis-app.yml.j2` (Host)
3. Replay `gateway.yml`
4. Maj `MAIL_FROM_ADDRESS=relances@rubis-sur-l-ongle.fr` dans le secret
5. (Optionnel) supprimer les A records `rubis.arthurbarre.fr` et
`app.rubis.arthurbarre.fr` chez OVH
---
## Déjà fait — NE PAS refaire
- Dockerfile (landing) + Dockerfile.web + Dockerfile.api (app split)
- nginx.conf (apps/web/nginx.conf) avec upstream rubis-api
- Manifests `k3s/` (landing) + `k3s/app/{api,web,redis}.yml`
- Routes Traefik `rubis.yml.j2` + `rubis-app.yml.j2`
- DNS OVH : A records `rubis` (id 5413044152) + `app.rubis` (id 5413305619)
- Repo Gitea + secrets CI (`KUBECONFIG`, `REGISTRY_PASSWORD`)
- Namespace + secret registry K3s (`gitea-registry`)
- Postgres : base `rubis_prod` + user `rubis` (10.10.10.3)
- MinIO : bucket `rubis-prod-invoices`
- Secret K3s `rubis-app-secrets` (APP_KEY, DB pwd, MinIO, Resend, Mistral)
- ConfigMap `rubis-api-config` (env non-sensibles)
Les prochains `/deploy` font uniquement build + rollout via push git.