From 21c92abc9c54188adfcf94a1ac702dfacd2006e4 Mon Sep 17 00:00:00 2001 From: ordinarthur <@arthurbarre.js@gmail.com> Date: Sat, 11 Apr 2026 14:09:16 +0200 Subject: [PATCH] feat: implement Kubernetes deployment infrastructure, migrate database to PostgreSQL, and add CI/CD pipeline --- .gitea/workflows/deploy.yml | 102 +++++++++++++++++++++++++++++ backend/Dockerfile | 27 ++++++++ backend/prisma/schema.prisma | 4 +- k8s/configmap.yml | 42 ++++++++++++ k8s/deployment.yml | 122 +++++++++++++++++++++++++++++++++++ k8s/namespace.yml | 4 ++ k8s/pvc.yml | 12 ++++ k8s/service.yml | 40 ++++++++++++ 8 files changed, 351 insertions(+), 2 deletions(-) create mode 100644 .gitea/workflows/deploy.yml create mode 100644 backend/Dockerfile create mode 100644 k8s/configmap.yml create mode 100644 k8s/deployment.yml create mode 100644 k8s/namespace.yml create mode 100644 k8s/pvc.yml create mode 100644 k8s/service.yml diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..266a106 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,102 @@ +name: Build & Deploy to K3s + +on: + push: + branches: [main] + +env: + REGISTRY: git.arthurbarre.fr + BACKEND_IMAGE: git.arthurbarre.fr/ordinarthur/freedge-backend + FRONTEND_IMAGE: git.arthurbarre.fr/ordinarthur/freedge-frontend + REGISTRY_USER: ordinarthur + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Login to Gitea Container Registry + run: | + echo "${{ secrets.REGISTRY_PASSWORD }}" | \ + docker login ${{ env.REGISTRY }} -u ${{ env.REGISTRY_USER }} --password-stdin + + - name: Build backend image + run: | + docker build \ + -t ${{ env.BACKEND_IMAGE }}:${{ github.sha }} \ + -t ${{ env.BACKEND_IMAGE }}:latest \ + ./backend + + - name: Build frontend image + run: | + docker build \ + --build-arg VITE_API_BASE_URL=https://freedge.app/api \ + --build-arg VITE_GOOGLE_CLIENT_ID=173866668387-i18igc0e1avqtsaqq6nig898bv6pvuk6.apps.googleusercontent.com \ + -t ${{ env.FRONTEND_IMAGE }}:${{ github.sha }} \ + -t ${{ env.FRONTEND_IMAGE }}:latest \ + ./frontend + + - name: Push backend image + run: | + docker push ${{ env.BACKEND_IMAGE }}:${{ github.sha }} + docker push ${{ env.BACKEND_IMAGE }}:latest + + - name: Push frontend image + run: | + docker push ${{ env.FRONTEND_IMAGE }}:${{ github.sha }} + docker push ${{ env.FRONTEND_IMAGE }}:latest + + - name: Install kubectl + run: | + curl -LO "https://dl.k8s.io/release/$(curl -Ls https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" + chmod +x kubectl + mv kubectl /usr/local/bin/kubectl + + - name: Configure kubeconfig + run: | + mkdir -p ~/.kube + echo "${{ secrets.KUBECONFIG }}" | base64 -d > ~/.kube/config + + - name: Apply namespace and shared resources + run: | + kubectl apply -f k8s/namespace.yml + kubectl apply -f k8s/configmap.yml + kubectl apply -f k8s/pvc.yml + kubectl apply -f k8s/service.yml + + - name: Create image pull secret + run: | + kubectl -n freedge create secret docker-registry gitea-registry-secret \ + --docker-server=${{ env.REGISTRY }} \ + --docker-username=${{ env.REGISTRY_USER }} \ + --docker-password="${{ secrets.REGISTRY_PASSWORD }}" \ + --dry-run=client -o yaml | kubectl apply -f - + + - name: Create app secrets + run: | + kubectl -n freedge create secret generic freedge-secrets \ + --from-literal=DATABASE_URL="${{ secrets.DATABASE_URL }}" \ + --from-literal=JWT_SECRET="${{ secrets.JWT_SECRET }}" \ + --from-literal=OPENAI_API_KEY="${{ secrets.OPENAI_API_KEY }}" \ + --from-literal=STRIPE_SECRET_KEY="${{ secrets.STRIPE_SECRET_KEY }}" \ + --from-literal=STRIPE_WEBHOOK_SECRET="${{ secrets.STRIPE_WEBHOOK_SECRET }}" \ + --from-literal=STRIPE_PRICE_ID_ESSENTIAL="${{ secrets.STRIPE_PRICE_ID_ESSENTIAL }}" \ + --from-literal=STRIPE_PRICE_ID_PREMIUM="${{ secrets.STRIPE_PRICE_ID_PREMIUM }}" \ + --dry-run=client -o yaml | kubectl apply -f - + + - name: Deploy workloads + run: | + kubectl apply -f k8s/deployment.yml + kubectl -n freedge set image deployment/freedge-backend \ + freedge-backend=${{ env.BACKEND_IMAGE }}:${{ github.sha }} + kubectl -n freedge set image deployment/freedge-frontend \ + freedge-frontend=${{ env.FRONTEND_IMAGE }}:${{ github.sha }} + kubectl -n freedge rollout status deployment/freedge-backend --timeout=180s + kubectl -n freedge rollout status deployment/freedge-frontend --timeout=180s + kubectl -n freedge rollout status deployment/freedge-proxy --timeout=180s + + - name: Cleanup old images + run: | + docker image prune -f diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..a2f838b --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,27 @@ +FROM node:20-slim AS build +WORKDIR /app + +COPY package*.json ./ +RUN npm ci + +COPY . . + +RUN npx prisma generate +RUN npm run build +RUN npm prune --omit=dev + +FROM node:20-slim +WORKDIR /app + +ENV NODE_ENV=production +ENV PORT=3000 + +COPY --from=build /app/package*.json ./ +COPY --from=build /app/node_modules ./node_modules +COPY --from=build /app/prisma ./prisma +COPY --from=build /app/dist ./dist +COPY --from=build /app/uploads ./uploads + +EXPOSE 3000 + +CMD ["node", "dist/server.js"] diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index e9b27e2..b97fcd5 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -3,7 +3,7 @@ generator client { } datasource db { - provider = "sqlite" + provider = "postgresql" url = env("DATABASE_URL") } @@ -55,4 +55,4 @@ model Recipe { user User @relation(fields: [userId], references: [id]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt -} \ No newline at end of file +} diff --git a/k8s/configmap.yml b/k8s/configmap.yml new file mode 100644 index 0000000..8430709 --- /dev/null +++ b/k8s/configmap.yml @@ -0,0 +1,42 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: freedge-config + namespace: freedge +data: + NODE_ENV: "production" + PORT: "3000" + LOG_LEVEL: "info" + CORS_ORIGINS: "https://freedge.app" + FRONTEND_URL: "https://freedge.app" + PUBLIC_BASE_URL: "https://freedge.app/api" + OPENAI_TEXT_MODEL: "gpt-4o-mini" + OPENAI_TRANSCRIBE_MODEL: "gpt-4o-mini-transcribe" + ENABLE_IMAGE_GENERATION: "true" + OPENAI_IMAGE_MODEL: "gpt-image-1" + OPENAI_IMAGE_QUALITY: "medium" + OPENAI_IMAGE_SIZE: "1024x1024" + OPENAI_MAX_RETRIES: "3" + OPENAI_TIMEOUT_MS: "60000" + proxy.conf: | + server { + listen 80; + server_name _; + + client_max_body_size 20M; + + location /api/ { + rewrite ^/api/(.*) /$1 break; + proxy_pass http://freedge-backend:3000; + 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; + } + + location / { + proxy_pass http://freedge-frontend:80; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } + } diff --git a/k8s/deployment.yml b/k8s/deployment.yml new file mode 100644 index 0000000..bafd6a4 --- /dev/null +++ b/k8s/deployment.yml @@ -0,0 +1,122 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: freedge-backend + namespace: freedge +spec: + replicas: 1 + selector: + matchLabels: + app: freedge-backend + template: + metadata: + labels: + app: freedge-backend + spec: + imagePullSecrets: + - name: gitea-registry-secret + initContainers: + - name: prisma-db-push + image: git.arthurbarre.fr/ordinarthur/freedge-backend:latest + command: ["sh", "-c", "npx prisma db push --skip-generate"] + envFrom: + - configMapRef: + name: freedge-config + - secretRef: + name: freedge-secrets + volumeMounts: + - name: uploads + mountPath: /app/uploads + containers: + - name: freedge-backend + image: git.arthurbarre.fr/ordinarthur/freedge-backend:latest + ports: + - containerPort: 3000 + envFrom: + - configMapRef: + name: freedge-config + - secretRef: + name: freedge-secrets + volumeMounts: + - name: uploads + mountPath: /app/uploads + readinessProbe: + httpGet: + path: /health + port: 3000 + initialDelaySeconds: 10 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /health + port: 3000 + initialDelaySeconds: 30 + periodSeconds: 30 + volumes: + - name: uploads + persistentVolumeClaim: + claimName: freedge-uploads +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: freedge-frontend + namespace: freedge +spec: + replicas: 1 + selector: + matchLabels: + app: freedge-frontend + template: + metadata: + labels: + app: freedge-frontend + spec: + imagePullSecrets: + - name: gitea-registry-secret + containers: + - name: freedge-frontend + image: git.arthurbarre.fr/ordinarthur/freedge-frontend:latest + ports: + - containerPort: 80 + readinessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 5 + periodSeconds: 10 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: freedge-proxy + namespace: freedge +spec: + replicas: 1 + selector: + matchLabels: + app: freedge-proxy + template: + metadata: + labels: + app: freedge-proxy + spec: + containers: + - name: nginx + image: nginx:alpine + ports: + - containerPort: 80 + volumeMounts: + - name: proxy-config + mountPath: /etc/nginx/conf.d/default.conf + subPath: proxy.conf + readinessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 5 + periodSeconds: 10 + volumes: + - name: proxy-config + configMap: + name: freedge-config diff --git a/k8s/namespace.yml b/k8s/namespace.yml new file mode 100644 index 0000000..9abc3d0 --- /dev/null +++ b/k8s/namespace.yml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: freedge diff --git a/k8s/pvc.yml b/k8s/pvc.yml new file mode 100644 index 0000000..333f6a2 --- /dev/null +++ b/k8s/pvc.yml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: freedge-uploads + namespace: freedge +spec: + accessModes: + - ReadWriteOnce + storageClassName: local-path + resources: + requests: + storage: 5Gi diff --git a/k8s/service.yml b/k8s/service.yml new file mode 100644 index 0000000..8e7fd4d --- /dev/null +++ b/k8s/service.yml @@ -0,0 +1,40 @@ +apiVersion: v1 +kind: Service +metadata: + name: freedge-backend + namespace: freedge +spec: + selector: + app: freedge-backend + ports: + - name: http + port: 3000 + targetPort: 3000 +--- +apiVersion: v1 +kind: Service +metadata: + name: freedge-frontend + namespace: freedge +spec: + selector: + app: freedge-frontend + ports: + - name: http + port: 80 + targetPort: 80 +--- +apiVersion: v1 +kind: Service +metadata: + name: freedge-proxy + namespace: freedge +spec: + type: NodePort + selector: + app: freedge-proxy + ports: + - name: http + port: 80 + targetPort: 80 + nodePort: 30082