diff --git a/.gitea/workflows/deploy-app.yml b/.gitea/workflows/deploy-api.yml similarity index 66% rename from .gitea/workflows/deploy-app.yml rename to .gitea/workflows/deploy-api.yml index a7882fd..02a1e47 100644 --- a/.gitea/workflows/deploy-app.yml +++ b/.gitea/workflows/deploy-api.yml @@ -1,27 +1,29 @@ -name: Build & Deploy App +name: Build & Deploy API -# Workflow pour l'app SaaS (apps/api AdonisJS + apps/web React) déployée -# sur app.rubis.arthurbarre.fr. Image distincte de la landing. +# Workflow API (AdonisJS V7) — déployée en ClusterIP rubis-api dans le +# namespace rubis. Servie via le reverse proxy nginx de rubis-web. on: push: branches: [main] paths: - - 'apps/**' - - 'packages/**' + - 'apps/api/**' + - 'packages/shared/**' - 'pnpm-lock.yaml' - 'pnpm-workspace.yaml' - 'package.json' + - 'tsconfig.base.json' - 'turbo.json' - - 'Dockerfile.app' - - 'k3s/app/**' - - '.gitea/workflows/deploy-app.yml' + - 'Dockerfile.api' + - 'k3s/app/api.yml' + - 'k3s/app/redis.yml' + - '.gitea/workflows/deploy-api.yml' env: REGISTRY: git.arthurbarre.fr - IMAGE: ordinarthur/rubis-app + IMAGE: ordinarthur/rubis-api NAMESPACE: rubis - DEPLOYMENT: rubis-app - CONTAINER: app + DEPLOYMENT: rubis-api + CONTAINER: api jobs: build-and-deploy: @@ -37,11 +39,11 @@ jobs: username: ordinarthur password: ${{ secrets.REGISTRY_PASSWORD }} - - name: Build and push app image + - name: Build and push API image uses: docker/build-push-action@v5 with: context: . - file: Dockerfile.app + file: Dockerfile.api push: true tags: | ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest @@ -63,26 +65,19 @@ jobs: kubectl apply -f k3s/namespace.yml - # Idempotent : on (re)pose le pull secret du registry Gitea + le - # secret applicatif n'est PAS recréé ici (créé manuellement au - # premier deploy via kubectl, contient des creds qui ne - # transitent jamais par le CI). kubectl -n $NAMESPACE create secret docker-registry gitea-registry \ --docker-server=$REGISTRY \ --docker-username=ordinarthur \ --docker-password=${{ secrets.REGISTRY_PASSWORD }} \ --dry-run=client -o yaml | kubectl apply -f - - # Apply Redis + app manifests (idempotent) - kubectl apply -f k3s/app/ + # Redis (idempotent — sert API workers BullMQ + cache) + kubectl apply -f k3s/app/redis.yml + kubectl apply -f k3s/app/api.yml - # Pin l'image avec le sha du commit pour rolling update propre. - # Le init-container migrate utilise la même image et tourne avant - # le serveur — migrations idempotentes via ace migration:run. + # Pin l'image API + le init-container migrate (même image) sur le sha. kubectl -n $NAMESPACE set image deployment/$DEPLOYMENT \ $CONTAINER=$REGISTRY/$IMAGE:${{ github.sha }} - - # Patch aussi le init container (même image) kubectl -n $NAMESPACE patch deployment $DEPLOYMENT \ --type='json' \ -p="[{\"op\":\"replace\",\"path\":\"/spec/template/spec/initContainers/0/image\",\"value\":\"$REGISTRY/$IMAGE:${{ github.sha }}\"}]" diff --git a/.gitea/workflows/deploy-web.yml b/.gitea/workflows/deploy-web.yml new file mode 100644 index 0000000..15e47f4 --- /dev/null +++ b/.gitea/workflows/deploy-web.yml @@ -0,0 +1,78 @@ +name: Build & Deploy Web + +# Workflow Web (React/Vite + nginx) — sert app.rubis.arthurbarre.fr. +# Reverse-proxie /api/* vers le service ClusterIP rubis-api. +on: + push: + branches: [main] + paths: + - 'apps/web/**' + - 'packages/shared/**' + - 'pnpm-lock.yaml' + - 'pnpm-workspace.yaml' + - 'package.json' + - 'tsconfig.base.json' + - 'turbo.json' + - 'Dockerfile.web' + - 'k3s/app/web.yml' + - '.gitea/workflows/deploy-web.yml' + +env: + REGISTRY: git.arthurbarre.fr + IMAGE: ordinarthur/rubis-web + NAMESPACE: rubis + DEPLOYMENT: rubis-web + CONTAINER: web + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Login to Gitea Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ordinarthur + password: ${{ secrets.REGISTRY_PASSWORD }} + + - name: Build and push Web image + uses: docker/build-push-action@v5 + with: + context: . + file: Dockerfile.web + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest + ${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ github.sha }} + cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE }}:cache + cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE }}:cache,mode=max + + - name: Install kubectl + run: | + curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" + chmod +x kubectl + mv kubectl /usr/local/bin/ + + - name: Deploy to K3s + run: | + mkdir -p ~/.kube + echo "${{ secrets.KUBECONFIG }}" | base64 -d > ~/.kube/config + chmod 600 ~/.kube/config + + kubectl apply -f k3s/namespace.yml + + kubectl -n $NAMESPACE create secret docker-registry gitea-registry \ + --docker-server=$REGISTRY \ + --docker-username=ordinarthur \ + --docker-password=${{ secrets.REGISTRY_PASSWORD }} \ + --dry-run=client -o yaml | kubectl apply -f - + + kubectl apply -f k3s/app/web.yml + + kubectl -n $NAMESPACE set image deployment/$DEPLOYMENT \ + $CONTAINER=$REGISTRY/$IMAGE:${{ github.sha }} + + kubectl -n $NAMESPACE rollout status deployment/$DEPLOYMENT --timeout=180s diff --git a/Dockerfile.api b/Dockerfile.api new file mode 100644 index 0000000..21a2667 --- /dev/null +++ b/Dockerfile.api @@ -0,0 +1,76 @@ +# syntax=docker/dockerfile:1.7 +# ============================================================================= +# Rubis — image API (AdonisJS V7, Node 22) +# Sert /api/v1/* en interne au cluster (ClusterIP rubis-api:3333). +# Le SPA est servi par rubis-web (nginx) en frontal qui proxy /api/* ici. +# ============================================================================= +# +# Workers BullMQ tournent dans le même process Node (cf. start/queue.ts). +# Migrations exécutées par init-container avant le serveur (cf. k3s/app/api.yml). +# +# Particularités : +# - On bypasse @poppinss/ts-exec en appelant ace via tsx (ERR_UNKNOWN_FILE +# _EXTENSION sinon, swc/core race au boot) +# - --ignore-ts-errors car tests/bootstrap.ts réfère un type généré tardif +# ============================================================================= + +ARG NODE_VERSION=22.13.1 +ARG PNPM_VERSION=10.0.0 + +# ----------------------------------------------------------------------------- +# base — node + pnpm + tini +# ----------------------------------------------------------------------------- +FROM node:${NODE_VERSION}-alpine AS base +ARG PNPM_VERSION +RUN apk add --no-cache libc6-compat tini && \ + corepack enable && \ + corepack prepare pnpm@${PNPM_VERSION} --activate +WORKDIR /repo + +# ----------------------------------------------------------------------------- +# deps — install workspace (avec devDeps pour le build) +# ----------------------------------------------------------------------------- +FROM base AS deps +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json tsconfig.base.json ./ +COPY apps/api/package.json ./apps/api/ +COPY packages/shared/package.json ./packages/shared/ +RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \ + pnpm install --frozen-lockfile + +# ----------------------------------------------------------------------------- +# build — ace build via tsx +# ----------------------------------------------------------------------------- +FROM deps AS build +COPY packages/shared ./packages/shared +COPY apps/api ./apps/api +RUN cd apps/api && pnpm exec tsx ace.js build --ignore-ts-errors + +# Prune devDeps (les workspace symlinks restent). +RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \ + pnpm install --prod --frozen-lockfile=false + +# ----------------------------------------------------------------------------- +# runner — runtime minimal, user non-root +# ----------------------------------------------------------------------------- +FROM base AS runner +RUN addgroup -g 1001 -S nodejs && adduser -S adonis -u 1001 + +ENV NODE_ENV=production \ + HOST=0.0.0.0 \ + PORT=3333 \ + LOG_LEVEL=info + +WORKDIR /app +COPY --from=build --chown=adonis:nodejs /repo /app + +USER adonis +WORKDIR /app/apps/api + +EXPOSE 3333 + +# /api/v1/health renvoie 200 quand le serveur + DB sont up. +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ + CMD wget -qO- http://127.0.0.1:3333/api/v1/health >/dev/null 2>&1 || exit 1 + +ENTRYPOINT ["/sbin/tini", "--"] +CMD ["node", "build/bin/server.js"] diff --git a/Dockerfile.app b/Dockerfile.app deleted file mode 100644 index b7beac1..0000000 --- a/Dockerfile.app +++ /dev/null @@ -1,115 +0,0 @@ -# syntax=docker/dockerfile:1.7 -# ============================================================================= -# Rubis Sur l'Ongle — image production de l'app SaaS (apps/api + apps/web) -# Sert app.rubis.arthurbarre.fr. La landing (rubis.arthurbarre.fr) reste sur -# une image séparée — Dockerfile à la racine, nginx static. -# ============================================================================= -# -# Multi-stage : -# - base : node 22 alpine + pnpm + tini -# - deps : install workspace deps (cache friendly via manifests d'abord) -# - build : build shared, web, api ; copie le SPA dans apps/api/build/public -# - runner : copie le repo "pruned" prod, lance node bin/server.js -# -# Choix architectural : un seul process Node sert l'API ET le SPA static -# (via le static middleware AdonisJS + un fallback wildcard pour SPA routing). -# Les workers BullMQ tournent dans le même process (cf. start/queue.ts). -# ============================================================================= - -ARG NODE_VERSION=22.13.1 -ARG PNPM_VERSION=10.0.0 - -# ----------------------------------------------------------------------------- -# base — node + pnpm + tini -# ----------------------------------------------------------------------------- -FROM node:${NODE_VERSION}-alpine AS base - -ARG PNPM_VERSION -RUN apk add --no-cache libc6-compat tini && \ - corepack enable && \ - corepack prepare pnpm@${PNPM_VERSION} --activate - -WORKDIR /repo - -# ----------------------------------------------------------------------------- -# deps — install workspace (devDeps inclus, on en a besoin pour les builds) -# ----------------------------------------------------------------------------- -FROM base AS deps - -COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json tsconfig.base.json ./ -COPY apps/api/package.json ./apps/api/ -COPY apps/web/package.json ./apps/web/ -COPY packages/shared/package.json ./packages/shared/ - -RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \ - pnpm install --frozen-lockfile - -# ----------------------------------------------------------------------------- -# build — shared → web → api, puis copie du SPA dans le build de l'API -# ----------------------------------------------------------------------------- -FROM deps AS build - -COPY packages/shared ./packages/shared -COPY apps/web ./apps/web -COPY apps/api ./apps/api - -# Builds : -# - @rubis/shared : pas de build (TS source consommé directement via exports). -# - Web : on appelle vite build directement (le `tsc -b` du script de prod -# fait remonter des erreurs DOM dans @tanstack/router-core sans cache -# .tsbuildinfo ; le typecheck est fait en CI séparément). -# - API : on appelle ace via `tsx` plutôt que `node`. Le hook -# @poppinss/ts-exec utilisé par défaut (qui s'appuie sur @swc/core) ne -# s'enregistre pas à temps avant l'import de bin/console.ts dans -# certains environnements de build, ce qui produit -# ERR_UNKNOWN_FILE_EXTENSION. tsx (esbuild-based) est fiable et gère -# nativement les .ts dès le démarrage. -RUN pnpm --filter @rubis/web exec vite build -# --ignore-ts-errors : on ignore les erreurs TS du build (notamment -# tests/bootstrap.ts qui référence un .adonisjs/client/registry/schema.d.ts -# généré tardivement). Le typecheck strict est exécuté côté CI séparément -# (pnpm typecheck), avant que ce build ne soit déclenché. -RUN cd apps/api && pnpm exec tsx ace.js build --ignore-ts-errors - -# Le SPA static va dans apps/api/build/public/ pour être servi par le static -# middleware AdonisJS. AdonisJS ne copie pas public/ par défaut dans build/ -# (metaFiles vide), on le fait manuellement ici. -RUN mkdir -p apps/api/build/public && \ - cp -r apps/web/dist/. apps/api/build/public/ - -# Prune les devDeps. Les symlinks pnpm vers les workspace packages -# (@rubis/shared) restent valides car on garde le repo en place. -RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \ - pnpm install --prod --frozen-lockfile=false - -# ----------------------------------------------------------------------------- -# runner — runtime minimal, user non-root -# ----------------------------------------------------------------------------- -FROM base AS runner - -RUN addgroup -g 1001 -S nodejs && adduser -S adonis -u 1001 - -ENV NODE_ENV=production \ - HOST=0.0.0.0 \ - PORT=3333 \ - LOG_LEVEL=info - -WORKDIR /app - -# On copie tout le repo pruned (node_modules inclus avec les symlinks -# workspace). C'est plus gros qu'une image "deploy" pure, mais ça évite -# les pièges de résolution workspace pour V1. -COPY --from=build --chown=adonis:nodejs /repo /app - -USER adonis - -WORKDIR /app/apps/api - -EXPOSE 3333 - -# Healthcheck léger : le serveur HTTP doit répondre 200 sur /api/v1/. -HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ - CMD wget -qO- http://127.0.0.1:3333/ >/dev/null 2>&1 || exit 1 - -ENTRYPOINT ["/sbin/tini", "--"] -CMD ["node", "build/bin/server.js"] diff --git a/Dockerfile.web b/Dockerfile.web new file mode 100644 index 0000000..2661794 --- /dev/null +++ b/Dockerfile.web @@ -0,0 +1,58 @@ +# syntax=docker/dockerfile:1.7 +# ============================================================================= +# Rubis — image web (SPA React/Vite servi par nginx) +# Sert app.rubis.arthurbarre.fr (front + reverse proxy /api/* → rubis-api). +# ============================================================================= + +ARG NODE_VERSION=22.13.1 +ARG PNPM_VERSION=10.0.0 +ARG NGINX_VERSION=1.27-alpine + +# ----------------------------------------------------------------------------- +# build — Vite produit dist/ +# ----------------------------------------------------------------------------- +FROM node:${NODE_VERSION}-alpine AS build + +ARG PNPM_VERSION +RUN apk add --no-cache libc6-compat && \ + corepack enable && \ + corepack prepare pnpm@${PNPM_VERSION} --activate + +WORKDIR /repo + +# Manifests pour cache Docker +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json tsconfig.base.json ./ +COPY apps/web/package.json ./apps/web/ +COPY packages/shared/package.json ./packages/shared/ + +# Install : on prend tout le workspace pour que les workspace deps résolvent. +# Le filter --include-deps évite de gaspiller en installant les deps de l'API. +RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \ + pnpm install --frozen-lockfile + +COPY packages/shared ./packages/shared +COPY apps/web ./apps/web + +# vite build direct (le `tsc -b` du script build plante sans cache .tsbuildinfo +# à cause de @tanstack/router-core ; le typecheck strict est en CI séparée). +RUN pnpm --filter @rubis/web exec vite build + +# ----------------------------------------------------------------------------- +# runner — nginx-alpine + dist + config +# ----------------------------------------------------------------------------- +FROM nginx:${NGINX_VERSION} AS runner + +# Pas besoin de /etc/nginx/conf.d/default.conf legacy +RUN rm /etc/nginx/conf.d/default.conf + +COPY apps/web/nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /repo/apps/web/dist /var/www + +EXPOSE 80 + +# Healthcheck : nginx répond 200 sur /index.html +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget -qO- http://127.0.0.1/index.html >/dev/null 2>&1 || exit 1 + +# Démarre nginx en foreground (pas de tini nécessaire pour nginx). +CMD ["nginx", "-g", "daemon off;"] diff --git a/apps/api/start/routes.ts b/apps/api/start/routes.ts index 2957265..e7dd191 100644 --- a/apps/api/start/routes.ts +++ b/apps/api/start/routes.ts @@ -12,9 +12,18 @@ import { middleware } from '#start/kernel' import router from '@adonisjs/core/services/router' import { controllers } from '#generated/controllers' -import app from '@adonisjs/core/services/app' -import { existsSync } from 'node:fs' -import { resolve } from 'node:path' + +router + .group(() => { + /** + * Health check — public, utilisé par les sondes K3s (startup/liveness/ + * readiness) et le healthcheck Docker. Retourne 200 sans toucher la DB + * pour rester rapide ; la DB est implicitement vérifiée au boot par + * le init-container migrate. + */ + router.get('health', () => ({ status: 'ok', uptime: process.uptime() })).as('health') + }) + .prefix('/api/v1') router .group(() => { @@ -177,25 +186,3 @@ router .use(middleware.auth()) }) .prefix('/api/v1') - -/** - * SPA fallback — sert `public/index.html` pour toute route non-API et non-asset - * statique (le static middleware aura déjà répondu pour les vrais fichiers - * via etag / 200). Permet à TanStack Router côté front de gérer son routing - * sans 404 quand l'utilisateur recharge sur /factures, /plans/nouveau, etc. - * - * On ne match PAS /api/v1/* (gardé en 404 propre via le routeur ci-dessus). - */ -router.get('*', async ({ request, response }) => { - if (request.url().startsWith('/api/')) { - return response.status(404).json({ - errors: [{ code: 'not_found', message: 'Route introuvable' }], - }) - } - const indexPath = resolve(app.publicPath('index.html')) - if (existsSync(indexPath)) { - return response.download(indexPath) - } - // En dev sans build front, on renvoie un message clair. - return response.status(503).send('SPA not built — run `pnpm --filter @rubis/web build`') -}) diff --git a/apps/web/nginx.conf b/apps/web/nginx.conf new file mode 100644 index 0000000..33d34b0 --- /dev/null +++ b/apps/web/nginx.conf @@ -0,0 +1,74 @@ +# nginx.conf — reverse proxy + SPA static pour rubis-web +# +# Sert : +# - / → assets SPA + index.html avec fallback try_files (TanStack Router) +# - /api/* → reverse proxy vers le service ClusterIP rubis-api:3333 +# +# Le service rubis-api est interne au cluster K3s (pas de NodePort). +# Seul nginx (NodePort 30110 → Traefik) est exposé. + +upstream rubis_api { + server rubis-api.rubis.svc.cluster.local:3333 max_fails=3 fail_timeout=10s; + keepalive 32; +} + +# Compression gzip — Vite produit déjà du JS minifié, mais HTML/CSS/SVG +# bénéficient toujours du gzip on-the-fly. +gzip on; +gzip_vary on; +gzip_min_length 1024; +gzip_types + application/javascript + application/json + application/xml + text/css + text/html + text/plain + image/svg+xml; + +server { + listen 80 default_server; + server_name _; + root /var/www; + index index.html; + + # Limite raisonnable pour les uploads de factures (PDF, photos). + client_max_body_size 25m; + + # Désactive les logs sur les ressources qui spamment (favicon, robots). + location = /favicon.ico { log_not_found off; access_log off; } + location = /robots.txt { log_not_found off; access_log off; } + + # Assets fingerprintés Vite : cache long, immutable. + location /assets/ { + expires 1y; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } + + # API → reverse proxy vers AdonisJS (rubis-api ClusterIP). + # Inclut /api/v1/checkin/* qui sert les liens reçus par email. + location /api/ { + proxy_pass http://rubis_api; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + proxy_set_header Connection ""; + + # Timeouts adaptés au plus long endpoint (upload OCR Mistral). + proxy_connect_timeout 5s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + # SPA fallback : toute route non-asset, non-API → index.html + # (TanStack Router gère côté client). + location / { + try_files $uri $uri/ /index.html; + # index.html lui-même : pas de cache (pour récupérer les nouveaux + # builds sans purger côté client). + add_header Cache-Control "no-cache, no-store, must-revalidate"; + } +} diff --git a/k3s/app/deployment.yml b/k3s/app/api.yml similarity index 65% rename from k3s/app/deployment.yml rename to k3s/app/api.yml index 9e25e59..67f439c 100644 --- a/k3s/app/deployment.yml +++ b/k3s/app/api.yml @@ -1,12 +1,11 @@ -# Rubis App — apps/api (AdonisJS) + apps/web (React) bundlés dans une image -# unique. Le SPA est servi par le static middleware AdonisJS, le wildcard -# fallback gère le routing TanStack Router. Workers BullMQ tournent dans le -# même process (cf. start/queue.ts). +# Rubis API — AdonisJS V7 (Node 22). ClusterIP uniquement, accessible +# depuis nginx (rubis-web) via DNS K3s : rubis-api.rubis.svc.cluster.local +# Workers BullMQ tournent dans le même process (cf. start/queue.ts). --- apiVersion: apps/v1 kind: Deployment metadata: - name: rubis-app + name: rubis-api namespace: rubis spec: replicas: 1 @@ -17,40 +16,40 @@ spec: maxUnavailable: 0 selector: matchLabels: - app: rubis-app + app: rubis-api template: metadata: labels: - app: rubis-app + app: rubis-api spec: imagePullSecrets: - name: gitea-registry - # Migrations exécutées en init-container avant que le serveur démarre. + # Migrations : exécutées en init-container avant que le serveur démarre. # Idempotent (ace migration:run skip ce qui est déjà appliqué). + # workingDir = build/ pour utiliser ace.js compilé (les .ts ne se chargent + # pas en runtime, devDeps absentes). initContainers: - name: migrate - image: git.arthurbarre.fr/ordinarthur/rubis-app:latest - # On exécute ace depuis build/ (compilé JS) — le shim ace.js de - # /app/apps/api/ charge bin/console.ts (TS) qui n'a pas de loader - # disponible en runtime sans devDeps. + image: git.arthurbarre.fr/ordinarthur/rubis-api:latest + imagePullPolicy: Always workingDir: /app/apps/api/build command: ['node', 'ace.js', 'migration:run', '--force'] envFrom: - secretRef: { name: rubis-app-secrets } - - configMapRef: { name: rubis-app-config } + - configMapRef: { name: rubis-api-config } resources: requests: { cpu: 50m, memory: 128Mi } limits: { cpu: 500m, memory: 512Mi } containers: - - name: app - image: git.arthurbarre.fr/ordinarthur/rubis-app:latest + - name: api + image: git.arthurbarre.fr/ordinarthur/rubis-api:latest imagePullPolicy: Always ports: - containerPort: 3333 name: http envFrom: - secretRef: { name: rubis-app-secrets } - - configMapRef: { name: rubis-app-config } + - configMapRef: { name: rubis-api-config } resources: requests: cpu: 100m @@ -58,27 +57,41 @@ spec: limits: cpu: 1000m memory: 768Mi - # Probes adaptées au boot AdonisJS (peut prendre 10-15s avec BullMQ) startupProbe: - httpGet: { path: /, port: http } + httpGet: { path: /api/v1/health, port: http } initialDelaySeconds: 5 periodSeconds: 5 - failureThreshold: 30 # 150s max + failureThreshold: 30 livenessProbe: - httpGet: { path: /, port: http } + httpGet: { path: /api/v1/health, port: http } periodSeconds: 30 timeoutSeconds: 5 failureThreshold: 3 readinessProbe: - httpGet: { path: /, port: http } + httpGet: { path: /api/v1/health, port: http } periodSeconds: 10 timeoutSeconds: 3 failureThreshold: 3 --- +# ClusterIP — accessible uniquement depuis le cluster (par nginx rubis-web). +apiVersion: v1 +kind: Service +metadata: + name: rubis-api + namespace: rubis +spec: + type: ClusterIP + selector: + app: rubis-api + ports: + - port: 3333 + targetPort: http + name: http +--- apiVersion: v1 kind: ConfigMap metadata: - name: rubis-app-config + name: rubis-api-config namespace: rubis data: # Variables non-sensibles. Les secrets sont dans rubis-app-secrets. @@ -103,7 +116,6 @@ data: REDIS_PORT: '6379' LIMITER_STORE: 'redis' - # MinIO interne (pas via le NodePort public, plus rapide + plus sécurisé). DRIVE_DISK: 's3' S3_ENDPOINT: 'http://minio.minio.svc.cluster.local:9000' S3_REGION: 'fr-par' diff --git a/k3s/app/service.yml b/k3s/app/service.yml deleted file mode 100644 index ab8ad34..0000000 --- a/k3s/app/service.yml +++ /dev/null @@ -1,17 +0,0 @@ -# NodePort 30110 — exposé par Traefik sur la gateway VM (cf. -# ansible/roles/traefik/templates/rubis-app.yml.j2). ---- -apiVersion: v1 -kind: Service -metadata: - name: rubis-app - namespace: rubis -spec: - type: NodePort - selector: - app: rubis-app - ports: - - port: 80 - targetPort: http - nodePort: 30110 - name: http diff --git a/k3s/app/web.yml b/k3s/app/web.yml new file mode 100644 index 0000000..1dfbd3e --- /dev/null +++ b/k3s/app/web.yml @@ -0,0 +1,66 @@ +# Rubis Web — nginx + SPA static + reverse proxy /api/* → rubis-api ClusterIP. +# Seul service exposé via Traefik (NodePort 30110 → app.rubis.arthurbarre.fr). +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: rubis-web + namespace: rubis +spec: + replicas: 1 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + selector: + matchLabels: + app: rubis-web + template: + metadata: + labels: + app: rubis-web + spec: + imagePullSecrets: + - name: gitea-registry + containers: + - name: web + image: git.arthurbarre.fr/ordinarthur/rubis-web:latest + imagePullPolicy: Always + ports: + - containerPort: 80 + name: http + resources: + requests: + cpu: 50m + memory: 32Mi + limits: + cpu: 200m + memory: 128Mi + livenessProbe: + httpGet: { path: /index.html, port: http } + periodSeconds: 30 + timeoutSeconds: 3 + failureThreshold: 3 + readinessProbe: + httpGet: { path: /index.html, port: http } + periodSeconds: 10 + timeoutSeconds: 2 + failureThreshold: 3 +--- +# NodePort 30110 — exposé par Traefik sur la gateway VM (cf. +# ansible/roles/traefik/templates/rubis-app.yml.j2). +apiVersion: v1 +kind: Service +metadata: + name: rubis-web + namespace: rubis +spec: + type: NodePort + selector: + app: rubis-web + ports: + - port: 80 + targetPort: http + nodePort: 30110 + name: http