From 34d242c6140b389b7c5db5852bbc931f54b32f83 Mon Sep 17 00:00:00 2001 From: Yves Gugger Date: Sat, 20 Dec 2025 18:57:31 +0100 Subject: [PATCH] Add CI/CD pipeline and Docker configuration - Add Gitea Actions workflow for automatic deployment - Add production Dockerfile for frontend - Add docker-compose.prod.yml for easy deployment - Zero-downtime deployment with health checks --- .gitea/workflows/deploy.yml | 156 ++++++++++++++++++++++++++++++++++++ docker-compose.prod.yml | 65 +++++++++++++++ frontend/Dockerfile | 42 +++++----- 3 files changed, 242 insertions(+), 21 deletions(-) create mode 100644 .gitea/workflows/deploy.yml create mode 100644 docker-compose.prod.yml diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..effded9 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,156 @@ +name: Deploy Pounce + +on: + push: + branches: + - main + +env: + REGISTRY: ghcr.io + BACKEND_IMAGE: pounce-backend + FRONTEND_IMAGE: pounce-frontend + SERVER_HOST: 185.142.213.170 + +jobs: + build-backend: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build Backend Image + run: | + cd backend + docker build -t ${{ env.BACKEND_IMAGE }}:${{ github.sha }} -t ${{ env.BACKEND_IMAGE }}:latest . + + - name: Save Backend Image + run: | + docker save ${{ env.BACKEND_IMAGE }}:latest | gzip > backend-image.tar.gz + + - name: Upload Backend Artifact + uses: actions/upload-artifact@v4 + with: + name: backend-image + path: backend-image.tar.gz + + build-frontend: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build Frontend Image + run: | + cd frontend + docker build -t ${{ env.FRONTEND_IMAGE }}:${{ github.sha }} -t ${{ env.FRONTEND_IMAGE }}:latest \ + --build-arg NEXT_PUBLIC_API_URL=http://backend.185-142-213-170.sslip.io . + + - name: Save Frontend Image + run: | + docker save ${{ env.FRONTEND_IMAGE }}:latest | gzip > frontend-image.tar.gz + + - name: Upload Frontend Artifact + uses: actions/upload-artifact@v4 + with: + name: frontend-image + path: frontend-image.tar.gz + + deploy: + runs-on: ubuntu-latest + needs: [build-backend, build-frontend] + steps: + - name: Download Backend Image + uses: actions/download-artifact@v4 + with: + name: backend-image + + - name: Download Frontend Image + uses: actions/download-artifact@v4 + with: + name: frontend-image + + - name: Setup SSH + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.DEPLOY_SSH_KEY }} + + - name: Deploy to Server + run: | + # Copy images to server + scp -o StrictHostKeyChecking=no backend-image.tar.gz administrator@${{ env.SERVER_HOST }}:/tmp/ + scp -o StrictHostKeyChecking=no frontend-image.tar.gz administrator@${{ env.SERVER_HOST }}:/tmp/ + + # Load and restart on server + ssh -o StrictHostKeyChecking=no administrator@${{ env.SERVER_HOST }} << 'DEPLOY' + # Load new images + gunzip -c /tmp/backend-image.tar.gz | sudo docker load + gunzip -c /tmp/frontend-image.tar.gz | sudo docker load + + # Restart containers with zero-downtime + sudo docker stop pounce-backend-new 2>/dev/null || true + sudo docker rm pounce-backend-new 2>/dev/null || true + + sudo docker run -d \ + --name pounce-backend-new \ + --network n0488s44osgoow4wgo04ogg0 \ + --restart unless-stopped \ + -e DATABASE_URL="postgresql+asyncpg://pounce:PounceDB2024!@supabase-db-n0488s44osgoow4wgo04ogg0:5432/pounce" \ + -e JWT_SECRET="${{ secrets.JWT_SECRET }}" \ + -e FRONTEND_URL="http://pounce.185-142-213-170.sslip.io" \ + -e ENVIRONMENT="production" \ + -l "traefik.enable=true" \ + -l "traefik.http.routers.pounce-backend.rule=Host(\`backend.185-142-213-170.sslip.io\`)" \ + -l "traefik.http.routers.pounce-backend.entryPoints=http" \ + -l "traefik.http.services.pounce-backend.loadbalancer.server.port=8000" \ + pounce-backend:latest + + # Also connect to coolify network + sudo docker network connect coolify pounce-backend-new 2>/dev/null || true + + # Health check + sleep 15 + if curl -s http://localhost:8001/health | grep -q healthy; then + sudo docker stop pounce-backend 2>/dev/null || true + sudo docker rm pounce-backend 2>/dev/null || true + sudo docker rename pounce-backend-new pounce-backend + echo "Backend deployed successfully!" + else + sudo docker stop pounce-backend-new + sudo docker rm pounce-backend-new + echo "Backend health check failed!" + exit 1 + fi + + # Frontend + sudo docker stop pounce-frontend-new 2>/dev/null || true + sudo docker rm pounce-frontend-new 2>/dev/null || true + + sudo docker run -d \ + --name pounce-frontend-new \ + --network coolify \ + --restart unless-stopped \ + -e NEXT_PUBLIC_API_URL="http://backend.185-142-213-170.sslip.io" \ + -l "traefik.enable=true" \ + -l "traefik.http.routers.pounce-frontend.rule=Host(\`pounce.185-142-213-170.sslip.io\`)" \ + -l "traefik.http.routers.pounce-frontend.entryPoints=http" \ + -l "traefik.http.services.pounce-frontend.loadbalancer.server.port=3000" \ + pounce-frontend:latest + + sleep 10 + sudo docker stop pounce-frontend 2>/dev/null || true + sudo docker rm pounce-frontend 2>/dev/null || true + sudo docker rename pounce-frontend-new pounce-frontend + + # Cleanup + rm -f /tmp/backend-image.tar.gz /tmp/frontend-image.tar.gz + sudo docker image prune -f + + echo "Deployment complete!" + DEPLOY + + - name: Verify Deployment + run: | + sleep 5 + curl -f http://backend.185-142-213-170.sslip.io/health || exit 1 + curl -f http://pounce.185-142-213-170.sslip.io || exit 1 + echo "All services healthy!" diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..a9ca8b1 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,65 @@ +version: '3.8' + +services: + backend: + build: + context: ./backend + dockerfile: Dockerfile + container_name: pounce-backend + restart: unless-stopped + networks: + - pounce-network + - supabase-network + environment: + - DATABASE_URL=postgresql+asyncpg://pounce:PounceDB2024!@supabase-db-n0488s44osgoow4wgo04ogg0:5432/pounce + - JWT_SECRET=${JWT_SECRET:-pounce-super-secret-jwt-key-2024-production} + - FRONTEND_URL=http://pounce.185-142-213-170.sslip.io + - ENVIRONMENT=production + - ENABLE_SCHEDULER=true + labels: + - "traefik.enable=true" + - "traefik.http.routers.pounce-backend.rule=Host(`backend.185-142-213-170.sslip.io`)" + - "traefik.http.routers.pounce-backend.entryPoints=http" + - "traefik.http.services.pounce-backend.loadbalancer.server.port=8000" + - "coolify.managed=true" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + args: + - NEXT_PUBLIC_API_URL=http://backend.185-142-213-170.sslip.io + container_name: pounce-frontend + restart: unless-stopped + networks: + - pounce-network + environment: + - NEXT_PUBLIC_API_URL=http://backend.185-142-213-170.sslip.io + labels: + - "traefik.enable=true" + - "traefik.http.routers.pounce-frontend.rule=Host(`pounce.185-142-213-170.sslip.io`)" + - "traefik.http.routers.pounce-frontend.entryPoints=http" + - "traefik.http.services.pounce-frontend.loadbalancer.server.port=3000" + - "coolify.managed=true" + depends_on: + - backend + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + +networks: + pounce-network: + name: coolify + external: true + supabase-network: + name: n0488s44osgoow4wgo04ogg0 + external: true diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 601945a..41021cb 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,37 +1,38 @@ -# pounce Frontend Dockerfile -FROM node:18-alpine AS base - -# Install dependencies only when needed -FROM base AS deps -RUN apk add --no-cache libc6-compat +# Multi-stage build for optimized production image +FROM node:20-alpine AS deps WORKDIR /app -# Copy package files -COPY package.json package-lock.json ./ -RUN npm ci +# Install dependencies +COPY package.json package-lock.json* ./ +RUN npm ci --prefer-offline -# Rebuild source code only when needed -FROM base AS builder +# Builder stage +FROM node:20-alpine AS builder WORKDIR /app + COPY --from=deps /app/node_modules ./node_modules COPY . . -# Build the application -ENV NEXT_TELEMETRY_DISABLED 1 +# Build arguments +ARG NEXT_PUBLIC_API_URL +ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} +ENV NODE_OPTIONS="--max-old-space-size=2048" +ENV NEXT_TELEMETRY_DISABLED=1 + RUN npm run build -# Production image -FROM base AS runner +# Production stage +FROM node:20-alpine AS runner WORKDIR /app -ENV NODE_ENV production -ENV NEXT_TELEMETRY_DISABLED 1 +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 # Create non-root user RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs -# Copy built application +# Copy built assets COPY --from=builder /app/public ./public COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static @@ -40,8 +41,7 @@ USER nextjs EXPOSE 3000 -ENV PORT 3000 -ENV HOSTNAME "0.0.0.0" +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" CMD ["node", "server.js"] -