127.0.0.1:5001 에서만 돌던 부품 수명분석 앱을 Docker + Cloudflare Tunnel로 parts.sielain.com 에 올리기까지 — 실패한 시도까지 그대로 기록한 실전 노트입니다.
Weibull 수명분석 Flask 앱이 맥미니 로컬에서 python app.py 로만 돌고 있었습니다. 터미널을 닫으면 죽고, 재부팅하면 사라지고, 외부에서는 접근할 수 없는 상태였죠. 목표는 단순합니다 — 기존 sielain.com 인프라에 얹어서 서브도메인으로 공개하는 것.
parts.sielain.com — nginx 불필요sielain.com/parts)를 택했다면 리버스 프록시(nginx) 계층이 추가로 필요했습니다. 서브도메인은 cloudflared의 hostname 라우팅만으로 끝나므로 움직이는 부품이 하나 적습니다.
소스는 ~/프로그램개발/.../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 에 등록합니다.
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 모드가 코드 수정 없이 배제됩니다.
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 파일 하나만: 컨테이너를 재빌드해도 데이터가 유지됩니다.
Zero Trust → Networks → Tunnels → 터널 선택 → Published application routes → Add:
| 필드 | 값 | 비고 |
|---|---|---|
| Subdomain | parts | Full hostname: parts.sielain.com |
| Path | (비움) | 모든 경로 매칭 |
| Type / URL | HTTP / parts-app:8000 | Docker 내부 DNS 이름 |
저장하면 DNS CNAME이 자동 생성되고 수 초 내에 서비스가 열립니다.
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는 읽지 않습니다.
원격 관리형 터널의 라우팅은 대시보드가 유일한 진입점입니다. hostname 추가 후 즉시 반영 — 컨테이너 재시작도 필요 없습니다.
| 터널 유형 | 기동 방식 | ingress 관리 위치 |
|---|---|---|
| 원격 관리형 (현재 구성) | TUNNEL_TOKEN 환경변수 | Cloudflare 대시보드 |
| 로컬 관리형 | credentials-file + config.yml | 로컬 config.yml |
/api/part_distribution 이 200이면 SQLite 볼륨 마운트까지 정상이라는 뜻입니다.
python app.py(5001) 수동 실행은 이제 불필요. 컨테이너가 restart: unless-stopped 로 대체up 시 parts-app 빌드 실패 가능 → docker compose up -d api db 로 서비스 지정 기동pump_data.db 는 PostgreSQL 동기화 대상이 아님. 맥미니 로컬 파일이 유일본 → 별도 백업 필요docker compose up -d --build parts-app 재빌드 (볼륨은 DB 파일만 해당)