feat(deploy): app.rubis.arthurbarre.fr — image, manifests K3s, route Traefik
Premier déploiement de l'app SaaS (apps/api + apps/web) — distinct de la landing déjà sur rubis.arthurbarre.fr. Architecture : - Image unique (Dockerfile.app, multi-stage) : AdonisJS sert l'API ET le SPA static via @adonisjs/static + wildcard fallback pour TanStack Router - Workers BullMQ tournent dans le même process Node (cf. start/queue.ts) - Redis 7 dans le namespace rubis (PVC local-path 1Gi) - Migrations en init-container avant le serveur (idempotent) Infra : - K3s namespace rubis (déjà existant) — ajout deploy/svc rubis-app + redis - NodePort 30110 → Traefik → app.rubis.arthurbarre.fr (TLS Let's Encrypt) - Postgres : base rubis_prod + user rubis créés sur 10.10.10.3 - MinIO : bucket rubis-prod-invoices créé via mc - Secrets K3s posés via kubectl create secret (APP_KEY généré, DB pwd généré, MinIO root creds réutilisées, Resend/Mistral keys) - DNS OVH A record app.rubis créé (id 5413305619) - CI Gitea : .gitea/workflows/deploy-app.yml séparé du workflow landing, filtres sur paths apps/**, packages/**, Dockerfile.app, k3s/app/** Code app : - Static middleware @adonisjs/static configuré - Wildcard route SPA fallback en fin de routes.ts - Fix erreurs strict TS qui bloquaient le build vite (unused vars, Client missing contactFirstName/LastName dans MSW) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
ca95dde9b3
commit
461ab9bcd9
90
.gitea/workflows/deploy-app.yml
Normal file
90
.gitea/workflows/deploy-app.yml
Normal file
@ -0,0 +1,90 @@
|
||||
name: Build & Deploy App
|
||||
|
||||
# Workflow pour l'app SaaS (apps/api AdonisJS + apps/web React) déployée
|
||||
# sur app.rubis.arthurbarre.fr. Image distincte de la landing.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'apps/**'
|
||||
- 'packages/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'pnpm-workspace.yaml'
|
||||
- 'package.json'
|
||||
- 'turbo.json'
|
||||
- 'Dockerfile.app'
|
||||
- 'k3s/app/**'
|
||||
- '.gitea/workflows/deploy-app.yml'
|
||||
|
||||
env:
|
||||
REGISTRY: git.arthurbarre.fr
|
||||
IMAGE: ordinarthur/rubis-app
|
||||
NAMESPACE: rubis
|
||||
DEPLOYMENT: rubis-app
|
||||
CONTAINER: app
|
||||
|
||||
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 app image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile.app
|
||||
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
|
||||
|
||||
# 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/
|
||||
|
||||
# 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.
|
||||
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 }}\"}]"
|
||||
|
||||
kubectl -n $NAMESPACE rollout status deployment/$DEPLOYMENT --timeout=300s
|
||||
@ -1,8 +1,17 @@
|
||||
name: Build & Deploy
|
||||
name: Build & Deploy Landing
|
||||
|
||||
# Workflow pour la landing static (rubis.arthurbarre.fr).
|
||||
# L'app SaaS (apps/api + apps/web) a son propre workflow : deploy-app.yml.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'landing/**'
|
||||
- 'Dockerfile'
|
||||
- 'k3s/namespace.yml'
|
||||
- 'k3s/deployment.yml'
|
||||
- 'k3s/service.yml'
|
||||
- '.gitea/workflows/deploy.yml'
|
||||
|
||||
env:
|
||||
REGISTRY: git.arthurbarre.fr
|
||||
|
||||
111
Dockerfile.app
Normal file
111
Dockerfile.app
Normal file
@ -0,0 +1,111 @@
|
||||
# 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 : `node ace build` (canonique AdonisJS V7) — produit apps/api/build
|
||||
# avec compiled JS, package.json runtime, et metaFiles configurés.
|
||||
#
|
||||
# Note : ce build peut planter en cross-compile ARM→amd64 (swc/core), donc
|
||||
# en local sur Mac silicon, builder pour --platform linux/arm64. Le CI
|
||||
# Gitea tourne nativement sur linux/amd64 et n'a pas le problème.
|
||||
RUN pnpm --filter @rubis/web exec vite build && \
|
||||
pnpm --filter @rubis/api build
|
||||
|
||||
# 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"]
|
||||
@ -59,6 +59,7 @@ export default defineConfig({
|
||||
() => import('@adonisjs/limiter/limiter_provider'),
|
||||
() => import('@adonisjs/mail/mail_provider'),
|
||||
() => import('@adonisjs/drive/drive_provider'),
|
||||
() => import('@adonisjs/static/static_provider')
|
||||
],
|
||||
|
||||
/*
|
||||
@ -110,7 +111,10 @@ export default defineConfig({
|
||||
| the production build.
|
||||
|
|
||||
*/
|
||||
metaFiles: [],
|
||||
metaFiles: [{
|
||||
pattern: 'public/**',
|
||||
reloadServer: false,
|
||||
}],
|
||||
|
||||
hooks: {
|
||||
init: [
|
||||
|
||||
17
apps/api/config/static.ts
Normal file
17
apps/api/config/static.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { defineConfig } from '@adonisjs/static'
|
||||
|
||||
/**
|
||||
* Configuration options to tweak the static files middleware.
|
||||
* The complete set of options are documented on the
|
||||
* official documentation website.
|
||||
*
|
||||
* https://docs.adonisjs.com/guides/static-assets
|
||||
*/
|
||||
const staticServerConfig = defineConfig({
|
||||
enabled: true,
|
||||
etag: true,
|
||||
lastModified: true,
|
||||
dotFiles: 'ignore',
|
||||
})
|
||||
|
||||
export default staticServerConfig
|
||||
@ -68,6 +68,7 @@
|
||||
"@adonisjs/mail": "^10.2.0",
|
||||
"@adonisjs/session": "^8.1.0",
|
||||
"@adonisjs/shield": "^9.0.0",
|
||||
"@adonisjs/static": "^2.0.1",
|
||||
"@aws-sdk/client-s3": "^3.1043.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.1043.0",
|
||||
"@japa/api-client": "^3.2.1",
|
||||
|
||||
@ -26,6 +26,7 @@ server.use([
|
||||
() => import('#middleware/force_json_response_middleware'),
|
||||
() => import('#middleware/container_bindings_middleware'),
|
||||
() => import('@adonisjs/cors/cors_middleware'),
|
||||
() => import('@adonisjs/static/static_middleware')
|
||||
])
|
||||
|
||||
/**
|
||||
|
||||
@ -12,10 +12,9 @@
|
||||
import { middleware } from '#start/kernel'
|
||||
import router from '@adonisjs/core/services/router'
|
||||
import { controllers } from '#generated/controllers'
|
||||
|
||||
router.get('/', () => {
|
||||
return { hello: 'world' }
|
||||
})
|
||||
import app from '@adonisjs/core/services/app'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
router
|
||||
.group(() => {
|
||||
@ -178,3 +177,25 @@ 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`')
|
||||
})
|
||||
|
||||
@ -59,18 +59,16 @@ export function CadenceCalendar({
|
||||
onAddStep,
|
||||
onAddStepAtOffset,
|
||||
onRemoveStep,
|
||||
globalTone,
|
||||
}: {
|
||||
steps: DraftStepLite[];
|
||||
selectedIndex?: number;
|
||||
onSelectStep?: (idx: number) => void;
|
||||
onUpdateStep?: (idx: number, patch: Partial<DraftStepLite>) => void;
|
||||
onAddStep: () => void;
|
||||
/** Création directe en cliquant une case vide. Reçoit l'offset depuis l'échéance. */
|
||||
/** Création directe en cliquant une case vide. Reçoit l'offset depuis
|
||||
* l'échéance. La tonalité par défaut est gérée côté parent. */
|
||||
onAddStepAtOffset?: (offsetDays: number) => void;
|
||||
onRemoveStep: (idx: number) => void;
|
||||
/** Tonalité par défaut pour une étape créée depuis le calendrier. */
|
||||
globalTone?: RelanceTone;
|
||||
}) {
|
||||
const dueDate = useMemo(() => {
|
||||
const now = new Date();
|
||||
|
||||
@ -29,7 +29,7 @@ const generateSchema = z.object({
|
||||
*/
|
||||
function mockGenerate(
|
||||
tone: z.infer<typeof generateSchema>["tone"],
|
||||
offsetDays: number,
|
||||
_offsetDays: number,
|
||||
prompt: string,
|
||||
): { subject: string; body: string } {
|
||||
const briefSuffix = prompt.trim() ? ` (${prompt.trim().slice(0, 60)}…)` : "";
|
||||
|
||||
@ -403,6 +403,8 @@ export const invoiceHandlers = [
|
||||
const created = mockDb.createClient(orgId, {
|
||||
name: fields.clientName,
|
||||
email: fields.clientEmail,
|
||||
contactFirstName: null,
|
||||
contactLastName: null,
|
||||
phone: null,
|
||||
address: null,
|
||||
siret: null,
|
||||
@ -546,6 +548,8 @@ export const invoiceHandlers = [
|
||||
const created = mockDb.createClient(orgId, {
|
||||
name: fields.clientName,
|
||||
email: fields.clientEmail!,
|
||||
contactFirstName: null,
|
||||
contactLastName: null,
|
||||
phone: null,
|
||||
address: null,
|
||||
siret: null,
|
||||
@ -569,6 +573,8 @@ export const invoiceHandlers = [
|
||||
const created = mockDb.createClient(orgId, {
|
||||
name: fields.clientName,
|
||||
email: fields.clientEmail!,
|
||||
contactFirstName: null,
|
||||
contactLastName: null,
|
||||
phone: null,
|
||||
address: null,
|
||||
siret: null,
|
||||
|
||||
@ -475,7 +475,6 @@ function StepCadence({ draft, onChange }: { draft: Draft; onChange: (d: Draft) =
|
||||
<CadenceCalendar
|
||||
steps={draft.steps}
|
||||
selectedIndex={selectedIdx}
|
||||
globalTone={draft.globalTone}
|
||||
// Toggle : re-cliquer une étape déjà sélectionnée la désélectionne
|
||||
// pour revenir à la vue d'ensemble.
|
||||
onSelectStep={(idx) =>
|
||||
|
||||
117
k3s/app/deployment.yml
Normal file
117
k3s/app/deployment.yml
Normal file
@ -0,0 +1,117 @@
|
||||
# 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).
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: rubis-app
|
||||
namespace: rubis
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxSurge: 1
|
||||
maxUnavailable: 0
|
||||
selector:
|
||||
matchLabels:
|
||||
app: rubis-app
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: rubis-app
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: gitea-registry
|
||||
# Migrations exécutées en init-container avant que le serveur démarre.
|
||||
# Idempotent (ace migration:run skip ce qui est déjà appliqué).
|
||||
initContainers:
|
||||
- name: migrate
|
||||
image: git.arthurbarre.fr/ordinarthur/rubis-app:latest
|
||||
workingDir: /app/apps/api
|
||||
command: ['node', 'ace', 'migration:run', '--force']
|
||||
envFrom:
|
||||
- secretRef: { name: rubis-app-secrets }
|
||||
- configMapRef: { name: rubis-app-config }
|
||||
resources:
|
||||
requests: { cpu: 50m, memory: 128Mi }
|
||||
limits: { cpu: 500m, memory: 512Mi }
|
||||
containers:
|
||||
- name: app
|
||||
image: git.arthurbarre.fr/ordinarthur/rubis-app:latest
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 3333
|
||||
name: http
|
||||
envFrom:
|
||||
- secretRef: { name: rubis-app-secrets }
|
||||
- configMapRef: { name: rubis-app-config }
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 768Mi
|
||||
# Probes adaptées au boot AdonisJS (peut prendre 10-15s avec BullMQ)
|
||||
startupProbe:
|
||||
httpGet: { path: /, port: http }
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
failureThreshold: 30 # 150s max
|
||||
livenessProbe:
|
||||
httpGet: { path: /, port: http }
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet: { path: /, port: http }
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: rubis-app-config
|
||||
namespace: rubis
|
||||
data:
|
||||
# Variables non-sensibles. Les secrets sont dans rubis-app-secrets.
|
||||
TZ: 'Europe/Paris'
|
||||
PORT: '3333'
|
||||
HOST: '0.0.0.0'
|
||||
NODE_ENV: 'production'
|
||||
LOG_LEVEL: 'info'
|
||||
APP_URL: 'https://app.rubis.arthurbarre.fr'
|
||||
WEB_URL: 'https://app.rubis.arthurbarre.fr'
|
||||
SESSION_DRIVER: 'cookie'
|
||||
COOKIE_SECURE: 'true'
|
||||
COOKIE_DOMAIN: 'app.rubis.arthurbarre.fr'
|
||||
|
||||
DB_CONNECTION: 'postgres'
|
||||
PG_HOST: '10.10.10.3'
|
||||
PG_PORT: '5432'
|
||||
PG_USER: 'rubis'
|
||||
PG_DB_NAME: 'rubis_prod'
|
||||
|
||||
REDIS_HOST: 'rubis-redis.rubis.svc.cluster.local'
|
||||
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'
|
||||
S3_BUCKET: 'rubis-prod-invoices'
|
||||
S3_FORCE_PATH_STYLE: 'true'
|
||||
|
||||
MAIL_DRIVER: 'resend'
|
||||
MAIL_FROM_ADDRESS: 'rubis@arthurbarre.fr'
|
||||
MAIL_FROM_NAME: "Rubis Sur l'Ongle"
|
||||
|
||||
OCR_PROVIDER: 'mistral'
|
||||
|
||||
ACCESS_TOKEN_TTL_MINUTES: '30'
|
||||
REFRESH_TOKEN_TTL_DAYS: '30'
|
||||
84
k3s/app/redis.yml
Normal file
84
k3s/app/redis.yml
Normal file
@ -0,0 +1,84 @@
|
||||
# Redis 7 — backend de BullMQ (queues `relances`, `checkins`) et du cache.
|
||||
# Single replica, suffisant V1. Persistance via local-path PVC pour ne pas
|
||||
# perdre les jobs schedulés en cas de redémarrage du pod.
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: rubis-redis-data
|
||||
namespace: rubis
|
||||
spec:
|
||||
accessModes: [ReadWriteOnce]
|
||||
storageClassName: local-path
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: rubis-redis
|
||||
namespace: rubis
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate # PVC RWO → pas de rollout simultané
|
||||
selector:
|
||||
matchLabels:
|
||||
app: rubis-redis
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: rubis-redis
|
||||
spec:
|
||||
containers:
|
||||
- name: redis
|
||||
image: redis:7.4-alpine
|
||||
args:
|
||||
- redis-server
|
||||
- --appendonly
|
||||
- 'yes'
|
||||
- --maxmemory
|
||||
- 256mb
|
||||
- --maxmemory-policy
|
||||
- allkeys-lru
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
name: redis
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 384Mi
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
livenessProbe:
|
||||
tcpSocket: { port: redis }
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 15
|
||||
readinessProbe:
|
||||
exec:
|
||||
command: [redis-cli, ping]
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: rubis-redis-data
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: rubis-redis
|
||||
namespace: rubis
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: rubis-redis
|
||||
ports:
|
||||
- port: 6379
|
||||
targetPort: redis
|
||||
name: redis
|
||||
17
k3s/app/service.yml
Normal file
17
k3s/app/service.yml
Normal file
@ -0,0 +1,17 @@
|
||||
# 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
|
||||
78
pnpm-lock.yaml
generated
78
pnpm-lock.yaml
generated
@ -62,6 +62,9 @@ importers:
|
||||
'@adonisjs/shield':
|
||||
specifier: ^9.0.0
|
||||
version: 9.0.0(adb93641c3908819f9462459e2e86e5c)
|
||||
'@adonisjs/static':
|
||||
specifier: ^2.0.1
|
||||
version: 2.0.1(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))
|
||||
'@aws-sdk/client-s3':
|
||||
specifier: ^3.1043.0
|
||||
version: 3.1043.0
|
||||
@ -638,6 +641,16 @@ packages:
|
||||
edge.js:
|
||||
optional: true
|
||||
|
||||
'@adonisjs/static@2.0.1':
|
||||
resolution: {integrity: sha512-pXSyQgha1yfyQDXPp0ywv7hJqk4JgCCkzWLkku2PW3Y6i/9GUTyzzNTCpz29loZzSF0b/EpiHXQE/sqBhW+p7w==}
|
||||
engines: {node: '>=24.0.0'}
|
||||
peerDependencies:
|
||||
'@adonisjs/assembler': ^8.0.0-next.7 || ^8.0.0
|
||||
'@adonisjs/core': ^7.0.0-next.0 || ^7.0.0
|
||||
peerDependenciesMeta:
|
||||
'@adonisjs/assembler':
|
||||
optional: true
|
||||
|
||||
'@adonisjs/tsconfig@2.0.0':
|
||||
resolution: {integrity: sha512-Uz8qvB6KR9otCh9zei2VEj7tPwdsrT7R+voYoN4tUfEijWRdTNgJ8d1CtyLKHaggCCOwZIwZLVklV/h2FDHgNw==}
|
||||
|
||||
@ -3470,6 +3483,9 @@ packages:
|
||||
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
escape-html@1.0.3:
|
||||
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
|
||||
|
||||
escape-string-regexp@1.0.5:
|
||||
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
@ -3736,6 +3752,10 @@ packages:
|
||||
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
fresh@2.0.0:
|
||||
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
fs-constants@1.0.0:
|
||||
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
||||
|
||||
@ -4580,6 +4600,10 @@ packages:
|
||||
parse5@7.3.0:
|
||||
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
|
||||
|
||||
parseurl@1.3.3:
|
||||
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
path-browserify@1.0.1:
|
||||
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
|
||||
|
||||
@ -4777,6 +4801,10 @@ packages:
|
||||
resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
range-parser@1.2.1:
|
||||
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
rate-limiter-flexible@9.1.1:
|
||||
resolution: {integrity: sha512-imxFjzPCmvDLMe7d2tsgiSQvs5EI2fI9SNymmslAfOqznZhsZ+PqbIjIYKpuSbd3pKovR1aMG47qfCLIO/adVg==}
|
||||
|
||||
@ -4977,6 +5005,10 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
send@1.2.1:
|
||||
resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
seroval-plugins@1.5.4:
|
||||
resolution: {integrity: sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw==}
|
||||
engines: {node: '>=10'}
|
||||
@ -4987,6 +5019,10 @@ packages:
|
||||
resolution: {integrity: sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
serve-static@2.2.1:
|
||||
resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
set-cookie-parser@2.7.2:
|
||||
resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
|
||||
|
||||
@ -6032,6 +6068,15 @@ snapshots:
|
||||
'@japa/api-client': 3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0)
|
||||
'@japa/plugin-adonisjs': 5.2.0(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))(@japa/api-client@3.2.1(@japa/assert@4.2.0(@japa/runner@5.3.0))(@japa/runner@5.3.0))(@japa/runner@5.3.0)
|
||||
|
||||
'@adonisjs/static@2.0.1(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@adonisjs/core@7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1))':
|
||||
dependencies:
|
||||
'@adonisjs/core': 7.3.2(@adonisjs/assembler@8.4.0(typescript@6.0.3))(@vinejs/vine@4.4.0)(pino-pretty@13.1.3)(youch@4.1.1)
|
||||
serve-static: 2.2.1
|
||||
optionalDependencies:
|
||||
'@adonisjs/assembler': 8.4.0(typescript@6.0.3)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@adonisjs/tsconfig@2.0.0': {}
|
||||
|
||||
'@antfu/install-pkg@1.1.0':
|
||||
@ -9061,6 +9106,8 @@ snapshots:
|
||||
|
||||
escalade@3.2.0: {}
|
||||
|
||||
escape-html@1.0.3: {}
|
||||
|
||||
escape-string-regexp@1.0.5: {}
|
||||
|
||||
escape-string-regexp@4.0.0: {}
|
||||
@ -9397,6 +9444,8 @@ snapshots:
|
||||
|
||||
fresh@0.5.2: {}
|
||||
|
||||
fresh@2.0.0: {}
|
||||
|
||||
fs-constants@1.0.0: {}
|
||||
|
||||
fsevents@2.3.3:
|
||||
@ -10132,6 +10181,8 @@ snapshots:
|
||||
dependencies:
|
||||
entities: 6.0.1
|
||||
|
||||
parseurl@1.3.3: {}
|
||||
|
||||
path-browserify@1.0.1: {}
|
||||
|
||||
path-exists@4.0.0: {}
|
||||
@ -10327,6 +10378,8 @@ snapshots:
|
||||
|
||||
random-bytes@1.0.0: {}
|
||||
|
||||
range-parser@1.2.1: {}
|
||||
|
||||
rate-limiter-flexible@9.1.1: {}
|
||||
|
||||
raw-body@3.0.2:
|
||||
@ -10542,12 +10595,37 @@ snapshots:
|
||||
|
||||
semver@7.7.4: {}
|
||||
|
||||
send@1.2.1:
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
encodeurl: 2.0.0
|
||||
escape-html: 1.0.3
|
||||
etag: 1.8.1
|
||||
fresh: 2.0.0
|
||||
http-errors: 2.0.1
|
||||
mime-types: 3.0.2
|
||||
ms: 2.1.3
|
||||
on-finished: 2.4.1
|
||||
range-parser: 1.2.1
|
||||
statuses: 2.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
seroval-plugins@1.5.4(seroval@1.5.4):
|
||||
dependencies:
|
||||
seroval: 1.5.4
|
||||
|
||||
seroval@1.5.4: {}
|
||||
|
||||
serve-static@2.2.1:
|
||||
dependencies:
|
||||
encodeurl: 2.0.0
|
||||
escape-html: 1.0.3
|
||||
parseurl: 1.3.3
|
||||
send: 1.2.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
set-cookie-parser@2.7.2: {}
|
||||
|
||||
set-cookie-parser@3.1.0: {}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user