온프레미스 서버에 Kamal로 무중단 배포 도입하기
Watchtower로 컨테이너를 다시 만들던 배포가 재배포 한 번에 서비스 대부분을 503으로 만든 일을 계기로, 직접 블루그린·Kamal·Coolify·Docker Swarm을 비교하고 Kamal을 고른 과정과 Tailscale SSH·서비스 간 호출·한 서버의 dev/prd·SMTP·예약 작업처럼 옮기면서 만난 문제를 정리합니다.
Watchtower로 컨테이너를 다시 만들던 배포가 재배포 한 번에 서비스 대부분을 503으로 만든 일을 계기로, 직접 블루그린·Kamal·Coolify·Docker Swarm을 비교하고 Kamal을 고른 과정과 Tailscale SSH·서비스 간 호출·한 서버의 dev/prd·SMTP·예약 작업처럼 옮기면서 만난 문제를 정리합니다.
stateless JWT의 roles 클레임은 발급 시점 값이라 관리자가 권한을 바꿔도 토큰 수명(30분) 동안 옛 권한으로 메뉴와 API가 동작했습니다. 블랙리스트 대신 토큰에 권한 계산 시각(pv)을 넣고 Redis의 변경 시각과 비교해 401로 끊는 설계, 커밋 전 캐시 재적재 레이스, fail-open 선택까지 정리합니다.
무중단 배포로 Spring Boot 배치 서버가 잠깐 두 개 뜨는 동안 @Scheduled 작업이 두 번 실행되는 문제를, 분산 락·Spring Batch·Quartz와 비교한 뒤 ShedLock(JDBC)과 lockAtLeastFor로 해결하고 graceful shutdown·테스트까지 갖춘 과정을 정리합니다.
서비스 API 9개가 account 서비스에 권한을 묻는 구조에서, 조회 실패 시 "허용"하던 fail-open 설계는 장애가 곧 권한 우회였습니다. Resilience4j로 호출부 단위 회로 4개를 두고, 무엇을 실패로 셀지(4xx 제외), 회로 OPEN을 liveness에 연결하지 않는 이유, 권한 조회를 503 fail-closed로 바꾼 결정과 그 직후 드러난 배포 공백 문제까지 정리합니다.
한쪽 구독이 끊겨도 다른 쪽 하나로 전부 돌아가게, 그리고 손으로 두 벌 고치는 설정 파일 없이. 지침 파일을 AGENTS.md 하나로 바꾸고, 빌드·커버리지 게이트를 에이전트 훅에서 git 훅으로 옮기고, 스킬 32개를 한 폴더에 모으고, PR마다 반대편 모델이 리뷰하게 만든 하루의 PR 다섯 개를 정리합니다. 그 과정에서 발견한 "한 번도 막지 못하고 있던 훅"과 "한 번도 돌지 않고 있던 git 훅" 이야기도 함께.
Spring Boot 3.4에서 4.1로, Kotlin 1.9에서 2.3(K2)으로 18개 모듈을 하루에 올리며 알게 된 것들. Kotlin 2.x의 plugin.jpa가 all-open을 켜면서 final이던 엔티티가 open이 되고, 그제야 @ManyToOne(LAZY) 67곳이 지금까지 EAGER로 돌고 있었다는 사실과 가짜 fetch join 179곳이 드러났습니다. Jackson 3 때문에 전 요청이 39ms 만에 408이 난 사고도 함께 정리합니다.
빌드 스테이지에서 .next만 복사한 Next.js 런타임 이미지가 두 달 사이 세 번 발목을 잡았습니다. images.remotePatterns가 무시돼 이미지가 400, basePath가 사라져 전 라우트 404, 비루트 사용자로 바꾸자 corepack이 pnpm을 다시 받으려다 멈추고 로거의 mkdir이 EACCES로 모든 BFF가 HTML 500. Next 소스에서 어떤 설정이 빌드 산출물에 들어가고 어떤 설정을 next start가 런타임에 읽는지 확인하고, 비루트 컨테이너 점검 목록을 정리합니다.
5월에 Claude 채팅 위젯을 ERP에 넣었다가 5일 만에 통째로 걷어냈고, 10월에 같은 Claude API를 배치 안의 "문서 판정기"로 다시 들였습니다. 바뀐 것은 모델이 아니라 문제 정의, 비용 단위, 실패 처리였습니다. 건당 $0.23, 하루 $37이라는 실측을 보고 100배 싼 판정 모델을 게이트로 앞세워 하루 $10을 목표로 재설계한 과정을 숫자와 함께 정리합니다.
각자 서브도메인에 살던 Next.js 앱 6개를 포털 호스트 아래 경로 접두(example.com/svc)로 옮겼습니다. 앱 배포와 프록시 반영이 같은 순간일 필요가 없게 만든 4단계 순서, Next가 basePath를 붙여 주지 않는 fetch·문자열 src·window.location을 전역 패치로 처리한 방법, 누락을 찾아내는 "접두 밖은 404" 프록시, 쿠키를 host-only로 바꾼 이유, 그리고 서비스워커 4개가 FCM 토큰을 서로 지우던 문제까지 정리합니다.
보안 리뷰 권고대로 refresh 토큰 회전과 재사용 탐지를 넣었더니 30분마다 전 사용자가 모든 서비스에서 동시에 로그아웃됐습니다. 데스크톱 셸이 한 refresh 토큰을 웹앱 5개 쿠키와 자기 저장소에 복제하는 구조에서 "재사용 = 탈취" 판정이 어떻게 오판으로 바뀌는지, 그리고 회전을 걷어내고 무엇을 남겼는지 정리합니다.