Repository navigation
Deploy Admin to Prod (EC2 + Docker Compose) #22
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: Deploy Admin to Prod (EC2 + Docker Compose) | |
| on: | |
| push: | |
| branches: | |
| - main | |
| workflow_dispatch: | |
| inputs: | |
| deploy_mode: | |
| description: 앱을 배포하거나 이전 검증을 위해 이미지만 빌드·업로드합니다 | |
| type: choice | |
| required: true | |
| default: full | |
| options: | |
| - full | |
| - image-only | |
| concurrency: | |
| group: saerok-admin-prod-deploy | |
| cancel-in-progress: false | |
| jobs: | |
| deploy: | |
| if: ${{ github.event_name == 'workflow_dispatch' || vars.PROD_ARM64_AUTODEPLOY == 'true' }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| environment: prod | |
| env: | |
| DEPLOY_MODE: ${{ inputs.deploy_mode || 'full' }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| # (옵션) BE와 동일 스타일 - 리포지토리명 소문자/메타 태그 | |
| - name: Normalize image repo (lowercase) | |
| id: normalize | |
| run: echo "image_repo=ghcr.io/${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT" | |
| - name: Set up ARM64 emulation | |
| uses: docker/setup-qemu-action@v3 | |
| with: | |
| platforms: arm64 | |
| - name: Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: GHCR login | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ secrets.GHCR_USERNAME }} | |
| password: ${{ secrets.GHCR_TOKEN }} | |
| - name: Meta (tags/labels) | |
| id: meta | |
| uses: docker/metadata-action@v5 | |
| with: | |
| images: ${{ steps.normalize.outputs.image_repo }} | |
| tags: | | |
| type=raw,value=prod-latest,enable=${{ env.DEPLOY_MODE == 'full' }} | |
| type=sha,format=long,prefix=prod-,enable=${{ env.DEPLOY_MODE == 'full' }} | |
| type=raw,value=migration-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }},enable=${{ env.DEPLOY_MODE == 'image-only' }} | |
| - name: Build & Push | |
| id: build | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| file: ./Dockerfile | |
| platforms: linux/amd64,linux/arm64 | |
| push: true | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| cache-from: type=registry,ref=${{ steps.normalize.outputs.image_repo }}:buildcache | |
| cache-to: type=registry,ref=${{ steps.normalize.outputs.image_repo }}:buildcache,mode=max | |
| - name: Copy compose files to EC2 | |
| if: ${{ env.DEPLOY_MODE == 'full' }} | |
| uses: appleboy/scp-action@v0.1.7 | |
| with: | |
| host: ${{ secrets.EC2_HOST }} | |
| username: ubuntu | |
| key: ${{ secrets.EC2_SSH_PRIVATE_KEY }} | |
| source: "deploy/docker-compose.yml,deploy/docker-compose.prod.yml" | |
| target: "~/saerok-admin" | |
| strip_components: 1 | |
| - name: Deploy on EC2 | |
| if: ${{ env.DEPLOY_MODE == 'full' }} | |
| uses: appleboy/ssh-action@v1 | |
| with: | |
| host: ${{ secrets.EC2_HOST }} | |
| username: ubuntu | |
| key: ${{ secrets.EC2_SSH_PRIVATE_KEY }} | |
| script: | | |
| set -euo pipefail | |
| cd ~/saerok-admin | |
| # 0) Docker & Compose must exist | |
| docker --version | |
| docker compose version | |
| # 1) GHCR login for pull | |
| echo "${{ secrets.GHCR_TOKEN }}" | docker login ghcr.io -u "${{ secrets.GHCR_USERNAME }}" --password-stdin | |
| # 2) Create ephemeral env file in RAM (owned by current user) and ensure cleanup | |
| # /run 은 tmpfs라 재부팅 시 날아감(의도된 동작) | |
| sudo install -d -m 0700 -o "$(id -un)" -g "$(id -gn)" /run/saerok-admin | |
| ENV_FILE="/run/saerok-admin/env.prod" | |
| export IMAGE_REF="${{ steps.normalize.outputs.image_repo }}@${{ steps.build.outputs.digest }}" | |
| umask 177 | |
| : > "$ENV_FILE" | |
| cleanup() { shred -u "$ENV_FILE" || rm -f "$ENV_FILE"; } | |
| trap cleanup EXIT | |
| add_kv() { printf '%s=%s\n' "$1" "$2" >> "$ENV_FILE"; } | |
| # 2-1) Runtime envs (prod) | |
| add_kv SAEROK_API_BASE_URL "${{ secrets.SAEROK_API_BASE_URL }}" | |
| add_kv SAEROK_API_PREFIX "${{ secrets.SAEROK_API_PREFIX }}" | |
| # Social login (admin 전용 redirect) | |
| add_kv KAKAO_CLIENT_ID "${{ secrets.KAKAO_CLIENT_ID }}" | |
| add_kv KAKAO_REDIRECT_URI "${{ secrets.KAKAO_REDIRECT_URI }}" | |
| add_kv APPLE_CLIENT_ID "${{ secrets.APPLE_CLIENT_ID }}" | |
| add_kv APPLE_REDIRECT_URI "${{ secrets.APPLE_REDIRECT_URI }}" | |
| add_kv UNSPLASH_ACCESS_KEY "${{ secrets.UNSPLASH_ACCESS_KEY }}" | |
| add_kv UNSPLASH_APP_NAME "${{ secrets.UNSPLASH_APP_NAME }}" | |
| # 3) Pull & Up with prod overlay (limits in prod.yml) | |
| docker compose -p saerok-admin -f docker-compose.yml -f docker-compose.prod.yml pull --quiet | |
| docker compose -p saerok-admin -f docker-compose.yml -f docker-compose.prod.yml up -d --force-recreate --quiet-pull | |
| # 4) Healthcheck: /login (HEAD OK) | |
| for i in {1..45}; do | |
| if curl -fsS --max-time 2 -I http://localhost:8081/login > /dev/null; then | |
| echo "✅ admin up" | |
| docker image prune -af --filter "until=24h" || true | |
| docker builder prune -af --filter "until=24h" || true | |
| exit 0 | |
| fi | |
| echo "⏳ ($i/45)"; sleep 3 | |
| done | |
| echo "❌ admin health check failed" | |
| # 세션이나 인증정보가 포함될 수 있는 원문 로그는 공개 Actions에 출력하지 않는다. | |
| # 상세 원인은 EC2에 접속해 권한이 있는 운영자가 직접 확인한다. | |
| docker inspect --format 'admin status={{.State.Status}} oom={{.State.OOMKilled}} restart={{.RestartCount}}' saerok-admin-prod || true | |
| exit 1 |