콘텐츠로 이동

집 PC를 포트 개방 0개로 클라우드로 만들기

클라우드 요금 없이 내 서비스를 직접 운영해보고 싶었습니다. 마침 집에 PC 가 있었고, 결과적으로 지금 이 글은 그 PC 에서 서빙되고 있습니다. 공유기 포트포워딩도, 공인 IP 도, DDNS 도 없이요. 이 글은 그 과정에서 배운 것과 삽질의 기록입니다.

전체 구조

                    인터넷
                      │  HTTPS
              ┌───────▼────────┐
              │ Cloudflare 엣지 │  ← TLS 종료, WAF, 봇 차단
              └───────┬────────┘
                      │  터널 (내 PC 가 밖으로 나가는 연결)
    ══════════════════│══════════════════  내 PC (포트 개방 0개)
              ┌───────▼────────┐
              │   cloudflared   │
              └───────┬────────┘
              ┌───────▼────────┐
              │     Traefik     │  ← Host 헤더로 앱 분배
              └──┬──────────┬──┘
              ┌──▼─┐     ┌──▼─┐
              │www │     │ …  │   앱 컨테이너들
              └────┘     └────┘

배운 것들

1. 터널은 "안에서 밖으로" 뚫는다

일반적인 셀프호스팅은 공유기에서 포트를 열고 공인 IP 를 DNS 에 걸어야 하는데, 그 순간 집 IP 가 인터넷에 그대로 노출됩니다. Cloudflare Tunnel 은 반대입니다. 내 PC 의 cloudflared 가 Cloudflare 쪽으로 나가는(outbound) 연결을 유지하고, 방문자의 요청은 그 연결을 타고 거꾸로 들어옵니다. 방화벽 입장에서 내 PC 는 아무 것도 리슨하지 않으므로 공격 표면이 크게 줄어듭니다.

2. 라우팅은 라벨로 "선언"한다

Traefik 은 도커 이벤트를 실시간으로 감시합니다. 앱 컨테이너에 라벨 몇 줄만 붙이면 프록시 재시작 없이 즉시 라우팅이 생깁니다.

labels:
  - "traefik.enable=true"
  - "traefik.http.routers.myapp.rule=Host(`myapp.salmonpoke.org`)"
  - "traefik.http.services.myapp.loadbalancer.server.port=80"

Cloudflare 쪽은 *.도메인 → traefik 와일드카드 한 줄이면 끝이라, 새 앱을 올릴 때 건드릴 것이 앱 자신의 compose 파일뿐입니다. "설정이 배포 대상과 함께 산다"는 감각을 여기서 처음 익혔습니다.

3. 최소 권한은 습관이다

Traefik 이 도커를 감시하려면 도커 소켓이 필요한데, 소켓을 통째로 주면 Traefik 이 침해됐을 때 호스트 전체를 장악당합니다. 그래서 읽기전용 소켓 프록시를 사이에 두고 컨테이너 조회 등 필요한 API 만 열었습니다. 모든 컨테이너에 cap_drop: ALLread_only 를 기본으로 깔고, 안 되는 것만 명시적으로 다시 열었습니다.

삽질 기록

read_only 컨테이너의 healthcheck 가 계속 실패한 이유

nginx 를 read_only 로 띄우면 엔트리포인트가 IPv6 리스너 설정을 추가하지 못해 IPv4(0.0.0.0:80)에만 붙습니다. 그런데 컨테이너 안에서 localhost::1(IPv6)로 먼저 해석되어 healthcheck 가 "Connection refused" 를 뱉었습니다. localhost127.0.0.1 로 바꾸니 해결. "localhost = 127.0.0.1" 이 아니라는 것을 몸으로 배웠습니다.

Traefik 이 도커에 붙지 못한 이유

Traefik v3.3 은 Docker API v1.24 를 하드코딩해서 호출하는데, 최신 Docker Engine 은 최소 API 버전이 1.40 이라 요청을 400 으로 거부합니다. 소켓 프록시 로그에서 400 "GET /v1.24/version" 을 발견하고 나서야 원인을 알았고, v3.7 로 올려 해결했습니다. 에러가 난 곳(traefik)이 아니라 경유지의 로그를 봐야 할 때가 있습니다.

다음 계획

이 기반 위에 MLOps 스택을 한 층씩 올릴 예정입니다.

  1. FastAPI 로 ML 모델 서빙 (api.salmonpoke.org)
  2. GitHub Actions 로 CI/CD — push 하면 자동 배포
  3. MLflow 실험 추적, Prometheus + Grafana 모니터링
  4. 데이터 파이프라인 자동화, k3s 이전

각 단계도 이렇게 글로 남기겠습니다.