Docker 2026.09.06 8 min read

main 변경분을 운영 이미지로 배포하기까지

main 변경을 일회용 MySQL에서 시험하고 커밋 태그와 digest를 기록해 배포했다. Worker 인증, Secret, NFS와 외부 readiness를 확인한 순서를 정리했다.

프로젝트 막바지에는 main에 변경이 자주 들어왔다. 매번 소스를 받은 뒤 일회용 DB에서 테스트하고, 이미지를 만들어 레지스트리에 올리고, 스택을 갱신했다. 배포 후 외부 readiness 응답까지 확인하는 순서로 작업했다.

이번 프로젝트에서는 CI/CD를 구성하지 않았다. 수동으로 배포하면서 빠뜨리지 않도록 실행할 명령과 확인할 결과를 절차에 남겼다.

원격 main을 받기 전에 작업 tree부터 확인했다

빌드 디렉터리에 로컬 수정이 남아 있으면 git pull 결과가 예상과 달라질 수 있었다. 먼저 git status --porcelain으로 작업 상태를 보고 현재 브랜치와 원격 커밋을 확인했다.

한 번은 로컬의 .env.example이 삭제돼 fast-forward가 막혔다. 확인한 파일의 삭제만 되돌린 다음 origin/main으로 fast-forward했다. 다른 로컬 작업까지 초기화할 이유는 없었다.

검증 스크립트를 실행하다 운영 셸의 연결이 닫힌 적도 있었다. source로 실행한 스크립트의 exit 1이 현재 셸을 종료한 것이었다. 이후에는 별도 프로세스로 실행하거나, 호출 방식에 따라 return과 exit을 구분해서 사용했다.

일회용 MySQL로 schema까지 검증했다

애플리케이션 테스트가 통과해도 MySQL 버전이나 Flyway 마이그레이션에서 문제가 생길 수 있었다. 운영과 같은 메이저 버전의 일회용 MySQL 컨테이너를 준비하고 Maven 테스트와 마이그레이션을 함께 확인했다.

최종 결과는 다음과 같았다.

Maven tests: 18
Failures: 0
Errors: 0
Skipped: 0
Build: SUCCESS
Flyway migrations: 2
Schema tables: 18

검증이 끝나면 일회용 DB를 삭제했다. 운영 데이터를 복사하지 않았으며, 스키마와 애플리케이션 시작 조건을 확인하는 별도 환경으로 사용했다.

latest 대신 commit 기반 tag를 사용했다

이미지 태그는 소스 커밋을 기준으로 만들었다. 나중에 docker service inspect로 실행 중인 이미지가 어느 코드에서 나왔는지 확인하기 위해서였다.

레지스트리에 올린 결과의 digest와 배포된 서비스 이미지의 digest도 비교했다. 처음에는 태그와 digest를 혼동했다. 태그는 붙여 놓은 이름이고 digest는 이미지 내용의 해시다. 같은 태그로 이미지를 다시 올리더라도 내용이 다르면 digest가 달라질 수 있었다.

배포 기록에는 커밋 태그와 digest를 함께 남겼다.

Worker의 registry 인증도 전달해야 했다

Manager에서는 이미지를 받을 수 있었지만 Worker 태스크는 pull access denied로 거부됐다. Manager의 셸에서 레지스트리에 로그인한 것만으로는 Worker에 인증이 전달되지 않았다.

스택을 배포할 때 아래 옵션으로 레지스트리 인증도 전달했다.

docker stack deploy \
  --with-registry-auth \
  -c compose.yaml \
  meetback

이후 docker service ps --no-trunc에서 두 Worker의 새 태스크가 이미지를 받고 Running 상태가 되는지 확인했다.

.env를 그대로 배포하지 않고 Secret으로 옮겼다

처음에는 Compose가 .env를 env_file로 읽었다. DB 비밀번호와 OAuth Secret, JWT 키가 평문 환경변수 목록에 남는 범위를 줄이려고 설정 전달 방식을 바꿨다.

설정을 Docker Secret 파일로 전달하고 Spring Boot가 읽도록 구성했다. App Secret은 애플리케이션 UID가 읽을 수 있게 설정했고, Redis 비밀번호와 ACL은 각각 별도 Secret으로 나눴다.

최종 서비스 환경변수 중 이름에 PASSWORD, SECRET, TOKEN, API_KEY가 들어간 평문 값이 남았는지 점검했다. Secret은 이름과 마운트 대상만 확인하고 내용을 출력하지 않았다.

