Development Log
1편. 집 밖에서 내 서버에 접속하기 — 공유기부터 HTTPS까지!
집 밖에서 홈서버에 접속하기 위해 공유기, DNS, Nginx와 HTTPS 경계를 하나씩 연결하고 검증한 기록입니다.
- #Home Server
- #Security
문제 집 안의 서버를 외부에서
ddongmy.com으로 안전하게 접근하고 싶었다. 선택 포트포워딩 → DNS → Host Nginx reverse proxy → Let's Encrypt HTTPS 순서로 요청 경로를 구성했다. 결과https://ddongmy.com하나로 외부 요청이 Ubuntu → Docker → Next.js까지 전달되는 구조를 완성했다. 배운 점 접속 장애는 애플리케이션부터 보지 않고 DNS → 네트워크 → 프록시 → 컨테이너 → 애플리케이션 순서로 경계를 나눠야 한다.
처음 목표는 Kubernetes도, 거창한 인프라도 아니었다. 집에 있는 PC 한 대를 서버로 만들고, 밖에서도 내가 만든 서비스에 접속해보자. 지금 돌아보면 K3s까지 이어진 홈서버의 시작은 애플리케이션보다 먼저 집 네트워크를 외부와 연결하는 일이었다.
시작점 — 집 안의 PC를 서버로 써보고 싶었다
프론트엔드 개발을 하면서 애플리케이션을 만드는 일에는 익숙했지만, 브라우저에서 주소를 입력한 뒤 내 코드까지 요청이 어떻게 도착하는지는 직접 구성해본 적이 많지 않았다.
그래서 Ubuntu를 설치한 홈서버를 하나 두고, ddongmy.com이라는 내 도메인으로 직접 서비스를 운영해보기로 했다.
처음에는 단순히 서버에서 애플리케이션만 실행하면 될 줄 알았다. 하지만 집 밖에서 접속하려면 그 전에 해결해야 할 것이 있었다.
외부에서 들어온 요청을 집 안의 어느 장비로 보낼 것인가? 이게 첫 번째 문제였다.
공유기 안쪽의 서버와 외부 인터넷을 연결하기
홈서버는 공유기 내부의 사설 네트워크에 있었다. 외부에서는 그 내부 IP로 직접 들어올 수 없기 때문에 공유기에서 홈서버로 요청을 넘겨주는 구성이 필요했다. 이때 포트포워딩을 처음 제대로 이해하게 됐다.
SSH는 기본 22번 포트를 그대로 외부에 노출하기보다 외부에서 2222로 접근하도록 구성했다. 실제로 이후에는 다음 형태로 집 밖에서 서버에 접속했다.
ssh -p 2222 dongmin@ddongmy.com
결과적으로 SSH의 외부 접근 흐름은 이렇게 됐다.
이 과정에서 공인 IP, 사설 IP, 포트포워딩이 각각 어떤 역할을 하는지 처음 실제 환경과 연결해서 이해했다.
연결이 안 될 때 서버부터 의심하지 않게 된 계기
처음에는 "SSH가 안 된다"는 하나의 증상으로 보였다. 하지만 내부 네트워크에서 서버가 살아 있고 SSH가 동작한다면 sshd 자체보다 외부에서 내부 서버까지 들어오는 경로를 확인해야 했다.
외부에서 접속 실패
↓
Ubuntu가 죽었나?
↓
내부에서는 서버와 SSH가 정상
↓
그렇다면 서버보다 앞단을 확인
↓
공인 IP → 공유기/GW → 포트포워딩 → 내부 IP
이 경험 이후부터는 접속 실패를 곧바로 애플리케이션 장애로 보지 않고, 어디까지 요청이 도달하는지를 한 단계씩 좁혀가는 방식으로 보기 시작했다. 나중에 Docker, Nginx, Traefik, Kubernetes를 다룰 때도 이 방식이 그대로 이어졌다.
초기 구축 당시 공유기 관리자 화면에서 어떤 메뉴와 값을 사용했는지까지는 과거 기록에서 정확하게 복원되지 않는다. 그래서 이 회고에서는 확인되는 구조와 실제 접속 방식까지만 기록하고, 기억나지 않는 UI 설정값은 임의로 만들지 않았다.
IP 대신 내 도메인으로 들어오고 싶었다
공인 IP 숫자를 외워서 접속하는 것보다 내가 가진 ddongmy.com을 홈서버에 연결하고 싶었다.
그래서 DNS가 집의 공인 IP를 가리키도록 구성했고, 이후에는 SSH도 웹 서비스도 도메인을 기준으로 접근할 수 있게 됐다.
이때부터 도메인 = 서버라고 단순하게 생각하지 않게 됐다. 실제로는 도메인이 DNS를 통해 공인 IP를 찾고, 공유기가 다시 내부의 서버로 요청을 넘긴다.
도메인으로 접속이 안 된다고 해서 곧바로 서버 문제라고 볼 수도 없었다. ddongmy.com이 현재 공인 IP를 가리키는지, 공유기까지 요청이 도착하는지, 그 요청이 홈서버로 전달되는지를 분리해서 봐야 했다. DNS는 단순히 예쁜 주소를 붙이는 기능이 아니라 요청 경로의 첫 번째 해석 단계라는 걸 실제 운영을 통해 이해하게 됐다.
이제 서버에 애플리케이션을 올려보자
외부에서 Ubuntu 서버에 접근할 수 있게 된 뒤에는 실제 서비스를 올렸다. 애플리케이션은 Docker 컨테이너로 실행했다.
ddongmy-os는 컨테이너 내부에서 Next.js가 3000 포트를 사용하고, 호스트에서는 3002로 연결하는 구조였다. 이후 ilsangham 같은 다른 서비스도 별도 포트를 사용했다.
소스는 /var/www/ddongmy-os에서 운영했고, Docker Compose로 빌드와 실행을 관리했다. 파일 권한 때문에 작업이 막혔을 때는 다음처럼 소유권을 정리한 기록도 남아 있다.
sudo chown -R $USER:$USER /var/www/ddongmy-os
docker compose up -d --build
처음에는 http://서버주소:3002처럼 접근할 수도 있었지만, 최종적으로 원하는 모습은 아니었다. 사용자 입장에서는 포트 번호를 알 필요 없이 https://ddongmy.com으로 들어오게 하고 싶었다.
Nginx를 앞에 두면서 구조가 서버답게 바뀌었다
그래서 Ubuntu Host에 Nginx를 두고 외부 요청을 Docker 컨테이너로 넘기도록 했다.
이때 처음으로 reverse proxy가 왜 필요한지 체감했다. 외부 사용자는 Docker가 어떤 포트를 사용하는지 알 필요가 없고, Nginx가 도메인과 요청을 기준으로 내부 애플리케이션에 전달하면 됐다.
서비스가 늘어나더라도 외부 진입점은 Nginx 하나로 유지할 수 있었다.
여기서 Nginx를 선택한 이유도 단순히 "웹서버니까"가 아니었다. Next.js 컨테이너의 3002 같은 내부 운영 포트를 인터넷에 그대로 노출하기보다, 외부 진입점은 80/443으로 고정하고 내부 서비스 위치는 reverse proxy 뒤에 숨기고 싶었다. 이후 서비스가 늘어나도 도메인과 경로를 Nginx에서 분기할 수 있다는 점도 홈서버 구조에 잘 맞았다.
외부 사용자가 알아야 하는 것
https://ddongmy.com
외부 사용자가 몰라도 되는 것
Docker container / host port 3002 / Next.js port 3000
이 구분은 이후 Kubernetes에서 Ingress → Service → Pod 구조를 이해할 때도 중요한 기준이 됐다.
HTTP에서 HTTPS로
도메인까지 연결하고 나니 다음은 HTTPS였다. Let's Encrypt 인증서를 사용하고 Nginx가 TLS를 처리하도록 구성했다. 최종적으로 첫 홈서버의 웹 요청 흐름은 다음과 같은 모습이 됐다.
처음에는 각각 따로 보였던 DNS, 포트포워딩, Nginx, Docker가 이 그림을 그리면서 하나의 요청 흐름으로 연결됐다. 특히 HTTPS를 붙이면서 TLS를 어디에서 종료할 것인가라는 개념도 처음 접했다. 당시 구조에서는 Nginx가 443 요청과 인증서를 담당하고, 뒤의 Docker/Next.js는 HTTP 애플리케이션 서버 역할에 집중했다. 나중에 K3s로 넘어가면서 이 TLS 종료 지점을 Host Nginx에서 Traefik으로 옮기게 되는데, 그 변화도 결국 이 첫 구조에서 출발했다.
서버가 뜨는 것만으로는 부족했다
서비스를 띄운 뒤에는 상태를 보고 싶어서 Netdata도 붙였다. Docker 상태를 Netdata에서 보기 위해 Docker 관련 설정과 권한을 조정하고 플러그인을 확인했다. 이 시점까지는 꽤 만족스러웠다.
“이제 집에 있는 서버로 내 서비스를 직접 운영할 수 있네.” 그런데 실제로 계속 운영해보니 다른 문제가 시작됐다. 공유기/GW가 이상해져 네트워크가 끊기기도 했고, 인증서를 갱신했는데 브라우저에서는 계속 만료된 것처럼 보이기도 했다. Docker 컨테이너와 Nginx 사이에서 장애가 발생하면 어느 계층이 문제인지 직접 찾아야 했다. 서비스를 한 번 띄우는 것과 계속 운영하는 것은 다른 문제였다. 다음 편: Docker로 서비스를 직접 운영하면서 겪은 네트워크, HTTPS, 컨테이너 문제와 그 과정에서 생긴 문제 해결 방식.