name: Production Deployment (Zero Downtime) on: push: branches: [ production ] env: NODE_VERSION: '20' DOCKER_IMAGE: portfolio-app IMAGE_TAG: production jobs: deploy-production: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm' - name: Install dependencies run: npm ci - name: Run linting run: npm run lint - name: Run tests run: npm run test:production - name: Build application run: npm run build - name: Build Docker image run: | echo "๐Ÿ—๏ธ Building production Docker image..." docker build -t ${{ env.DOCKER_IMAGE }}:${{ env.IMAGE_TAG }} . docker tag ${{ env.DOCKER_IMAGE }}:${{ env.IMAGE_TAG }} ${{ env.DOCKER_IMAGE }}:latest echo "โœ… Docker image built successfully" - name: Zero-Downtime Production Deployment run: | echo "๐Ÿš€ Starting zero-downtime production deployment..." COMPOSE_FILE="docker-compose.production.yml" CONTAINER_NAME="portfolio-app" HEALTH_PORT="3000" # Backup current container ID if running OLD_CONTAINER=$(docker ps -q -f name=$CONTAINER_NAME || echo "") # Start new container with updated image (docker-compose will handle this) echo "๐Ÿ†• Starting new production container..." docker compose -f $COMPOSE_FILE up -d --no-deps --build portfolio # Wait for new container to be healthy echo "โณ Waiting for new container to be healthy..." for i in {1..60}; do NEW_CONTAINER=$(docker ps -q -f name=$CONTAINER_NAME) if [ ! -z "$NEW_CONTAINER" ]; then # Check health status HEALTH=$(docker inspect $NEW_CONTAINER --format='{{.State.Health.Status}}' 2>/dev/null || echo "starting") if [ "$HEALTH" == "healthy" ]; then echo "โœ… New container is healthy!" break fi # Also check HTTP health endpoint if curl -f http://localhost:$HEALTH_PORT/api/health > /dev/null 2>&1; then echo "โœ… New container is responding!" break fi fi echo "โณ Waiting... ($i/60)" sleep 2 done # Verify new container is working if ! curl -f http://localhost:$HEALTH_PORT/api/health > /dev/null 2>&1; then echo "โŒ New container failed health check!" docker compose -f $COMPOSE_FILE logs --tail=50 portfolio exit 1 fi # Remove old container if it exists and is different if [ ! -z "$OLD_CONTAINER" ]; then NEW_CONTAINER=$(docker ps -q -f name=$CONTAINER_NAME) if [ "$OLD_CONTAINER" != "$NEW_CONTAINER" ]; then echo "๐Ÿงน Removing old container..." docker stop $OLD_CONTAINER 2>/dev/null || true docker rm $OLD_CONTAINER 2>/dev/null || true fi fi echo "โœ… Production deployment completed with zero downtime!" env: NODE_ENV: production LOG_LEVEL: ${{ vars.LOG_LEVEL || 'info' }} NEXT_PUBLIC_BASE_URL: ${{ vars.NEXT_PUBLIC_BASE_URL || 'https://dk0.dev' }} MY_EMAIL: ${{ vars.MY_EMAIL }} MY_INFO_EMAIL: ${{ vars.MY_INFO_EMAIL }} MY_PASSWORD: ${{ secrets.MY_PASSWORD }} MY_INFO_PASSWORD: ${{ secrets.MY_INFO_PASSWORD }} ADMIN_BASIC_AUTH: ${{ secrets.ADMIN_BASIC_AUTH }} N8N_WEBHOOK_URL: ${{ vars.N8N_WEBHOOK_URL || '' }} N8N_SECRET_TOKEN: ${{ secrets.N8N_SECRET_TOKEN || '' }} - name: Production Health Check run: | echo "๐Ÿ” Running production health checks..." for i in {1..20}; do if curl -f http://localhost:3000/api/health && curl -f http://localhost:3000/ > /dev/null; then echo "โœ… Production is fully operational!" exit 0 fi echo "โณ Waiting for production... ($i/20)" sleep 3 done echo "โŒ Production health check failed!" docker compose -f docker-compose.production.yml logs --tail=50 exit 1 - name: Cleanup run: | echo "๐Ÿงน Cleaning up old images..." docker image prune -f echo "โœ… Cleanup completed"