OCI backend는 Docker Compose stack으로 Caddy, Spring Boot API, Redis, Redpanda를 실행합니다. 외부에 열린 포트는 Caddy 80/443뿐입니다. Spring(8080/8081), Redis(6379), Redpanda(9092)는 compose 내부 network에만 있습니다.
전체 릴리즈 순서는 release-publish-runbook.md를 따릅니다.
| 파일 | 역할 |
|---|---|
deploy/oci/04-install-docker.sh |
VM에 Docker Engine과 Compose plugin 설치 (1회) |
deploy/oci/05-deploy-compose-stack.sh |
로컬에서 실행하는 compose 배포 script |
deploy/oci/compose.yml |
runtime stack (caddy, readmates-api, redis, redpanda) |
deploy/oci/Caddyfile |
{$CADDY_SITE} → readmates-api:8080, 민감 header를 뺀 stdout 로그 |
deploy/oci/readmates-stack.service |
compose를 감싸는 systemd unit |
deploy/oci/watch-compose-post-deploy.sh |
배포 직후 watch helper |
VM 경로:
| 경로 | 내용 | 누가 만드나 |
|---|---|---|
/etc/readmates/readmates.env |
Spring 운영 환경 변수 | sync-config workflow |
/etc/readmates/caddy.env |
CADDY_SITE |
05-deploy-compose-stack.sh |
/opt/readmates/.env |
READMATES_SERVER_IMAGE |
05-deploy-compose-stack.sh |
/opt/readmates/compose.yml, Caddyfile |
stack 정의 | 05-deploy-compose-stack.sh |
-
VM에 Docker를 설치합니다.
ssh -i ~/.ssh/readmates_oci ubuntu@<vm-public-ip> 'bash -s' < deploy/oci/04-install-docker.sh
-
deploy key와
deploy사용자를 준비합니다: VM deploy key bootstrap. -
GitHub Actions
sync-configworkflow로/etc/readmates/readmates.env를 만듭니다: secrets management runbook. -
DB backup이
/var/backups/readmates/mysql에 쌓이도록 준비합니다: oci-mysql-heatwave.md.
05-deploy-compose-stack.sh는 readmates.env를 만들거나 secret을 렌더링하지 않습니다. App container의 OTLP endpoint는 compose가 http://tempo:4318/v1/traces로 고정합니다.
아래 중 하나라도 맞지 않으면 05-deploy-compose-stack.sh를 실행하지 않습니다.
- VM의
/etc/readmates/readmates.env가 있고 권한이600입니다. - VM에서 Docker와 Compose plugin이 동작합니다. (script도 확인합니다)
/var/backups/readmates/mysql에 최근 2일 안의*.sql.gz가 있습니다. (script도 확인하고, 없으면 멈춥니다)- GHCR package가 private이면 VM에서 registry login이 끝나 있습니다.
- legacy host
readmates-server와 hostcaddy를 중지/disable해도 되는 작업 시간과 권한이 있습니다. script가 compose 시작 전에 둘을 중지합니다. 되돌릴 준비가 없으면 먼저 Rollback을 준비합니다.
-
서버 검증을 끝냅니다.
./scripts/server-ci-check.sh ./server/gradlew -p server integrationTest
-
릴리즈라면
Deploy Server Image가 같은 tag에서 성공했는지 확인합니다. -
배포를 실행합니다.
READMATES_SERVER_IMAGE='ghcr.io/<owner>/<repo>/readmates-server:vX.Y.Z' \ VM_PUBLIC_IP='<vm-public-ip>' \ CADDY_SITE=api.example.com \ READMATES_APP_BASE_URL=https://app.example.com \ ./deploy/oci/05-deploy-compose-stack.sh
환경 변수:
| 이름 | 필수 | 설명 |
|---|---|---|
VM_PUBLIC_IP |
예 | VM 주소 |
CADDY_SITE |
예 | 직접 API HTTPS host, 예: api.example.com |
READMATES_SERVER_IMAGE |
릴리즈 시 예 | ghcr.io/로 시작하면 VM에서 pull합니다. 그 밖의 값(기본 readmates-server:local)이면 로컬에서 bootJar + docker build server 후 VM으로 전송합니다(전환 검증용). |
READMATES_APP_BASE_URL |
권장 | BFF smoke와 watch가 호출할 브라우저 origin. 생략하면 script 기본값을 씁니다. |
READMATES_SMOKE_AUTH_BASE_URL |
선택 | watch의 OAuth redirect_uri 기준 origin. 기본은 READMATES_APP_BASE_URL |
SSH_KEY |
선택 | 기본 ~/.ssh/readmates_oci |
REMOTE_USER |
선택 | 기본 ubuntu |
READMATES_RUN_POST_DEPLOY_WATCH |
선택 | 기본 true. false면 watch를 건너뜁니다. |
완료 확인:
- script가
Compose stack 배포 완료까지 성공합니다. readmates-stacksystemd unit이 active입니다.- compose
readmates-apihealth, BFF auth smoke, post-deploy watch가 통과합니다. - Redis/Kafka 기능 flag는 별도 rollout 전까지 바꾸지 않습니다.
앱 배포는 --remove-orphans를 쓰지 않습니다. 같은 VM에서 관측 stack(Prometheus/Grafana/Alertmanager)이 같은 Compose project에 붙어 있을 수 있기 때문입니다. 필요 없는 legacy container는 docker compose ps -a와 현재 compose 파일을 대조한 뒤 운영자가 직접 정리합니다.
script는 VM의 /var/log/readmates/deploy-attempts.jsonl에 단계별 기록을 남깁니다. 자동 rollback이나 재시도 트리거가 아니라, 운영자가 실패 단계를 보고 판단하기 위한 기록입니다. 상태 모델과 단계별 대응은 Deploy Attempts를 따릅니다.
- 기본 형식(
READMATES_LEDGER_FORMAT=both)은 이벤트마다 두 줄을 씁니다.- legacy 줄:
attemptId,event,status,stage,at,durationSeconds, 선택detail(문자열) - JSON 줄:
ts,stage,event,status,detail(key/value 객체),attemptId,durationSeconds
- legacy 줄:
legacy또는json으로 한 형식만 쓸 수 있습니다. 경로는READMATES_DEPLOY_LEDGER로 바꿀 수 있습니다.detail에는image=...,imageId=...,exitCode=...같은 짧은 값만 들어갑니다. 외부에 공유하기 전 image 이름이 public-safe인지 확인합니다.- env 파일 내용, DB host, password, secret, cookie, token, 요청/응답 전문, 실제 멤버 데이터는 기록하지 않습니다.
ssh -i ~/.ssh/readmates_oci ubuntu@<vm-public-ip> 'cd /opt/readmates && sudo docker compose -f compose.yml exec -T readmates-api /app/bin/readmates-http-get 127.0.0.1 8080 /internal/health'
curl -fsS https://app.example.com/api/bff/api/auth/me
READMATES_SMOKE_BASE_URL=https://app.example.com \
READMATES_SMOKE_AUTH_BASE_URL=https://app.example.com \
./scripts/smoke-production-integrations.shBFF auth smoke가 통과하면 script가 watch-compose-post-deploy.sh를 실행합니다. watch는 readmates-stack/compose health, BFF auth smoke, OAuth redirect smoke, 최근 backend log의 ERROR/exception 패턴을 확인합니다.
watch 실패는 배포 실패로 기록되지만 자동 rollback은 하지 않습니다. 운영자가 ledger와 log를 보고 이전 image rollback이나 env 조사를 고릅니다. 수동 실행은 Post-deploy Watch를 봅니다.
/opt/readmates/.env의 READMATES_SERVER_IMAGE를 이전 tag로 바꾸고 readmates-api만 다시 올립니다.
ssh -i ~/.ssh/readmates_oci ubuntu@<vm-public-ip> 'cd /opt/readmates && printf "READMATES_SERVER_IMAGE=%s\n" "ghcr.io/<owner>/<repo>/readmates-server:<previous-tag>" | sudo tee .env >/dev/null && sudo docker compose -f compose.yml up -d readmates-api'migration이 있었던 릴리즈는 이전 image가 새 schema와 호환되는지 먼저 확인합니다. schema는 되돌리지 않습니다.
전환 실패나 장애 대응용입니다.
-
compose stack을 멈춥니다.
ssh -i ~/.ssh/readmates_oci ubuntu@<vm-public-ip> 'sudo systemctl stop readmates-stack || true && sudo systemctl disable readmates-stack || true'
-
검증된 JAR가 VM에 있는지 봅니다.
ssh -i ~/.ssh/readmates_oci ubuntu@<vm-public-ip> 'sudo test -s /opt/readmates/readmates-server.jar && sudo ls -lh /opt/readmates/readmates-server.jar'
-
없으면 로컬에서 다시 올립니다.
./server/gradlew -p server bootJar VM_PUBLIC_IP='<vm-public-ip>' ./deploy/oci/03-deploy.sh -
legacy host 서비스를 Spring → Caddy 순서로 올립니다.
ssh -i ~/.ssh/readmates_oci ubuntu@<vm-public-ip> 'sudo systemctl enable --now readmates-server && sudo systemctl enable --now caddy && sudo systemctl status readmates-server --no-pager && sudo systemctl status caddy --no-pager'
readmates-server systemd unit은 저장소에 없습니다. legacy 경로를 쓰려면 VM에 unit이 남아 있어야 합니다.
compose 안의 endpoint는 redis://redis:6379, redpanda:9092이며 compose가 container에 주입합니다. Redis를 먼저 켜고, 안정된 뒤 알림/Kafka를 따로 켭니다. 값은 sync-config로 반영하고 readmates-api를 재시작해야 적용됩니다.
Redis 순서:
READMATES_REDIS_ENABLED=true
READMATES_RATE_LIMIT_ENABLED=true
READMATES_AUTH_SESSION_CACHE_ENABLED=true
READMATES_PUBLIC_CACHE_ENABLED=true
READMATES_NOTES_CACHE_ENABLED=trueKafka 순서:
READMATES_NOTIFICATIONS_ENABLED=true
READMATES_KAFKA_ENABLED=true