Lecture · DevOps / Deployment

로컬 Flask 앱을
서브도메인으로 공개하기

127.0.0.1:5001 에서만 돌던 부품 수명분석 앱을 Docker + Cloudflare Tunnel로 parts.sielain.com 에 올리기까지 — 실패한 시도까지 그대로 기록한 실전 노트입니다.

이 강의의 핵심 교훈
"터널이 원격 관리형이면, 로컬 config.yml은 읽히지 않는다"
Flask Docker Compose Cloudflare Tunnel gunicorn SQLite
01

문제 정의 — 무엇이 걸림돌이었나

constraints
출발점

Weibull 수명분석 Flask 앱이 맥미니 로컬에서 python app.py 로만 돌고 있었습니다. 터미널을 닫으면 죽고, 재부팅하면 사라지고, 외부에서는 접근할 수 없는 상태였죠. 목표는 단순합니다 — 기존 sielain.com 인프라에 얹어서 서브도메인으로 공개하는 것.

제약 조건
  • 소스가 docker 프로젝트 바깥 폴더에 있음 (경로에 한글 포함)
  • SQLite 파일 DB — 데이터를 잃으면 안 됨
  • 공유기 포트 개방 불가 (가정용 네트워크)
  • 기존 sielain.com 서비스에 영향 주면 안 됨
설계 결정 3가지
  • 유지 Streamlit 전환 대신 Flask 그대로 — 재작성 리스크 회피
  • 서브도메인 서브패스 대신 parts.sielain.com — nginx 불필요
  • Docker화 로컬 프로세스 제거 — 재부팅 자동 복구
서브패스(sielain.com/parts)를 택했다면 리버스 프록시(nginx) 계층이 추가로 필요했습니다. 서브도메인은 cloudflared의 hostname 라우팅만으로 끝나므로 움직이는 부품이 하나 적습니다.

02

최종 아키텍처 — 요청이 흐르는 길

architecture
🌐
브라우저 → parts.sielain.com
DNS CNAME은 대시보드 저장 시 자동 생성 (parts → <tunnel-id>.cfargotunnel.com)
☁️
Cloudflare Edge
TLS 종단 · 인바운드 포트 개방 없음 — cloudflared가 아웃바운드로 터널 유지
🚇
sielain-tunnel (cloudflared 컨테이너)
기존 sielain.com과 같은 터널 하나를 공유 — hostname으로 분기
🐳
http://parts-app:8000
Docker 내부 DNS — 같은 sielain_net 브리지 네트워크라서 컨테이너 이름으로 해석
🐍
gunicorn 2워커 → Flask → pump_data.db
SQLite는 호스트 파일을 볼륨 마운트 — 컨테이너를 재빌드해도 데이터 유지
포트를 하나도 열지 않았습니다. cloudflared는 Cloudflare 엣지로 나가는 연결만 만들기 때문에 공유기 포트포워딩·방화벽 규칙이 전혀 필요 없습니다. 가정용 네트워크에서 서비스를 공개하는 가장 안전한 패턴입니다.

03

구현 — 4단계로 끝내기

symlink · dockerfile · compose · dashboard
1
경로 분리 문제 → symlink 한 줄

소스는 ~/프로그램개발/.../parts_cycle_dashboard, docker 프로젝트는 ~/docker. compose 파일에 한글 경로를 직접 쓰는 대신 symlink로 연결합니다.

ln -s "/Users/sielain/프로그램개발/python/Projects/parts_cycle_dashboard" \
      /Users/sielain/docker/parts-app

compose에는 build: ./parts-app 만 기재 → 리포지토리가 특정 기기 경로에 오염되지 않음. symlink 자체는 기기 종속이므로 .gitignore 에 등록합니다.

2
Dockerfile — debug 모드가 자동으로 빠지는 이유
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["gunicorn", "-w", "2", "-b", "0.0.0.0:8000", "app:app"]

app.py 말미의 app.run(debug=True)__main__ 실행에서만 동작합니다. gunicorn이 CMD로 앱 객체를 직접 로드하므로 개발용 debug 모드가 코드 수정 없이 배제됩니다.

3
docker-compose.yml — 서비스 9줄 추가
parts-app:
  build: ./parts-app
  container_name: parts_app
  restart: unless-stopped
  volumes:
    - ./parts-app/pump_data.db:/app/pump_data.db   # SQLite만 마운트
  networks:
    - sielain_net                                   # cloudflared와 동일망

ports: 를 지정하지 않은 것에 주목 — 외부 노출은 cloudflared가 전담하므로 호스트 포트를 하나도 점유하지 않습니다. 볼륨은 DB 파일 하나만: 컨테이너를 재빌드해도 데이터가 유지됩니다.

4
Cloudflare 대시보드 — Published application routes

Zero Trust → Networks → Tunnels → 터널 선택 → Published application routes → Add:

필드비고
SubdomainpartsFull hostname: parts.sielain.com
Path(비움)모든 경로 매칭
Type / URLHTTP / parts-app:8000Docker 내부 DNS 이름

저장하면 DNS CNAME이 자동 생성되고 수 초 내에 서비스가 열립니다.


04

실패 기록 — config.yml은 왜 무시됐나

remotely-managed vs locally-managed
✗ 시도 1 — config.yml ingress 추가 (실패)

cloudflared/config.yml 에 hostname을 추가하고 컨테이너에 마운트했지만, 기동 로그가 진실을 말해줬습니다:

INF Updated to new configuration
    config="{ingress: [sielain.com → api:8000, 404]}"
    ← parts.sielain.com이 없다. 로컬 파일이 아니라 서버 설정을 받아옴

원인: 이 터널은 TUNNEL_TOKEN 환경변수로 기동하는 원격 관리형(remotely-managed) 터널. 라우팅 설정을 Cloudflare 서버에서 내려받으며, 로컬 config.yml의 ingress는 읽지 않습니다.

✓ 시도 2 — 대시보드 Published application routes (성공)

원격 관리형 터널의 라우팅은 대시보드가 유일한 진입점입니다. hostname 추가 후 즉시 반영 — 컨테이너 재시작도 필요 없습니다.

터널 유형기동 방식ingress 관리 위치
원격 관리형 (현재 구성)TUNNEL_TOKEN 환경변수Cloudflare 대시보드
로컬 관리형credentials-file + config.yml로컬 config.yml
UI 함정 주의: 2026년 기준 대시보드에서 구 "Public Hostnames"는 "Published application routes" 탭으로 이름이 바뀌었습니다. 옆의 "Hostname routes (Beta)" 탭은 WARP 클라이언트 전용 사설 네트워크 라우트라서 공개 웹 서비스에 쓰면 안 됩니다 — 화면에 "Cloudflare One Client가 필요하다"는 안내가 보이면 잘못 들어온 것입니다.
검증 습관: 배포 직후 스모크 테스트로 전 페이지 + DB를 사용하는 API까지 확인했습니다. 6개 경로 모두 200 — 특히 /api/part_distribution 이 200이면 SQLite 볼륨 마운트까지 정상이라는 뜻입니다.

05

운영 체크리스트

operations
이 패턴의 재사용성: 로컬에서만 돌던 어떤 웹 앱이든 ① Dockerfile ② compose 서비스 추가 ③ 대시보드 hostname 등록 — 이 3단계면 새 서브도메인으로 공개할 수 있습니다. 포트 개방도, nginx도, 기존 서비스 중단도 없이.