집 서버에 주식 앱 배포하다 겪은 삽질 3가지
iptime CAA 차단, DB 포트 충돌, 수동 체크아웃 — 직접 겪어야 나오는 것들
GitHub Actions self-hosted runner를 집 서버에 붙이고, Caddy + DuckDNS로 주식 앱을 자동 배포하는 구성을 짰습니다. 구조 자체는 단순한데, 실제로 올려보니 예상과 전혀 다른 지점에서 막혔습니다. 세 번의 삽질을 하나씩 풀어볼게요.
삽질 1: dig +short CAA iptime.org 한 줄이 이틀을 해결했다
처음엔 korat.iptime.org로 Let’s Encrypt 인증서를 발급받으려 했습니다. 80/443 포트포워딩을 열고, Caddy 설정을 맞추고, DuckDNS 갱신기까지 돌렸는데 인증서가 끝내 안 나왔습니다.
Caddy 로그에 뜨는 건 이것뿐이었습니다:
challenge failed ... error:caa ... "prevents issuance"
포워딩부터 의심했고, 공인 IP 확인, NAT 중첩 여부, ISP 차단 가능성까지 하나씩 짚었습니다. 다 정상이었습니다. 이틀째 되던 날 CAA 레코드를 떠올렸고, 딱 한 줄 쳐봤습니다:
dig +short CAA iptime.org
결과: 0 issue ";". 모든 공인 CA의 인증서 발급을 전면 차단하는 레코드였습니다. iptime.org 상위 도메인 DNS는 제가 제어할 수 없으니 고칠 방법도 없었습니다.
해결은 DuckDNS로 전환하는 것이었습니다. CAA 레코드 없음, DDNS 내장, 무료. SITE_DOMAIN 환경변수 하나 바꾸는 것으로 도메인을 교체했고, 곧바로 certificate obtained successfully가 찍혔습니다.
앞으로는: 새 도메인을 쓸 때 가장 먼저 dig +short CAA <도메인>과 dig +short CAA <상위도메인>을 확인합니다. 서브도메인만 체크하면 부족합니다. 상위 도메인이 막혀 있으면 서브도메인도 막힙니다.
삽질 2: self-hosted runner와 라이브 DB가 같은 포트를 쓰고 있었다
self-hosted runner는 배포 타깃이기도 했습니다. 같은 집 서버에 라이브 dev 스택(trading_db_dev)이 항상 떠 있는 상태에서, CI가 돌 때마다 같은 서버에서 테스트용 DB 컨테이너를 새로 올렸습니다.
dev-release push마다 CI가 이 에러로 깨졌습니다:
Bind for 127.0.0.1:5432 failed: port is already allocated
처음엔 일시적인 leftover 컨테이너 문제인가 싶었는데, 아니었습니다. 라이브 dev 스택의 trading_db_dev가 127.0.0.1:5432를 상시 점유하고 있고, CI의 trading_db가 0.0.0.0:5432로 거기 위에 바인드하려다 충돌하는 구조적 문제였습니다. 에러 로그가 0.0.0.0이 아니라 127.0.0.1:5432를 콕 집어 가리키는 게 단서였습니다.
원인을 파악하고 나니 해결은 명확했습니다. CI 컨테이너들은 애초에 호스트 포트 publish가 필요 없었습니다. 컨테이너 내부 네트워크로 통신하면 충분했으니까요. docker-compose.ci.yaml 오버라이드를 만들어 ports: !reset []으로 호스트 publish를 제거했습니다.
(Compose v2.24 이상에서 !reset이 지원됩니다. 그냥 []로는 기존 항목이 안 지워지고 concat됩니다.)
앞으로는: self-hosted runner가 배포 타깃이면 CI 임시 스택과 라이브 스택은 호스트 포트가 겹치면 안 됩니다. CI는 내부 네트워크로만 쓰는 게 정석입니다.
삽질 3: 낡은 수동 체크아웃 하나가 사이트를 다운시켰다
CD를 붙인 뒤에도 수동 배포하던 버릇이 남아 있었습니다. 서버에 손으로 clone해 둔 ~/trading_mvp 폴더에서 docker compose up을 쳤습니다. 에러가 한 번에 하나씩, 순서대로 터졌습니다.
invalid proto:— 포트 표기에 끝 콜론 오타가 있었습니다.Network deploy_default Created— 낡은 파일엔name:핀이 없어 프로젝트명이 디렉터리명deploy로 갈렸습니다."trading_web_dev" is already in use— CD가 띄운 스택(trading-mvp-dev)과 컨테이너 이름이 충돌했습니다.Can't locate revision— 낡은 체크아웃엔 최신 alembic 마이그레이션이 없었습니다.RuntimeError: PII_ENCRYPTION_KEY가 설정되지 않았습니다—backend.env에 키가 없어 migrate가 fail-fast하고, 이걸 기다리던 backend·worker·beat·caddy가 모두Created에서 멈췄습니다. 사이트 다운.
CD가 관리하는 사본은 ~/actions-runner/_work/... 아래에 따로 있었고 라이브 자체는 멀쩡했지만, 낡은 수동 폴더가 같은 컨테이너 이름으로 스택을 올리려다 연쇄 충돌을 냈습니다.
해결은 수동 폴더 삭제였습니다. 데이터는 /srv/trading-mvp/data에 bind mount로 따로 있어서 폴더를 지워도 보존됐습니다.
앞으로는: 서버에 저장소 사본은 CD 전용 하나만 둡니다. 상태 점검은 docker ps, docker logs trading_*_dev로 충분합니다. 서버에서 직접 체크아웃하는 습관이 충돌의 원인이었습니다.
외부 도달이 안 될 때 체크리스트
세 번 삽질을 거치며 진단 순서가 몸에 익었습니다. 직접 겪어야만 나오는 것들이 있습니다. “외부에서 안 들어와요”는 거의 항상 이 계층 중 하나입니다:
컨테이너 → 호스트 방화벽 → LAN 도달 → 이중 NAT → 공유기 포워딩 → ISP 차단 → DNS → CAA → 인증서
위에서 아래로 내려가며 처음 실패하는 계층을 찾으면 원인이 특정됩니다. 한 가지 주의: 외부 도달 테스트는 반드시 휴대폰 LTE에서, 와이파이를 끄고 해야 합니다. 집 LAN 안에서 공인 도메인으로 접속하면 NAT loopback 경로라 포워딩을 안 거쳐 결과가 왜곡됩니다.
집 서버에 Let’s Encrypt를 쓴다면, 새 도메인마다 dig +short CAA <도메인>과 dig +short CAA <상위도메인>을 먼저 확인하세요. 저처럼 이틀 삽질 안 해도 됩니다.
[주식앱 시리즈] <- 이전 편: AI 에이전트로 주식 앱 만들면서 겪은 것들 [집서버 시리즈] <- 이전 편: 집서버 1대에 앱 여러 개 올리기 함께 보기: 1인 개발 삽질 연대기 · KIS API 규제 삽질