feat: add production deployment configuration #1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI/CD for Production Server | |
| on: | |
| push: | |
| branches: [ main ] # Only main branch | |
| permissions: | |
| contents: read | |
| jobs: | |
| ci-cd-prod: | |
| name: Test, Build, and Deploy to Production Server | |
| runs-on: ubuntu-latest | |
| steps: | |
| # ======================================== | |
| # CI Stage: Test & Lint | |
| # ======================================== | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '20' | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Run linter | |
| run: npm run lint | |
| - name: Run tests | |
| run: npm test -- --ci --coverage --maxWorkers=2 | |
| # ======================================== | |
| # CD Stage: Build & Push Docker Image | |
| # ======================================== | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Log in to Docker Hub | |
| uses: docker/login-action@v3 | |
| with: | |
| username: ${{ secrets.DOCKER_USERNAME }} | |
| password: ${{ secrets.DOCKER_PASSWORD }} | |
| - name: Build and push Docker image | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: . | |
| file: ./Dockerfile | |
| push: true | |
| tags: | | |
| ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }}:latest | |
| ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }}:prod | |
| ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }}:prod-${{ github.sha }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| build-args: | | |
| NODE_ENV=production | |
| NEXT_TELEMETRY_DISABLED=1 | |
| # ======================================== | |
| # Deploy Stage: SSH & Deploy | |
| # ======================================== | |
| - name: Setup SSH key and config | |
| run: | | |
| mkdir -p ~/.ssh | |
| echo "${{ secrets.PROD_SSH_PRIVATE_KEY }}" > ~/.ssh/my-key.pem | |
| chmod 400 ~/.ssh/my-key.pem | |
| ssh-keyscan -H ${{ secrets.PROD_SERVER_HOST }} >> ~/.ssh/known_hosts | |
| echo -e "Host *\n ServerAliveInterval 60\n ServerAliveCountMax 3" >> ~/.ssh/config | |
| - name: Create app directory on server | |
| run: | | |
| ssh -i ~/.ssh/my-key.pem ${{ secrets.PROD_SERVER_USER }}@${{ secrets.PROD_SERVER_HOST }} "sudo mkdir -p /opt/app/nextjs && sudo chown ${{ secrets.PROD_SERVER_USER }}:${{ secrets.PROD_SERVER_USER }} /opt/app/nextjs" | |
| - name: Copy deployment files to server | |
| run: | | |
| scp -i ~/.ssh/my-key.pem docker-compose-prod.yaml ${{ secrets.PROD_SERVER_USER }}@${{ secrets.PROD_SERVER_HOST }}:/opt/app/nextjs/ | |
| scp -i ~/.ssh/my-key.pem nginx.conf ${{ secrets.PROD_SERVER_USER }}@${{ secrets.PROD_SERVER_HOST }}:/opt/app/nextjs/ | |
| - name: Deploy to Production Server | |
| run: | | |
| ssh -i ~/.ssh/my-key.pem ${{ secrets.PROD_SERVER_USER }}@${{ secrets.PROD_SERVER_HOST }} << 'EOF' | |
| cd /opt/app/nextjs | |
| # Write .env content from GitHub Secret | |
| echo "${{ secrets.ENV_FILE_PROD }}" > .env | |
| # Pull latest production image | |
| docker pull ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }}:prod | |
| # Stop and remove existing containers | |
| docker compose -f docker-compose-prod.yaml down | |
| # Start new containers | |
| docker compose -f docker-compose-prod.yaml up -d | |
| echo "✅ Production deployment complete" | |
| EOF | |
| # ======================================== | |
| # Health Check Stage (Stricter for Production) | |
| # ======================================== | |
| - name: Comprehensive Health Check | |
| run: | | |
| ssh -i ~/.ssh/my-key.pem ${{ secrets.PROD_SERVER_USER }}@${{ secrets.PROD_SERVER_HOST }} << 'EOF' | |
| echo "=== Starting Production Health Check ===" | |
| # 1. Check if Next.js container is running | |
| CONTAINER_ID=$(docker ps -q --filter "name=nextjs-app-prod") | |
| if [ -z "$CONTAINER_ID" ]; then | |
| echo "❌ Next.js container not running" | |
| docker ps -a | |
| docker logs nextjs-app-prod --tail 50 | |
| exit 1 | |
| fi | |
| echo "✅ Next.js container is running (ID: $CONTAINER_ID)" | |
| # 2. Check if Nginx container is running | |
| NGINX_CONTAINER_ID=$(docker ps -q --filter "name=nginx-proxy-prod") | |
| if [ -z "$NGINX_CONTAINER_ID" ]; then | |
| echo "❌ Nginx container not running" | |
| docker ps -a | |
| docker logs nginx-proxy-prod --tail 50 | |
| exit 1 | |
| fi | |
| echo "✅ Nginx container is running (ID: $NGINX_CONTAINER_ID)" | |
| # 3. Wait for Docker health check (Next.js) | |
| echo "Waiting for Next.js container to become healthy..." | |
| for i in {1..36}; do | |
| HEALTH_STATUS=$(docker inspect --format='{{.State.Health.Status}}' nextjs-app-prod 2>/dev/null || echo "no-healthcheck") | |
| if [ "$HEALTH_STATUS" == "healthy" ]; then | |
| echo "✅ Next.js container is healthy" | |
| break | |
| elif [ "$HEALTH_STATUS" == "no-healthcheck" ]; then | |
| echo "⚠️ No healthcheck configured, checking endpoint directly" | |
| break | |
| fi | |
| echo "Current health status: $HEALTH_STATUS ($i/36)" | |
| sleep 10 | |
| if [ $i -eq 36 ]; then | |
| echo "❌ Container failed to become healthy after 6 minutes" | |
| docker logs nextjs-app-prod --tail 100 | |
| exit 1 | |
| fi | |
| done | |
| # 4. Wait for Docker health check (Nginx) | |
| echo "Waiting for Nginx container to become healthy..." | |
| for i in {1..12}; do | |
| NGINX_HEALTH_STATUS=$(docker inspect --format='{{.State.Health.Status}}' nginx-proxy-prod 2>/dev/null || echo "no-healthcheck") | |
| if [ "$NGINX_HEALTH_STATUS" == "healthy" ]; then | |
| echo "✅ Nginx container is healthy" | |
| break | |
| fi | |
| echo "Current nginx health status: $NGINX_HEALTH_STATUS ($i/12)" | |
| sleep 10 | |
| if [ $i -eq 12 ]; then | |
| echo "❌ Nginx failed to become healthy after 2 minutes" | |
| docker logs nginx-proxy-prod --tail 100 | |
| exit 1 | |
| fi | |
| done | |
| # 5. Check Next.js health endpoint through Nginx | |
| echo "Checking health endpoint through Nginx..." | |
| for i in {1..30}; do | |
| HEALTH_RESPONSE=$(curl -s http://localhost:${NGINX_PORT:-80}/api/health || echo "") | |
| if echo "$HEALTH_RESPONSE" | grep -q '"status":"ok"'; then | |
| echo "✅ Application health check passed" | |
| echo "Health response: $HEALTH_RESPONSE" | |
| break | |
| fi | |
| echo "Waiting for application to start... ($i/30)" | |
| sleep 10 | |
| if [ $i -eq 30 ]; then | |
| echo "❌ Application health check failed after 5 minutes" | |
| echo "Last response: $HEALTH_RESPONSE" | |
| docker logs nextjs-app-prod --tail 100 | |
| docker logs nginx-proxy-prod --tail 50 | |
| exit 1 | |
| fi | |
| done | |
| # 6. Smoke test: Check if Nginx responds | |
| echo "Running smoke test..." | |
| HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:${NGINX_PORT:-80}/api/health) | |
| if [ "$HTTP_CODE" == "200" ]; then | |
| echo "✅ Smoke test passed (HTTP $HTTP_CODE)" | |
| else | |
| echo "❌ Smoke test failed (HTTP $HTTP_CODE)" | |
| exit 1 | |
| fi | |
| echo "=== Health Check Complete ===" | |
| docker ps --filter "name=nextjs-app-prod" | |
| docker ps --filter "name=nginx-proxy-prod" | |
| EOF | |
| - name: Display deployment info | |
| if: success() | |
| run: | | |
| echo "✅ Production deployment successful!" | |
| echo "🔗 Production Server: http://${{ secrets.PROD_SERVER_HOST }}:${{ secrets.NGINX_PORT || 80 }}" | |
| echo "🐳 Image: ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }}:prod" | |
| echo "📦 Commit: ${{ github.sha }}" | |
| echo "⚠️ Please verify the production deployment manually" | |
| # ======================================== | |
| # Rollback on Failure (Optional) | |
| # ======================================== | |
| - name: Rollback on failure | |
| if: failure() | |
| run: | | |
| echo "❌ Deployment failed! Consider manual rollback if needed." | |
| echo "To rollback, SSH to server and run:" | |
| echo " cd /opt/app/nextjs" | |
| echo " docker pull ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }}:prod-<previous-sha>" | |
| echo " docker compose -f docker-compose-prod.yaml down" | |
| echo " docker compose -f docker-compose-prod.yaml up -d" |