From 2f8aeceb655e5dcde37dc7d12942cd6e15a4dac1 Mon Sep 17 00:00:00 2001 From: CheChe903 Date: Sat, 30 May 2026 15:40:30 +0900 Subject: [PATCH 1/2] chore: switch deployment to Docker Compose --- .github/workflows/dev-cd.yml | 48 +++-- .github/workflows/prod-cd.yml | 48 +++-- .gitignore | 7 +- Dockerfile | 2 +- docker-compose.yml | 9 + .../deployment-and-runtime-config.md | 186 ++++++++++++++++++ src/main/resources/application-dev.yml | 22 ++- src/main/resources/application-local.yml | 18 ++ src/main/resources/application-private.yml | 17 -- src/main/resources/application-prod.yml | 22 ++- src/main/resources/application.yml | 3 +- 11 files changed, 333 insertions(+), 49 deletions(-) create mode 100644 docker-compose.yml create mode 100644 docs/architecture/deployment-and-runtime-config.md delete mode 100644 src/main/resources/application-private.yml diff --git a/.github/workflows/dev-cd.yml b/.github/workflows/dev-cd.yml index f1f96ce..4f75c93 100644 --- a/.github/workflows/dev-cd.yml +++ b/.github/workflows/dev-cd.yml @@ -8,7 +8,9 @@ on: jobs: build-and-push: - runs-on: self-hosted + runs-on: [self-hosted, crew-wiki-dev] + env: + IMAGE_NAME: ${{ secrets.DOCKERHUB_USERNAME }}/crew-wiki steps: - name: Checkout source uses: actions/checkout@v4 @@ -43,21 +45,30 @@ jobs: file: ./Dockerfile push: true tags: | - ${{ secrets.DOCKERHUB_USERNAME }}/crew-wiki:latest - ${{ secrets.DOCKERHUB_USERNAME }}/crew-wiki:${{ github.sha }} + ${{ env.IMAGE_NAME }}:dev-latest + ${{ env.IMAGE_NAME }}:dev-${{ github.sha }} deploy: needs: build-and-push - runs-on: self-hosted + runs-on: [self-hosted, crew-wiki-dev] + env: + IMAGE_NAME: ${{ secrets.DOCKERHUB_USERNAME }}/crew-wiki:dev-latest steps: + - name: Checkout source + uses: actions/checkout@v4 + - name: Deploy to Server run: | set -euo pipefail BASE_DIR=/home/ubuntu/crew-wiki - IMG=${{ secrets.DOCKERHUB_USERNAME }}/crew-wiki:latest mkdir -p "$BASE_DIR" + cp docker-compose.yml "$BASE_DIR/docker-compose.yml" + cat > "$BASE_DIR/.env" <<'EOF' + IMAGE_NAME=${{ env.IMAGE_NAME }} + CONTAINER_NAME=crew-wiki + HOST_PORT=${{ secrets.DEV_HOST_PORT || '8080' }} SPRING_PROFILES_ACTIVE=dev LOG_PATH=${{ secrets.LOG_PATH }} JWT_SECRET_KEY=${{ secrets.JWT_SECRET_KEY }} @@ -65,16 +76,29 @@ jobs: DEV_DB_URL=${{ secrets.DEV_DB_URL }} DEV_DB_USERNAME=${{ secrets.DEV_DB_USERNAME }} DEV_DB_PASSWORD=${{ secrets.DEV_DB_PASSWORD }} + CORS_ALLOWED_ORIGINS=${{ secrets.DEV_CORS_ALLOWED_ORIGINS || 'http://localhost:3000, https://dev.crew-wiki.site, https://dev.api.crew-wiki.site' }} + SWAGGER_SERVER_URL=${{ secrets.DEV_SWAGGER_SERVER_URL || 'https://dev.api.crew-wiki.site' }} AWS_CREDENTIALS_ACCESS_KEY=${{ secrets.AWS_CREDENTIALS_ACCESS_KEY }} AWS_CREDENTIALS_SECRET_KEY=${{ secrets.AWS_CREDENTIALS_SECRET_KEY }} S3_BUCKET=${{ secrets.S3_BUCKET }} EOF - docker rm -f crew-wiki || true - docker pull "$IMG" + cd "$BASE_DIR" + if docker compose version >/dev/null 2>&1; then + COMPOSE="docker compose" + elif docker-compose --version >/dev/null 2>&1; then + COMPOSE="docker-compose" + else + echo "Docker Compose is not installed" + exit 1 + fi + + COMPOSE_PROJECT=$(docker inspect "$CONTAINER_NAME" \ + --format '{{ index .Config.Labels "com.docker.compose.project" }}' 2>/dev/null || true) + if [ -z "$COMPOSE_PROJECT" ] && docker ps -a --format '{{.Names}}' | grep -qx "$CONTAINER_NAME"; then + docker rm -f "$CONTAINER_NAME" + fi - docker run -d \ - --name crew-wiki \ - --env-file "$BASE_DIR/.env" \ - -p 8080:8080 \ - "$IMG" + $COMPOSE --env-file "$BASE_DIR/.env" pull app + $COMPOSE --env-file "$BASE_DIR/.env" up -d --remove-orphans + $COMPOSE --env-file "$BASE_DIR/.env" ps diff --git a/.github/workflows/prod-cd.yml b/.github/workflows/prod-cd.yml index 8def94f..94bbb21 100644 --- a/.github/workflows/prod-cd.yml +++ b/.github/workflows/prod-cd.yml @@ -8,7 +8,9 @@ on: jobs: build-and-push: - runs-on: self-hosted + runs-on: [self-hosted, crew-wiki-prod] + env: + IMAGE_NAME: ${{ secrets.DOCKERHUB_USERNAME }}/crew-wiki steps: - name: Checkout source uses: actions/checkout@v4 @@ -43,21 +45,30 @@ jobs: file: ./Dockerfile push: true tags: | - ${{ secrets.DOCKERHUB_USERNAME }}/crew-wiki:latest - ${{ secrets.DOCKERHUB_USERNAME }}/crew-wiki:${{ github.sha }} + ${{ env.IMAGE_NAME }}:prod-latest + ${{ env.IMAGE_NAME }}:prod-${{ github.sha }} deploy: needs: build-and-push - runs-on: self-hosted + runs-on: [self-hosted, crew-wiki-prod] + env: + IMAGE_NAME: ${{ secrets.DOCKERHUB_USERNAME }}/crew-wiki:prod-latest steps: + - name: Checkout source + uses: actions/checkout@v4 + - name: Deploy to Server run: | set -euo pipefail BASE_DIR=/home/ubuntu/crew-wiki - IMG=${{ secrets.DOCKERHUB_USERNAME }}/crew-wiki:latest mkdir -p "$BASE_DIR" + cp docker-compose.yml "$BASE_DIR/docker-compose.yml" + cat > "$BASE_DIR/.env" <<'EOF' + IMAGE_NAME=${{ env.IMAGE_NAME }} + CONTAINER_NAME=crew-wiki + HOST_PORT=${{ secrets.PROD_HOST_PORT || '8080' }} SPRING_PROFILES_ACTIVE=prod LOG_PATH=${{ secrets.LOG_PATH }} JWT_SECRET_KEY=${{ secrets.JWT_SECRET_KEY }} @@ -65,16 +76,29 @@ jobs: PROD_DB_URL=${{ secrets.PROD_DB_URL }} PROD_DB_USERNAME=${{ secrets.PROD_DB_USERNAME }} PROD_DB_PASSWORD=${{ secrets.PROD_DB_PASSWORD }} + CORS_ALLOWED_ORIGINS=${{ secrets.PROD_CORS_ALLOWED_ORIGINS || 'https://crew-wiki.site, https://api.crew-wiki.site' }} + SWAGGER_SERVER_URL=${{ secrets.PROD_SWAGGER_SERVER_URL || 'https://api.crew-wiki.site' }} AWS_CREDENTIALS_ACCESS_KEY=${{ secrets.AWS_CREDENTIALS_ACCESS_KEY }} AWS_CREDENTIALS_SECRET_KEY=${{ secrets.AWS_CREDENTIALS_SECRET_KEY }} S3_BUCKET=${{ secrets.S3_BUCKET }} EOF - docker rm -f crew-wiki || true - docker pull "$IMG" + cd "$BASE_DIR" + if docker compose version >/dev/null 2>&1; then + COMPOSE="docker compose" + elif docker-compose --version >/dev/null 2>&1; then + COMPOSE="docker-compose" + else + echo "Docker Compose is not installed" + exit 1 + fi + + COMPOSE_PROJECT=$(docker inspect "$CONTAINER_NAME" \ + --format '{{ index .Config.Labels "com.docker.compose.project" }}' 2>/dev/null || true) + if [ -z "$COMPOSE_PROJECT" ] && docker ps -a --format '{{.Names}}' | grep -qx "$CONTAINER_NAME"; then + docker rm -f "$CONTAINER_NAME" + fi - docker run -d \ - --name crew-wiki \ - --env-file "$BASE_DIR/.env" \ - -p 8080:8080 \ - "$IMG" + $COMPOSE --env-file "$BASE_DIR/.env" pull app + $COMPOSE --env-file "$BASE_DIR/.env" up -d --remove-orphans + $COMPOSE --env-file "$BASE_DIR/.env" ps diff --git a/.gitignore b/.gitignore index d964430..1242208 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,9 @@ out/ ### VS Code ### .vscode/ -src/main/resources/application-private.yml +application-*.yml +!src/main/resources/application-dev.yml +!src/main/resources/application-local.yml +!src/main/resources/application-prod.yml +.env +.env.* diff --git a/Dockerfile b/Dockerfile index c0aa37f..94df447 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,4 +6,4 @@ COPY build/libs/*.jar app.jar EXPOSE 8080 -ENTRYPOINT ["java", "-Dspring.profiles.active=${SPRING_PROFILES_ACTIVE}", "-jar", "app.jar"] +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..0e9f5d4 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,9 @@ +services: + app: + image: ${IMAGE_NAME:?IMAGE_NAME is required} + container_name: ${CONTAINER_NAME:-crew-wiki} + restart: unless-stopped + env_file: + - .env + ports: + - "${HOST_PORT:-8080}:8080" diff --git a/docs/architecture/deployment-and-runtime-config.md b/docs/architecture/deployment-and-runtime-config.md new file mode 100644 index 0000000..9488e52 --- /dev/null +++ b/docs/architecture/deployment-and-runtime-config.md @@ -0,0 +1,186 @@ +# Docker Compose 기반 배포와 런타임 설정 주입 + +## 개요 + +기존 dev/prod 배포는 GitHub Actions self-hosted runner에서 이미지를 빌드한 뒤 `docker run` 명령으로 컨테이너를 직접 교체했다. +이 방식은 서버에 배포 상태를 설명하는 `docker-compose.yml`이 없어서 컨테이너 이름, 포트, 환경변수, 재시작 정책이 워크플로 스크립트 안에 흩어지는 문제가 있었다. + +또한 `application-private.yml`이라는 파일이 실제 비밀값 파일처럼 보였지만, 내용은 환경변수 placeholder였다. +이 이름은 "private 값은 어디에 넣어야 하는가"라는 혼란을 만들었다. + +현재 배포 구조는 dev/prod 모두 `docker-compose.yml`을 기준으로 구성한다. + +## 배포 구조 + +dev/prod 배포는 `docker-compose.yml` 기반으로 수행한다. + +GitHub Actions는 다음 책임만 가진다. + +1. 애플리케이션 JAR 빌드 +2. Docker 이미지 빌드 및 Docker Hub push +3. 배포 대상 디렉터리에 `docker-compose.yml` 복사 +4. GitHub Secrets 값을 이용해 서버의 `.env` 생성 +5. `docker compose pull app` 및 `docker compose up -d --remove-orphans` 실행 + +Spring 설정은 프로필별 설정 파일에 직접 둔다. + +- `application.yml`: 활성 프로필 기본값만 정의 +- `application-local.yml`: 로컬 실행용 H2, 더미 JWT/AWS 값 정의 +- `application-dev.yml`: dev DB/JWT/AWS placeholder 정의 +- `application-prod.yml`: prod DB/JWT/AWS placeholder 정의 + +`application-private.yml`은 삭제한다. + +## 설정 값 주입 흐름 + +비밀값은 Git 저장소에 커밋하지 않는다. + +배포 시 값은 아래 순서로 전달된다. + +```text +GitHub Secrets +-> GitHub Actions가 서버에 생성하는 .env +-> Docker Compose env_file +-> 컨테이너 환경변수 +-> Spring Boot placeholder 해석 +``` + +예를 들어 `application-prod.yml`의 값은 다음처럼 둔다. + +```yml +security: + jwt: + token: + secret-key: ${JWT_SECRET_KEY} +``` + +그리고 배포 시 `.env`에는 실제 값을 넣는다. + +```env +JWT_SECRET_KEY=actual-secret-value +``` + +Docker가 secret을 자동으로 만들어 주는 것은 아니다. +Docker Compose는 `.env`에 적힌 값을 컨테이너 환경변수로 전달하고, Spring Boot가 그 값을 읽는다. + +## 이미지 태그 정책 + +dev와 prod가 같은 `latest` 태그를 공유하지 않는다. + +- dev: `crew-wiki:dev-latest`, `crew-wiki:dev-${GITHUB_SHA}` +- prod: `crew-wiki:prod-latest`, `crew-wiki:prod-${GITHUB_SHA}` + +이렇게 분리해 develop 브랜치 배포가 main 브랜치 배포 이미지를 덮어쓰지 않도록 한다. + +## 서버 디렉터리와 포트 + +dev/prod는 서로 다른 인스턴스에서 실행된다. +따라서 각 인스턴스 내부의 배포 디렉터리, 컨테이너 이름, 기본 호스트 포트는 동일하게 사용한다. + +- 배포 디렉터리: `/home/ubuntu/crew-wiki` +- 컨테이너 이름: `crew-wiki` +- 기본 호스트 포트: `8080` + +같은 인스턴스 안에서 dev/prod를 동시에 실행하는 구조로 바뀌면 그때 디렉터리, 컨테이너 이름, 포트를 환경별로 분리한다. + +## Runner 라우팅 + +dev/prod 인스턴스가 다르면 GitHub Actions self-hosted runner도 환경별로 명확히 라우팅되어야 한다. + +워크플로가 `runs-on: self-hosted`만 사용하면 GitHub가 접근 가능한 self-hosted runner 중 하나를 선택할 수 있다. +dev runner와 prod runner가 같은 저장소에 모두 등록되어 있고 추가 label이 없다면 dev 배포가 prod 인스턴스에서 실행되거나, 반대로 prod 배포가 dev 인스턴스에서 실행될 위험이 있다. + +실제 점검 중 dev 인스턴스의 runner에서 `prod-cd.yml`이 실행된 흔적이 확인되었다. +따라서 워크플로는 generic `self-hosted`가 아니라 환경별 label을 반드시 지정한다. + +runner는 인스턴스마다 하나씩 둔다. + +- dev 인스턴스 runner name: `crew-wiki-dev` +- dev 인스턴스 runner label: `crew-wiki-dev` +- prod 인스턴스 runner name: `crew-wiki-prod` +- prod 인스턴스 runner label: `crew-wiki-prod` + +워크플로의 build/deploy job은 다음처럼 환경별 label을 지정한다. + +```yml +# dev-cd.yml +runs-on: [self-hosted, crew-wiki-dev] + +# prod-cd.yml +runs-on: [self-hosted, crew-wiki-prod] +``` + +runner 재설정 절차는 다음 순서로 진행한다. + +1. GitHub repository settings에서 기존 runner를 확인한다. +2. dev 인스턴스의 기존 runner는 `crew-wiki-dev` 이름과 label로 재등록한다. +3. prod 인스턴스에는 새 runner를 설치하고 `crew-wiki-prod` 이름과 label로 등록한다. +4. 각 인스턴스에서 runner service가 `active (running)`인지 확인한다. +5. GitHub repository settings의 runner 목록에서 dev/prod runner가 각각 Online인지 확인한다. + +runner 등록 명령은 GitHub UI에서 발급되는 registration token이 필요하다. +토큰은 짧은 시간만 유효하므로 runner 재등록 시점에 GitHub UI에서 새로 발급받는다. + +등록 명령 예시는 다음과 같다. + +```sh +./config.sh \ + --url https://github.com/Crew-Wiki/backend \ + --token \ + --name crew-wiki-dev \ + --labels crew-wiki-dev \ + --unattended \ + --replace + +sudo ./svc.sh install +sudo ./svc.sh start +``` + +prod 인스턴스에서는 `--name crew-wiki-prod`, `--labels crew-wiki-prod`를 사용한다. + +## 필요한 GitHub Secrets + +공통: + +- `DOCKERHUB_USERNAME` +- `DOCKERHUB_TOKEN` +- `JWT_SECRET_KEY` +- `JWT_SECRET_EXPIRE_LENGTH` +- `AWS_CREDENTIALS_ACCESS_KEY` +- `AWS_CREDENTIALS_SECRET_KEY` +- `S3_BUCKET` + +dev: + +- `DEV_DB_URL` +- `DEV_DB_USERNAME` +- `DEV_DB_PASSWORD` +- `DEV_HOST_PORT` optional +- `DEV_CORS_ALLOWED_ORIGINS` optional +- `DEV_SWAGGER_SERVER_URL` optional + +prod: + +- `PROD_DB_URL` +- `PROD_DB_USERNAME` +- `PROD_DB_PASSWORD` +- `PROD_HOST_PORT` optional +- `PROD_CORS_ALLOWED_ORIGINS` optional +- `PROD_SWAGGER_SERVER_URL` optional + +GitHub Secrets 값에는 YAML key를 포함하지 않고 실제 값만 저장한다. +예를 들어 `PROD_DB_URL` secret value는 `jdbc:mysql://...` 형식이어야 하며, `url: jdbc:mysql://...`처럼 설정 파일 조각을 넣지 않는다. + +## 운영상 이점 + +- 배포 상태가 `docker-compose.yml`에 명시된다. +- dev/prod 이미지 태그 충돌을 피한다. +- 실제 비밀값은 Git 저장소와 Docker 이미지에 들어가지 않는다. +- `application-private.yml` 이름에서 오는 혼란이 사라진다. + +## 주의점 + +- self-hosted runner가 실제 배포 서버와 같은 인스턴스라는 가정에서는 SSH가 필요 없다. +- runner와 배포 서버가 다르면 GitHub Actions에서 SSH로 서버에 접속해 `docker-compose.yml`과 `.env`를 배치해야 한다. +- 서버에는 Docker Compose v2(`docker compose`) 또는 v1(`docker-compose`)가 설치되어 있어야 한다. +- 기존 `docker run` 방식으로 생성된 `crew-wiki` 컨테이너는 compose label이 없으므로, 최초 compose 전환 배포 때 제거한 뒤 compose가 다시 생성한다. diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 773f800..59a97f5 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -16,7 +16,25 @@ spring: dialect: org.hibernate.dialect.MySQL8Dialect cors: - allowed-origins: http://localhost:3000, https://dev.crew-wiki.site, https://dev.api.crew-wiki.site + allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:3000, https://dev.crew-wiki.site, https://dev.api.crew-wiki.site} swagger: - server-url: https://dev.api.crew-wiki.site + server-url: ${SWAGGER_SERVER_URL:https://dev.api.crew-wiki.site} + +security: + jwt: + token: + secret-key: ${JWT_SECRET_KEY} + expire-length: ${JWT_SECRET_EXPIRE_LENGTH} + +cloud: + aws: + credentials: + access-key: ${AWS_CREDENTIALS_ACCESS_KEY} + secret-key: ${AWS_CREDENTIALS_SECRET_KEY} + s3: + bucket: ${S3_BUCKET} + region: + static: ap-northeast-2 + stack: + auto: false diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 100b669..d401495 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -22,3 +22,21 @@ cors: swagger: server-url: https://dev.api.crew-wiki.site + +security: + jwt: + token: + secret-key: local-development-secret-key-local-development-secret-key + expire-length: 3600000 + +cloud: + aws: + credentials: + access-key: local + secret-key: local + s3: + bucket: local + region: + static: ap-northeast-2 + stack: + auto: false diff --git a/src/main/resources/application-private.yml b/src/main/resources/application-private.yml deleted file mode 100644 index fa7b38c..0000000 --- a/src/main/resources/application-private.yml +++ /dev/null @@ -1,17 +0,0 @@ -security: - jwt: - token: - secret-key: ${JWT_SECRET_KEY} - expire-length: ${JWT_SECRET_EXPIRE_LENGTH} - -cloud: - aws: - credentials: - access-key: ${AWS_CREDENTIALS_ACCESS_KEY} - secret-key: ${AWS_CREDENTIALS_SECRET_KEY} - s3: - bucket: ${S3_BUCKET} - region: - static: ap-northeast-2 - stack: - auto: false diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index fadf8d7..b00c30f 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -16,7 +16,25 @@ spring: dialect: org.hibernate.dialect.MySQL8Dialect cors: - allowed-origins: https://crew-wiki.site, https://api.crew-wiki.site + allowed-origins: ${CORS_ALLOWED_ORIGINS:https://crew-wiki.site, https://api.crew-wiki.site} swagger: - server-url: https://api.crew-wiki.site + server-url: ${SWAGGER_SERVER_URL:https://api.crew-wiki.site} + +security: + jwt: + token: + secret-key: ${JWT_SECRET_KEY} + expire-length: ${JWT_SECRET_EXPIRE_LENGTH} + +cloud: + aws: + credentials: + access-key: ${AWS_CREDENTIALS_ACCESS_KEY} + secret-key: ${AWS_CREDENTIALS_SECRET_KEY} + s3: + bucket: ${S3_BUCKET} + region: + static: ap-northeast-2 + stack: + auto: false diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index cc83341..b251e2d 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,4 +1,3 @@ spring: profiles: - include: private - active: local # 기본 로컬 실행 프로필 + active: ${SPRING_PROFILES_ACTIVE:local} From 256dec096055cc529d32311e31bdcb8cc5c0664f Mon Sep 17 00:00:00 2001 From: CheChe903 Date: Sat, 30 May 2026 16:02:27 +0900 Subject: [PATCH 2/2] fix: define deployment container name --- .github/workflows/dev-cd.yml | 1 + .github/workflows/prod-cd.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/dev-cd.yml b/.github/workflows/dev-cd.yml index 4f75c93..61bfa3a 100644 --- a/.github/workflows/dev-cd.yml +++ b/.github/workflows/dev-cd.yml @@ -61,6 +61,7 @@ jobs: run: | set -euo pipefail BASE_DIR=/home/ubuntu/crew-wiki + CONTAINER_NAME=crew-wiki mkdir -p "$BASE_DIR" cp docker-compose.yml "$BASE_DIR/docker-compose.yml" diff --git a/.github/workflows/prod-cd.yml b/.github/workflows/prod-cd.yml index 94bbb21..483abd1 100644 --- a/.github/workflows/prod-cd.yml +++ b/.github/workflows/prod-cd.yml @@ -61,6 +61,7 @@ jobs: run: | set -euo pipefail BASE_DIR=/home/ubuntu/crew-wiki + CONTAINER_NAME=crew-wiki mkdir -p "$BASE_DIR" cp docker-compose.yml "$BASE_DIR/docker-compose.yml"