diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..abd248a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +node_modules +.next +.git +.forgejo +.claude +drizzle +dev-server.log +npm-debug.log* +.env +.env.* +!.env.example diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml new file mode 100644 index 0000000..1fd69a8 --- /dev/null +++ b/.forgejo/workflows/build.yml @@ -0,0 +1,249 @@ +name: Deploy eCourt Note Web to Server + +on: + push: + tags: + - "v*.*.*" + workflow_dispatch: + +env: + # Container Registry + REGISTRY_URL: ${{ vars.CONTAINER_REGISTRY }} + REGISTRY_USERNAME: ${{ vars.CONTAINER_REGISTRY_USERNAME }} + REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + + # Docker Image + WEB_IMAGE_NAME: chamomind/ecourt-note + IMAGE_TAG: ${{ github.ref_name }} + + # SSH Deployment + DEPLOY_HOST: ${{ vars.SSH_DEPLOY_HOST }} + DEPLOY_USER: ${{ vars.SSH_DEPLOY_USER }} + + # Notifications + DISCORD_WEBHOOK_URL: ${{ vars.DISCORD_WEBHOOK_URL }} + NEXT_PUBLIC_APP_URL: ${{ vars.NEXT_PUBLIC_APP_URL }} + +jobs: + validate-schema: + name: Validate Database Schema (Drizzle) + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Verify tag is on main branch + run: | + echo "======================================================" + echo "Verifying tag is pushed to main branch" + echo "======================================================" + echo "Tag: ${{ github.ref_name }}" + echo "Commit: ${{ github.sha }}" + + # Fetch main branch + git fetch origin main + + # Check if the tag's commit is on main branch + if git branch -r --contains ${{ github.sha }} | grep -q "origin/main"; then + echo "✅ Tag is on main branch" + echo "test_branch=true" >> $GITHUB_ENV + else + echo "❌ Tag is NOT on main branch" + echo "This workflow only triggers for tags pushed to main branch" + echo "" + echo "Current branches containing this commit:" + git branch -r --contains ${{ github.sha }} + exit 1 + fi + + - name: Check for schema changes + id: schema-check + run: | + # โปรเจกต์นี้ใช้ Drizzle (drizzle-kit push) ไม่มีโฟลเดอร์ migrations — ดูการเปลี่ยนแปลงที่ db/schema.ts + if git diff --name-only origin/main...HEAD | grep -qE "^db/schema\.ts$"; then + echo "has_schema_changes=true" >> $GITHUB_OUTPUT + echo "✅ Schema changes detected" + else + echo "has_schema_changes=false" >> $GITHUB_OUTPUT + echo "ℹ️ No schema changes" + fi + + - name: Schema change summary + if: steps.schema-check.outputs.has_schema_changes == 'true' + run: | + echo "=== Schema Change Summary ===" + echo "" + echo "Database schema (db/schema.ts) has been modified in this deployment." + echo "This project uses Drizzle push (no migration files)." + echo "" + echo "⚠️ REMINDERS:" + echo " 1. Run 'npx drizzle-kit push' on the target database (staging first)" + echo " 2. Monitor logs after deployment" + echo "" + echo "📚 Reference: README.md / docs/PLAN.md §3" + + build: + name: Build Web Docker Image + runs-on: ubuntu-latest + needs: validate-schema + if: startsWith(github.ref, 'refs/tags/') + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY_URL }} + username: ${{ env.REGISTRY_USERNAME }} + password: ${{ env.REGISTRY_PASSWORD }} + + - name: Build Docker image (no buildx) + run: | + # Build using standard docker build (not buildx) + git reset --hard HEAD + cd ${{ github.workspace }} + docker build \ + --no-cache \ + --pull \ + --file ./docker/web.Dockerfile \ + --tag ${{ env.REGISTRY_URL }}/${{ env.WEB_IMAGE_NAME }}:${{ env.IMAGE_TAG }} \ + --tag ${{ env.REGISTRY_URL }}/${{ env.WEB_IMAGE_NAME }}:latest \ + . + + echo "✅ Build successful!" + docker images | grep ${{ env.WEB_IMAGE_NAME }} + + - name: Push Docker image + run: | + docker push ${{ env.REGISTRY_URL }}/${{ env.WEB_IMAGE_NAME }}:${{ env.IMAGE_TAG }} + docker push ${{ env.REGISTRY_URL }}/${{ env.WEB_IMAGE_NAME }}:latest + echo "✅ Image pushed to registry" + + - name: Test image can run + run: | + # Quick test that image can start + docker run --rm ${{ env.REGISTRY_URL }}/${{ env.WEB_IMAGE_NAME }}:${{ env.IMAGE_TAG }} node --version + + deploy: + name: Deploy eCourt Note Web to Server + runs-on: ubuntu-latest + needs: build + if: startsWith(github.ref, 'refs/tags/') + steps: + - name: Deploy web on server + uses: appleboy/ssh-action@v1.2.1 + with: + host: ${{ env.DEPLOY_HOST }} + port: ${{ vars.SSH_DEPLOY_PORT }} + username: ${{ env.DEPLOY_USER }} + password: ${{ secrets.SSH_DEPLOY_PASSWORD }} + script: | + cd ~/repo + + chmod +x ./replace-env.sh ./deploy.sh + ./replace-env.sh ECOURT_NOTE_TAG "${{ env.IMAGE_TAG }}" + ./deploy.sh + + notify: + name: Notify Deployment Status + runs-on: ubuntu-latest + needs: [build, deploy] + if: always() + steps: + - name: Send Discord notification - Success + if: ${{ needs.deploy.result == 'success' }} + run: | + curl -H "Content-Type: application/json" \ + -d '{ + "embeds": [{ + "title": "✅ eCourt Note Deployment Successful", + "description": "eCourt Note Web deployed to server", + "color": 3066993, + "fields": [ + { + "name": "Environment", + "value": "Test", + "inline": true + }, + { + "name": "Tag", + "value": "${{ github.ref_name }}", + "inline": true + }, + { + "name": "Commit", + "value": "`${{ github.sha }}`", + "inline": false + }, + { + "name": "Image Tag", + "value": "`${{ env.IMAGE_TAG }}`", + "inline": true + }, + { + "name": "Web URL", + "value": "${{ env.NEXT_PUBLIC_APP_URL }}", + "inline": false + }, + { + "name": "Deployed By", + "value": "${{ github.actor }}", + "inline": true + } + ], + "footer": { + "text": "eCourt Note CD Pipeline" + }, + "timestamp": "'"$(date -u +%Y-%m-%dT%H:%M:%S.000Z)"'" + }] + }' \ + ${{ env.DISCORD_WEBHOOK_URL }} + + - name: Send Discord notification - Failure + if: ${{ needs.deploy.result == 'failure' || needs.build.result == 'failure' }} + run: | + curl -H "Content-Type: application/json" \ + -d '{ + "embeds": [{ + "title": "❌ eCourt Note Deployment Failed", + "description": "Failed to deploy eCourt Note Web to server", + "color": 15158332, + "fields": [ + { + "name": "Environment", + "value": "Test", + "inline": true + }, + { + "name": "Tag", + "value": "${{ github.ref_name }}", + "inline": true + }, + { + "name": "Commit", + "value": "`${{ github.sha }}`", + "inline": false + }, + { + "name": "Failed Job", + "value": "${{ needs.build.result == 'failure' && 'Build' || 'Deploy' }}", + "inline": true + }, + { + "name": "Deployed By", + "value": "${{ github.actor }}", + "inline": true + } + ], + "footer": { + "text": "eCourt Note CD Pipeline" + }, + "timestamp": "'"$(date -u +%Y-%m-%dT%H:%M:%S.000Z)"'" + }] + }' \ + ${{ env.DISCORD_WEBHOOK_URL }} + exit 1 diff --git a/docker/web.Dockerfile b/docker/web.Dockerfile new file mode 100644 index 0000000..14116ca --- /dev/null +++ b/docker/web.Dockerfile @@ -0,0 +1,37 @@ +# ─── Stage 1: ติดตั้ง dependencies (npm ci รวม postinstall คัดลอก pdf.worker) ─── +FROM node:24-alpine AS deps +WORKDIR /app +COPY package.json package-lock.json ./ +# postinstall เขียน public/pdf.worker.min.mjs — ต้องมีโฟลเดอร์ public/ ก่อน ไม่งั้น npm ci ล้ม +RUN mkdir -p public && npm ci + +# ─── Stage 2: build Next.js (output: standalone) ─── +FROM node:24-alpine AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +# ประกันว่า pdf.worker.min.mjs ตรงกับเวอร์ชัน pdfjs-dist ใน node_modules เสมอ +# (postinstall รันใน stage deps ซึ่งยังไม่มี public/ ของ repo) +RUN node -e "require('fs').copyFileSync('node_modules/pdfjs-dist/build/pdf.worker.min.mjs','public/pdf.worker.min.mjs')" +ENV NEXT_TELEMETRY_DISABLED=1 +# db/index.ts throw ถ้าไม่มี DATABASE_URL ตอน next build เก็บ page data — +# ใส่ค่า dummy (postgres.js เชื่อมแบบ lazy ไม่มี query ตอน build) ตัวจริงฉีดตอน docker run +ENV DATABASE_URL=postgresql://placeholder:placeholder@127.0.0.1:5432/placeholder +RUN npm run build + +# ─── Stage 3: runtime ตัวเบา (แค่ standalone + public/) ─── +FROM node:24-alpine AS runner +WORKDIR /app +ENV NODE_ENV=production \ + NEXT_TELEMETRY_DISABLED=1 \ + PORT=3000 \ + HOSTNAME=0.0.0.0 +RUN addgroup -S nodejs && adduser -S nextjs -G nodejs +# Next 14.2 รวม public/ (pdf.worker, ฟอนต์ Sarabun, samples PDF) ไว้ใน standalone แล้ว +# แต่ไม่รวม .next/static (JS chunks) — ต้องคัดลอกเพิ่ม ไม่งั้นหน้าเว็บไม่มี JS +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static +USER nextjs +EXPOSE 3000 +# DATABASE_URL ฉีดตอนรันผ่าน env ของ container (อย่าฝังใน image) +CMD ["node", "server.js"] diff --git a/next.config.mjs b/next.config.mjs index 4678774..366308b 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,4 +1,7 @@ /** @type {import('next').NextConfig} */ -const nextConfig = {}; +const nextConfig = { + // สำหรับ Docker (docker/web.Dockerfile) — build แล้วได้ .next/standalone รันด้วย node server.js + output: "standalone", +}; export default nextConfig;