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 and tests in parallel run: | npm run lint & LINT_PID=$! npm run test:production & TEST_PID=$! wait $LINT_PID $TEST_PID - name: Build application run: npm run build - name: Build Docker image run: | echo "๐Ÿ—๏ธ Building production Docker image with BuildKit cache..." DOCKER_BUILDKIT=1 docker build \ --cache-from ${{ env.DOCKER_IMAGE }}:${{ env.IMAGE_TAG }} \ --cache-from ${{ env.DOCKER_IMAGE }}:latest \ -t ${{ env.DOCKER_IMAGE }}:${{ env.IMAGE_TAG }} \ -t ${{ 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 "") # Export environment variables for docker-compose export N8N_WEBHOOK_URL="${{ vars.N8N_WEBHOOK_URL || '' }}" export N8N_SECRET_TOKEN="${{ secrets.N8N_SECRET_TOKEN || '' }}" export N8N_API_KEY="${{ vars.N8N_API_KEY || '' }}" # Also export other variables that docker-compose needs export MY_EMAIL="${{ vars.MY_EMAIL }}" export MY_INFO_EMAIL="${{ vars.MY_INFO_EMAIL }}" export MY_PASSWORD="${{ secrets.MY_PASSWORD }}" export MY_INFO_PASSWORD="${{ secrets.MY_INFO_PASSWORD }}" export ADMIN_BASIC_AUTH="${{ secrets.ADMIN_BASIC_AUTH }}" # Start new container with updated image (docker-compose will handle this) echo "๐Ÿ†• Starting new production container..." echo "๐Ÿ“ Environment check: N8N_WEBHOOK_URL=${N8N_WEBHOOK_URL:-(not set)}" 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..." HEALTH_CHECK_PASSED=false for i in {1..90}; do NEW_CONTAINER=$(docker ps -q -f name=$CONTAINER_NAME) if [ ! -z "$NEW_CONTAINER" ]; then # Check Docker health status first (most reliable) HEALTH=$(docker inspect $NEW_CONTAINER --format='{{.State.Health.Status}}' 2>/dev/null || echo "starting") if [ "$HEALTH" == "healthy" ]; then echo "โœ… New container is healthy (Docker health check)!" # Also verify HTTP endpoint from inside container if docker exec $NEW_CONTAINER curl -f -s --max-time 5 http://localhost:3000/api/health > /dev/null 2>&1; then echo "โœ… Container HTTP endpoint is also responding!" HEALTH_CHECK_PASSED=true break else echo "โš ๏ธ Docker health check passed, but HTTP endpoint test failed. Continuing..." fi fi # Try HTTP health endpoint from host (may not work if port not mapped yet) if curl -f -s --max-time 2 http://localhost:$HEALTH_PORT/api/health > /dev/null 2>&1; then echo "โœ… New container is responding to HTTP health check from host!" HEALTH_CHECK_PASSED=true break fi # Show container status for debugging if [ $((i % 10)) -eq 0 ]; then echo "๐Ÿ“Š Container status: $(docker inspect $NEW_CONTAINER --format='{{.State.Status}}' 2>/dev/null || echo 'unknown')" echo "๐Ÿ“Š Health status: $HEALTH" echo "๐Ÿ“Š Testing from inside container:" docker exec $NEW_CONTAINER curl -f -s --max-time 2 http://localhost:3000/api/health 2>&1 | head -1 || echo "Container HTTP test failed" docker compose -f $COMPOSE_FILE logs --tail=5 portfolio 2>/dev/null || true fi fi echo "โณ Waiting... ($i/90)" sleep 2 done # Final verification: Check Docker health status (most reliable) NEW_CONTAINER=$(docker ps -q -f name=$CONTAINER_NAME) if [ ! -z "$NEW_CONTAINER" ]; then FINAL_HEALTH=$(docker inspect $NEW_CONTAINER --format='{{.State.Health.Status}}' 2>/dev/null || echo "unknown") if [ "$FINAL_HEALTH" == "healthy" ]; then echo "โœ… Final verification: Container is healthy!" HEALTH_CHECK_PASSED=true fi fi # Verify new container is working if [ "$HEALTH_CHECK_PASSED" != "true" ]; then echo "โŒ New container failed health check!" echo "๐Ÿ“‹ Container logs:" docker compose -f $COMPOSE_FILE logs --tail=100 portfolio echo "๐Ÿ“‹ Container inspect:" docker inspect $NEW_CONTAINER 2>/dev/null || echo "Container not found" echo "๐Ÿ“‹ Testing health endpoint from inside container:" docker exec $NEW_CONTAINER curl -v http://localhost:3000/api/health 2>&1 || echo "Container HTTP test failed" echo "๐Ÿ“‹ Testing health endpoint from host:" curl -v http://localhost:$HEALTH_PORT/api/health 2>&1 || echo "Host HTTP test failed" 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 || '' }} N8N_API_KEY: ${{ vars.N8N_API_KEY || '' }} - 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"