Skip to content

Deploy Admin to Prod (EC2 + Docker Compose) #22

Deploy Admin to Prod (EC2 + Docker Compose)

Deploy Admin to Prod (EC2 + Docker Compose) #22

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