From fcff23cd7ca8876b427c5a38aad0f87174f8731d Mon Sep 17 00:00:00 2001 From: Johngreen Date: Tue, 10 Feb 2026 12:21:07 +0900 Subject: [PATCH] chore: add CI/CD pipeline and container configs for production deployment - Containerfile.backend/frontend for Docker builds - docker-compose.prod.yml (PostgreSQL + API:8100 + Dashboard:4000) - Gitea Actions workflow for auto-deploy on push to main - Frontend dev port changed to 3100 Co-Authored-By: Claude Opus 4 --- .gitea/workflows/deploy.yml | 46 ++++++++++++++++++++++++ Containerfile.backend | 23 ++++++++++++ Containerfile.frontend | 46 ++++++++++++++++++++++++ dashboard/package.json | 2 +- docker-compose.prod.yml | 70 +++++++++++++++++++++++++++++++++++++ main.py | 2 +- 6 files changed, 187 insertions(+), 2 deletions(-) create mode 100644 .gitea/workflows/deploy.yml create mode 100644 Containerfile.backend create mode 100644 Containerfile.frontend 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..2a10017 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,46 @@ +name: Deploy to Production + +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: ubuntu-latest + container: + image: catthehacker/ubuntu:act-latest + steps: + - name: Install tools + run: | + rm -f /etc/apt/sources.list.d/github_git-lfs.list || true + apt-get update && apt-get install -y openssh-client rsync git + + - name: Checkout code + run: | + git clone https://geonhee:${{ secrets.DEPLOY_TOKEN }}@g.wace.me/geonhee/factoryOps-v2.git /tmp/factoryOps-v2 + cd /tmp/factoryOps-v2 && git checkout main + + - name: Deploy to server + run: | + mkdir -p ~/.ssh + echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + + rsync -avz --delete \ + --exclude '.git' \ + --exclude 'node_modules' \ + --exclude '__pycache__' \ + --exclude '.venv' \ + --exclude '.env' \ + --exclude '.next' \ + --exclude 'planning' \ + -e "ssh -o StrictHostKeyChecking=no -i ~/.ssh/deploy_key" \ + /tmp/factoryOps-v2/ geonhee@192.168.1.200:~/factoryops-v2/ + + ssh -o StrictHostKeyChecking=no -i ~/.ssh/deploy_key geonhee@192.168.1.200 << 'ENDSSH' + cd ~/factoryops-v2 + ~/.local/bin/docker-compose -f docker-compose.prod.yml up -d --build --force-recreate + echo "FactoryOps v2 deployed at $(date)" + ENDSSH + rm -f ~/.ssh/deploy_key diff --git a/Containerfile.backend b/Containerfile.backend new file mode 100644 index 0000000..4265a10 --- /dev/null +++ b/Containerfile.backend @@ -0,0 +1,23 @@ +FROM python:3.11-slim + +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY main.py . +COPY src/ ./src/ +COPY alembic/ ./alembic/ +COPY alembic.ini . +COPY scripts/ ./scripts/ + +ENV PYTHONPATH=/app +ENV PYTHONUNBUFFERED=1 + +EXPOSE 8000 + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/Containerfile.frontend b/Containerfile.frontend new file mode 100644 index 0000000..e362f2d --- /dev/null +++ b/Containerfile.frontend @@ -0,0 +1,46 @@ +FROM node:20-alpine AS base + +FROM base AS deps +RUN apk add --no-cache libc6-compat +WORKDIR /app + +COPY dashboard/package.json dashboard/package-lock.json* ./ +RUN npm ci + +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY dashboard/ . + +ENV NEXT_TELEMETRY_DISABLED=1 + +ARG NEXT_PUBLIC_API_URL +ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} + +RUN npm run build + +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public + +RUN mkdir .next +RUN chown nextjs:nodejs .next + +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs + +EXPOSE 4000 + +ENV PORT=4000 +ENV HOSTNAME="0.0.0.0" + +CMD ["node", "server.js"] diff --git a/dashboard/package.json b/dashboard/package.json index 304396e..a9262d7 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", + "dev": "next dev --port 3100", "build": "next build", "start": "next start", "lint": "eslint" diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..4ccaeaf --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,70 @@ +version: '3.8' + +services: + postgres: + image: postgres:16-alpine + container_name: factoryops-v2-db + restart: unless-stopped + environment: + POSTGRES_USER: factoryops + POSTGRES_PASSWORD: factoryops + POSTGRES_DB: factoryops_v2 + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U factoryops"] + interval: 10s + timeout: 5s + retries: 5 + + factoryops-api: + build: + context: . + dockerfile: Containerfile.backend + container_name: factoryops-v2-api + restart: unless-stopped + ports: + - "8100:8000" + environment: + - PYTHONPATH=/app + - PYTHONUNBUFFERED=1 + - DATABASE_URL=postgresql+asyncpg://factoryops:factoryops@postgres:5432/factoryops_v2 + - JWT_SECRET_KEY=${JWT_SECRET_KEY:-factoryops-v2-prod-secret} + - CORS_ORIGINS=https://factoryops.vexplor.com + depends_on: + postgres: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/api/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 20s + + factoryops-dashboard: + build: + context: . + dockerfile: Containerfile.frontend + args: + NEXT_PUBLIC_API_URL: https://api.factoryops.vexplor.com + container_name: factoryops-v2-dashboard + restart: unless-stopped + ports: + - "4000:4000" + environment: + - NODE_ENV=production + depends_on: + - factoryops-api + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://localhost:4000"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + +volumes: + postgres_data: + +networks: + default: + name: factoryops-v2-network diff --git a/main.py b/main.py index d585634..f875991 100644 --- a/main.py +++ b/main.py @@ -43,7 +43,7 @@ CORS_ORIGINS = ( os.getenv("CORS_ORIGINS", "").split(",") if os.getenv("CORS_ORIGINS") else [] ) if not CORS_ORIGINS: - CORS_ORIGINS = ["http://localhost:3000", "http://127.0.0.1:3000"] + CORS_ORIGINS = ["http://localhost:3100", "http://127.0.0.1:3100"] app.add_middleware( CORSMiddleware, allow_origins=CORS_ORIGINS,