Skip to content

feat: add production deployment configuration #1

feat: add production deployment configuration

feat: add production deployment configuration #1

Workflow file for this run

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"