이 글의 위치: 기술 블로그 → 인프라 · 로컬 서버 운영. 작성·검증 방식은 Editorial Policy 참조.
집에 있는 Debian PC 한 대로 키워드 조사 도구를 포함한 여러 서비스를 실제로 굴리며 겪은 기록을 묶은 로컬 서버 운영기 1편입니다. 이 글은 "어떻게 공개하나"를 다루고, 그 위에 올린 통합 로그인(Keycloak) 이야기와 Docker Compose에서 K3s로의 이주는 다음 편들에서 이어집니다.
"집 IP를 어떻게 인터넷에 노출하지?"에서 막힙니다
집 PC에서 서비스를 하나 띄웠습니다. localhost:3000에선 잘 뜹니다. 그런데 이걸 밖에서 admin.example.store로 접속하게 하려는 순간 벽에 부딪힙니다. 집 인터넷은 보통 유동 IP고, 공유기 뒤에 있어서 외부에서 직접 들어올 수 없습니다. 검색하면 나오는 답은 대개 이렇습니다 — "공유기에서 80/443 포트포워딩하고, DDNS로 도메인 붙이세요."
포트포워딩은 동작합니다. 사실 저도 처음엔 이 방식으로 공개했습니다. 집 PC에서 LG 공유기 관리자 페이지에 접속해, 외부 80/443을 집 PC로 넘기는 포트포워딩 규칙을 만들었습니다. 문제는 집이 유동 IP라 IP가 바뀌면 도메인이 엉뚱한 곳을 가리킨다는 점이었는데, 이건 FreeDNS에서 무료 도메인을 발급받아 바뀌는 집 IP를 자동으로 추적(DDNS)하게 해서 해결했습니다. 그리고 그 도메인 앞에 Cloudflare를 두어 DNS·TLS를 엣지에서 관리하게 했습니다. 이 구성으로도 밖에서 도메인 접속은 됩니다.
그런데 쓰다 보니 세 가지가 계속 걸렸습니다. ① 공유기에 열어둔 80/443이 그대로 공격 표면으로 남고, ② 유동 IP가 바뀔 때 DDNS 갱신이 늦으면 잠깐씩 접속이 끊기고, ③ ISP가 가정용 회선의 80/443을 막는 경우도 있어 신경 쓸 게 많았습니다. Cloudflare 프록시가 오리진 IP를 가려주긴 했지만, 공유기의 포트는 여전히 열려 있어야 했습니다. 결국 저는 그 열어둔 포트마저 없애기로 했습니다 — 인바운드 포트를 하나도 열지 않는 Cloudflare Tunnel입니다.
발상의 전환: 들어오게 하지 말고, 나가서 연결한다
포트포워딩은 "밖에서 우리 집으로 들어오는" 통로를 뚫는 방식입니다. 터널은 반대입니다. 집 PC에서 실행되는 데몬(cloudflared)이 바깥(Cloudflare)으로 먼저 아웃바운드 연결을 맺어두고, 방문자 트래픽은 Cloudflare가 그 연결을 통해 집 PC로 되돌려줍니다.
[포트포워딩] 방문자 → 공유기(80/443 개방) → 집 PC (인바운드 통로를 연다)
[터널] 집 PC( cloudflared ) ──아웃바운드──▶ Cloudflare
방문자 → Cloudflare ──기존 연결로 되돌림──▶ 집 PC (인바운드 0)

