feat: gitignore env.local 허용 #12
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 | |
| continue-on-error: true | |
| # ======================================== | |
| # 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: | | |
| set -e | |
| mkdir -p ~/.ssh | |
| # Write SSH private key (handle \n in secret) | |
| echo "${{ secrets.PROD_SSH_PRIVATE_KEY }}" | tr -d '\r' > ~/.ssh/my-key.pem | |
| chmod 600 ~/.ssh/my-key.pem | |
| # Verify key file was created | |
| if [ ! -s ~/.ssh/my-key.pem ]; then | |
| echo "Error: SSH key file is empty" | |
| exit 1 | |
| fi | |
| echo "SSH key written successfully ($(wc -l < ~/.ssh/my-key.pem) lines)" | |
| # Add server to known_hosts | |
| echo "Adding ${{ secrets.PROD_SERVER_HOST }}:${{ secrets.PROD_SSH_PORT }} to known_hosts..." | |
| ssh-keyscan -p ${{ secrets.PROD_SSH_PORT }} -H ${{ secrets.PROD_SERVER_HOST }} >> ~/.ssh/known_hosts 2>&1 || { | |
| echo "Warning: ssh-keyscan failed, but continuing..." | |
| echo "You may need to manually verify the host key on first connection" | |
| } | |
| # Configure SSH keep-alive and disable strict host checking for automation | |
| cat >> ~/.ssh/config << 'EOF' | |
| Host * | |
| ServerAliveInterval 60 | |
| ServerAliveCountMax 3 | |
| StrictHostKeyChecking no | |
| UserKnownHostsFile ~/.ssh/known_hosts | |
| EOF | |
| echo "✓ SSH configuration complete" | |
| - name: Create app directory on server | |
| run: | | |
| ssh -i ~/.ssh/my-key.pem -p ${{ secrets.PROD_SSH_PORT }} ${{ secrets.PROD_SERVER_USER }}@${{ secrets.PROD_SERVER_HOST }} "mkdir -p ~/devnogi-react" | |
| - name: Copy deployment files to server | |
| run: | | |
| scp -i ~/.ssh/my-key.pem -P ${{ secrets.PROD_SSH_PORT }} docker-compose-prod.yaml ${{ secrets.PROD_SERVER_USER }}@${{ secrets.PROD_SERVER_HOST }}:~/devnogi-react/ | |
| - name: Deploy to Production Server | |
| run: | | |
| ssh -i ~/.ssh/my-key.pem -p ${{ secrets.PROD_SSH_PORT }} ${{ secrets.PROD_SERVER_USER }}@${{ secrets.PROD_SERVER_HOST }} << 'EOF' | |
| cd ~/devnogi-react | |
| # 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 -p ${{ secrets.PROD_SSH_PORT }} ${{ 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. 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 | |
| # 3. Check Next.js health endpoint directly | |
| echo "Checking health endpoint (port 3010)..." | |
| for i in {1..30}; do | |
| HEALTH_RESPONSE=$(curl -s http://localhost:3010/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 | |
| exit 1 | |
| fi | |
| done | |
| # 4. Smoke test: Check if Next.js responds | |
| echo "Running smoke test..." | |
| HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3010/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" | |
| EOF | |
| - name: Display deployment info | |
| if: success() | |
| run: | | |
| echo "✅ Production deployment successful!" | |
| echo "🐳 Image: ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }}:prod" | |
| echo "📦 Commit: ${{ github.sha }}" | |
| echo "🔗 Next.js App: http://${{ secrets.PROD_SERVER_HOST }}:3010" | |
| echo "⚠️ Configure your host Nginx to proxy to localhost:3010" | |
| 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 ~/devnogi-react" | |
| 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" |