Swarm Manager나 호스트 root 권한을 가진 운영자는 Secret을 사용할 수 있다. 이 구성으로 줄이려던 것은 소스, Compose, docker inspect의 환경변수 출력과 터미널 기록에 민감한 값이 남는 경우였다.

NFS volume은 container 시작 전부터 준비돼야 했다

App의 /app/uploads/feed는 NFS 볼륨을 사용한다. Worker에 NFS 클라이언트 패키지가 없거나 export 권한과 UID가 맞지 않으면 Spring Boot가 시작되기 전에 태스크가 Rejected 상태가 됐다.

두 Worker에 NFS 마운트 도구를 준비했다. NFSv4.2와 TCP, hard,timeo=600,retrans=2 옵션을 사용하고 App은 10001:10001로 실행했다. NFS 디렉터리 소유자도 같은 ID로 맞췄다.

Worker 1의 App 컨테이너에서 표식 파일을 쓰고 Worker 2에서 같은 내용을 읽은 뒤 삭제했다. 두 복제본이 실제로 같은 파일을 사용하는지 확인한 과정이다.

readiness와 Docker Healthcheck는 같은 것이 아니었다

코드에는 /readyz가 추가됐다. 기본 Cloudflare Tunnel과 예비 NPM 경로에서 모두 HTTP 200과 {"status":"UP"} 응답을 확인했다. 외부에서도 DB 연결과 애플리케이션 시작 상태를 점검할 수 있게 됐다.

다만 최종 Service 명세의 Docker Healthcheck는 null이었다. readiness 엔드포인트는 있었지만 Swarm이 이를 태스크 상태 판단에 사용하도록 연결되지는 않았다.

최종 배포 상태는 다음과 같다.

  • 애플리케이션 readiness 엔드포인트: 구현과 외부 검증 완료
  • Docker 이미지 또는 Compose healthcheck: 최종 Service에 미연결
  • readiness 결과에 따른 롤링 업데이트 판단: 추가 보완 필요

후속 배포에서는 이미지에 curl이나 가벼운 점검 도구가 있는지 확인하고 127.0.0.1:8080/readyz를 healthcheck에 연결해야 한다. 포트가 열린 상태에서도 요청을 처리하지 못할 수 있어 응답 결과를 확인할 필요가 있었다.

배포 완료의 기준

배포할 때는 아래 결과를 모두 확인했다.

  • 테스트와 Flyway 마이그레이션 통과
  • 커밋 태그와 레지스트리 digest 기록
  • App 2/2 Running, Worker별 1개
  • 두 태스크의 이미지 태그·digest 일치
  • DB 연결과 스키마 테이블 18개 확인
  • 두 Worker 사이 NFS 읽기·삭제 성공
  • 기본·예비 외부 경로 /readyz 200
  • 최근 서비스 로그에 반복 재시작이나 치명적 오류 없음

수동 배포에서 놓치기 쉬웠던 인증 전달, NFS 권한, healthcheck 연결 상태를 절차에 추가했다. DB 전용망과 MySQL 복제 구성은 별도 글에 정리했다.

실제 검증 화면

  1. Secret 값은 출력하지 않고 이름과 주입 대상만 확인했다. App의 실행 계정은 10001:10001이었다.
  2. Service에서 meetback_app_data가 /app/uploads/feed에 연결됐는지 확인했다.
  3. NFSv4.2 볼륨이 /app/uploads/feed에 연결된 배포 명세다.
  4. Secret 이름과 root가 아닌 실행 UID를 확인했다. Healthcheck는 null로 표시됐다.
  5. /readyz는 Cloudflare Tunnel과 NPM 경로에서 모두 HTTP 200이었다.
    앱 실행 사용자와 Docker Secret을 앱 설정 파일·Redis 암호 파일에 연결한 조회 결과

Docker 앱 데이터 볼륨을 컨테이너의 업로드 디렉터리에 연결한 마운트 요약

앱 볼륨의 NFSv4.2 마운트 옵션과 NoCopy 설정을 확인한 Docker 서비스 JSON

Docker Secret 목록과 앱 실행 사용자 10001:10001, Healthcheck가 null인 조회 결과

기본·예비 readyz 경로가 모두 HTTP 200과 TLS 검증 결과 0을 반환한 curl 실행 결과

SEARCH JOURNAL

기록 검색

글 제목, 요약, 태그에서 찾습니다.

검색을 준비하고 있어요.

검색어를 입력해 주세요.

    Ctrl/⌘ K 검색 Esc 닫기 ↑ ↓ 결과 이동