집 방화벽 입장에서는 그냥 "밖으로 나가는 연결" 하나일 뿐이라, 공유기에 아무 포트도 열 필요가 없습니다. 유동 IP여도 상관없습니다. 연결은 항상 집에서 바깥으로 맺으니까요.
포트포워딩 vs 터널 — 나란히 놓고 보기
| 항목 | 포트포워딩(+DDNS) | Cloudflare Tunnel |
|---|---|---|
| 공유기 인바운드 포트 | 80/443 개방 필요 | 0개 (아웃바운드만) |
| 유동 IP | DDNS로 계속 추적 | 무관 (IP를 몰라도 됨) |
| 공격 표면 | 집 IP·포트 직접 노출 | 오리진 IP 숨김(터널 뒤) |
| TLS 인증서 | 직접 발급·갱신(Let’s Encrypt 등) | Cloudflare가 종단(엣지에서 처리) |
| ISP의 80/443 차단 | 막히면 곤란 | 영향 없음 |
| 설정 위치 | 공유기 + 서버 양쪽 | 서버 한 곳(config.yml) |
제 환경에서 결정적이었던 건 "오리진 IP를 숨긴다"와 "TLS를 엣지가 종단한다" 두 가지였습니다. 집 PC는 사설망 뒤에 있고, 인증서 갱신을 신경 쓸 필요도 없어졌습니다. nginx는 평문 HTTP만 다루면 됩니다.
터널 하나로 도메인 여러 개 — 그리고 그 뒤의 게이트웨이
터널은 호스트네임별로 어디로 보낼지만 정하면 됩니다. 제 cloudflared 설정은 이렇게 생겼습니다(값은 예시).
# /etc/cloudflared/config.yml
tunnel: <tunnel-id>
ingress:
- hostname: example.store
service: http://localhost:8090
- hostname: admin.example.store
service: http://localhost:8090
- hostname: api.example.store
service: http://localhost:8090
- service: http_status:404 # 어디에도 안 맞으면 404
눈에 띄는 건 모든 호스트네임이 같은 localhost:8090으로 간다는 점입니다. 도메인별 분기는 터널이 아니라 그 뒤의 nginx 게이트웨이가 맡습니다. 터널은 "일단 다 게이트웨이로", 게이트웨이가 "이 도메인·이 경로는 이 서비스로"를 정하는 2단 구조입니다.
Cloudflare Tunnel ─▶ nginx 게이트웨이(:8090) ─┬─ example.store → 프런트
├─ admin.example.store → 관리 콘솔
└─ api.example.store/sap/ → 백엔드 A
/ums/ → 백엔드 B ...

게이트웨이의 도메인 라우팅은 nginx server_name으로, 경로 라우팅은 location으로 나눕니다.
server {
listen 80;
server_name admin.example.store; # 이 도메인은
location / {
proxy_pass http://admin-front:3000; # 이 서비스로
}
}
server {
listen 80;
server_name api.example.store;
location /sap/ { proxy_pass http://backend-a:8082; } # 경로별로 다른 백엔드
location /ums/ { proxy_pass http://backend-b:8082; }
}
이 2단 구조의 장점은, 서비스를 추가할 때 터널은 그대로 두고 게이트웨이 설정만 바꾸면 된다는 것입니다. 터널의 호스트네임 목록은 거의 손대지 않습니다.
실제로 붙여보고 확인한 것
터널을 켜고(systemctl start cloudflared) 게이트웨이를 올린 뒤, 밖에서 실제로 도는지는 curl로 확인합니다. 도구의 상태창을 믿기보다 응답 코드를 직접 봅니다.
$ curl -s -o /dev/null -w "%{http_code}\n" https://admin.example.store/
200
한 가지 배운 함정이 있습니다. 게이트웨이 nginx 설정 파일을 Docker에서 단일 파일로 bind mount해 두면, 편집기로 파일을 고쳐도 컨테이너가 옛 내용을 계속 봅니다. 파일을 새로 쓰면 inode가 바뀌는데, 단일 파일 마운트는 예전 inode에 묶여 있기 때문입니다. nginx -s reload로도 안 잡히고, 컨테이너를 재생성(docker compose up -d --force-recreate)해야 새 파일이 반영됩니다. 이건 나중에 K3s로 옮길 때 ConfigMap으로 바뀌면서 자연히 사라졌습니다.
정리
- 집 PC를 공개하는 데 포트포워딩·고정 IP가 필수는 아닙니다. 아웃바운드 터널이면 인바운드 포트 0으로 도메인을 붙일 수 있습니다.
- 터널은 "일단 게이트웨이로", 도메인·경로 분기는 nginx 게이트웨이가 맡는 2단 구조가 서비스 추가에 유연합니다.
- TLS 종단·오리진 숨김을 엣지에 맡기면, 집 서버는 평문 HTTP 라우팅만 신경 쓰면 됩니다.
- 확인은 상태창이 아니라
curl응답 코드로. 단일 파일 bind mount의 inode 함정도 기억해 두시길 바랍니다.
다음 편에서는 이렇게 공개한 여러 서비스에 한 번 로그인으로 다 접근하게 만든 이야기 — 서비스마다 흩어져 있던 커스텀 JWT 인증을 Keycloak 통합 로그인으로 무중단 전환한 과정을 다룹니다.
참고
- Cloudflare Tunnel(
cloudflared) 공식 문서 · developers.cloudflare.com/cloudflare-one/connections/connect-networks - nginx
server_name/proxy_pass· nginx.org/en/docs/http/ngx_http_core_module.html - 같은 시리즈 — 한 번 로그인으로 다 쓰기: Keycloak 통합 로그인 · Docker Compose에서 K3s로
답글 남기기