포트 매핑은 멀쩡한데 밖에서만 안 붙을 때, 127.0.0.1과 0.0.0.0의 차이
환경 Docker Engine · Django · Flask · Node.js
컨테이너를 띄우고 -p 8080:80 까지 걸었는데 브라우저가 붙지 못한다. docker ps 에는 포트 매핑이 멀쩡히 찍혀 있다. 컨테이너 안에 들어가 curl 을 쳐 보면 응답도 잘 온다. 밖에서만 안 된다. 이럴 때 매핑이나 방화벽을 뒤지기 전에 볼 자리는 애플리케이션이 붙잡은 주소다.
루프백만 받는 주소와 전부 받는 주소
둘 다 '내 컴퓨터'처럼 읽히지만 하는 일이 다르다. 127.0.0.1 은 실제 주소다. 루프백 인터페이스 하나를 가리키고, 그 주소로 들어온 연결만 받는다. 같은 기계 안에서 출발한 연결이 아니면 애초에 여기로 오지 않는다.
0.0.0.0 은 주소가 아니라 주소를 정하지 않았다는 표시다. 서버가 이걸로 바인딩하면 그 기계에 붙은 인터페이스 전부에서 연결을 받는다. 루프백도 포함이고, 랜카드도 포함이고, 컨테이너에 딸린 가상 인터페이스도 포함이다.
그래서 붙느냐 못 붙느냐는 포트 번호가 아니라 그 연결이 어느 인터페이스로 들어왔느냐에서 갈린다. 표로 보면 이렇게 나뉜다.
연결이 들어오는 경로 127.0.0.1 바인딩 0.0.0.0 바인딩
-------------------------------- ----------------- --------------
같은 기계에서 localhost 로 받는다 받는다
같은 랜의 다른 PC 에서 못 받는다 받는다
호스트 → 컨테이너 안 서비스 못 받는다 받는다
프록시가 다른 컨테이너에 있을 때 못 받는다 받는다
docker -p 로 넘어온 연결 못 받는다 받는다
바깥 노출 범위 루프백뿐 매핑·방화벽에 달림포트가 아니라 인터페이스에서 갈린다
0.0.0.0 은 주소가 아니라 어느 인터페이스로 들어와도 받는다는 뜻이다.
컨테이너 안의 127.0.0.1은 컨테이너 것이다
컨테이너는 자기 네트워크 인터페이스를 따로 갖는다. 브리지 네트워크에 붙은 컨테이너에는 그 네트워크 대역의 주소가 하나씩 붙는다. 루프백도 그 안에 따로 하나 있다. 호스트의 127.0.0.1 과 컨테이너의 127.0.0.1 은 이름만 같지 서로 다른 자리다.
-p 8080:80 은 호스트 포트로 들어온 연결을 컨테이너 인터페이스 주소의 80 번으로 넘긴다. 넘어간 연결은 172.x 로 시작하는 컨테이너 주소에 도착한다. 애플리케이션이 컨테이너 안 127.0.0.1:80 만 잡고 있으면 그 연결을 받아 줄 쪽이 없다. 매핑은 멀쩡한데 밖에서만 끊기는 이유가 여기다.
컨테이너 안에서 curl 127.0.0.1 이 잘 되는 게 사람을 헷갈리게 한다. 그건 루프백으로 들어간 연결이니 당연히 받는다. 확인은 curl 로 하지 말고 무엇을 듣고 있는지로 한다.
# 컨테이너가 어떤 주소를 듣고 있는지
docker exec <컨테이너> ss -ltnp
# 127.0.0.1:80 → 컨테이너 안에서만 붙는다
# 0.0.0.0:80 → 밖에서 넘어온 연결도 받는다
# *:80 → IPv6 까지 함께 받는다
# 매핑 자체는 이걸로 본다
docker port <컨테이너>매핑을 의심하기 전에 LISTEN 주소부터 본다
기본값이 프레임워크마다 반대로 잡혀 있다
어느 주소를 잡을지는 대개 프레임워크 기본값이 정한다. 그 기본값이 서로 반대라는 게 사고의 절반을 만든다.
Django 개발 서버는 127.0.0.1:8000 으로 뜬다. 이 주소로는 네트워크의 다른 기계에서 못 붙고, 보이게 하려면 0.0.0.0 이나 그 줄임인 0 을 직접 줘야 한다. Flask 의 app.run() 도 기본이 127.0.0.1 이다. 둘 다 잠근 채로 시작한다.
Node 는 반대다. server.listen(3000) 처럼 호스트를 빼면 IPv6 를 쓸 수 있는 환경에서는 :: 로, 아니면 0.0.0.0 으로 받는다. 대부분의 운영체제에서 :: 로 듣고 있으면 IPv4 쪽 0.0.0.0 도 함께 받는다. 그래서 Node 앱은 컨테이너에 그냥 넣어도 붙고, 같은 감각으로 Django 를 넣으면 안 붙는다.
스택을 옮겨 다닐 때 이 차이만 기억해 두면 원인을 찾는 시간이 크게 줄어든다.
# Django — 컨테이너나 다른 기계에서 붙어야 하면 직접 준다
python manage.py runserver 0.0.0.0:8000
# Flask
flask run --host 0.0.0.0 --port 5000
# Node — 호스트를 빼면 이미 전부 받는 상태다
node -e "require('http').createServer((_,r)=>r.end('ok')).listen(3000)"잠긴 채로 뜨는 쪽만 손대면 된다
Django·Flask 는 잠근 채로 뜨고, Node 는 열린 채로 뜬다.
열어 두면 어디까지 열리는가
그렇다고 0.0.0.0 이 만능은 아니다. 그 기계에 닿을 수 있는 곳이면 어디서든 그 포트가 보인다는 뜻이라, 개발용으로 잠깐 띄운 DB 나 디버그 화면이 사내망 전체에 그대로 열리는 일이 생긴다.
도커에서는 한 가지 더 조심할 게 있다. 컨테이너 포트를 -p 로 공개하면 그 트래픽은 nat 테이블에서 먼저 방향을 튼다. ufw 가 보는 INPUT 체인까지 오지 않는다는 뜻이다. 방화벽으로 막아 뒀으니 괜찮다고 생각했는데 실제로는 열려 있는 상태가 여기서 나온다.
그래서 잠글 자리는 애플리케이션이 아니라 호스트 쪽 매핑이다. -p 에 아이피를 안 적으면 도커는 0.0.0.0 으로, 즉 모든 인터페이스에 공개한다. 앞에 127.0.0.1 을 붙여 두면 그 호스트 안에서만 보인다. 컨테이너 안 앱은 0.0.0.0 을 듣게 두고 노출 범위는 매핑에서 정하는 편이 덜 어긋난다.
# 아이피를 안 적으면 0.0.0.0, 곧 전부 공개다
docker run -d -p 8080:80 myapp
# 호스트 안에서만 보이게
docker run -d -p 127.0.0.1:8080:80 myapp
# 지금 어디에 묶여 있는지 확인
docker port myapp
ss -ltn | grep 8080공개 범위는 앱이 아니라 -p 앞자리에서 정한다
그래서 어느 쪽을 언제 쓰나
127.0.0.1 로 잡을 자리는 그 기계 안에서만 쓰는 것들이다. 리버스 프록시가 같은 기계에 있고 프록시만 앱에 붙는 구성, 관리용 콘솔, 로컬 개발 서버. 이럴 때는 앱이 스스로 잠가 두는 쪽이 안전하다. 매핑을 잘못 적어도 밖으로 새지 않는다.
0.0.0.0 으로 잡을 자리는 다른 기계나 다른 컨테이너가 붙어야 하는 것들이다. 컨테이너에 넣는 웹 서버가 대표적이다. 컨테이너 경계가 이미 한 겹 벽이라, 앱까지 루프백으로 잠가 두면 아무도 못 들어온다.
헷갈릴 때 기준은 하나면 된다. 이 연결이 루프백으로 들어오나, 다른 인터페이스로 들어오나. 컨테이너 밖에서 오는 것, 프록시가 다른 컨테이너에 있는 것, 휴대폰으로 열어 보는 것은 전부 뒤쪽이다.
이런 구성이면 앱 바인딩 노출 범위 조절
--------------------------------------- ----------- -----------------------
같은 기계의 nginx 만 앱에 붙는다 127.0.0.1 따로 안 해도 된다
컨테이너 웹서버, 호스트에서만 쓴다 0.0.0.0 -p 127.0.0.1:8080:80
컨테이너 웹서버, 밖에 공개한다 0.0.0.0 -p 8080:80
같은 랜의 휴대폰으로 개발 서버를 연다 0.0.0.0 랜 밖은 방화벽으로
로컬 개발용으로만 띄운 DB 127.0.0.1 따로 안 해도 된다앱은 어디서 받을지, 매핑은 어디까지 보일지를 나눠 정한다
앱을 잠글지, 매핑을 잠글지 중 하나만 고른다. 양쪽 다 잠그면 아무도 못 붙는다.
조치 후 확인할 것
- 밖에서 안 붙으면 docker exec <컨테이너> ss -ltnp 로 LISTEN 주소부터 본다. 127.0.0.1:포트 로 잡혀 있으면 매핑을 아무리 고쳐도 안 붙는다.
- docker port <컨테이너> 로 호스트 쪽이 0.0.0.0 인지 127.0.0.1 인지 확인한다. 공개할 생각이 없었는데 0.0.0.0 이면 -p 앞에 주소를 붙여 다시 올린다.
- Django·Flask 를 컨테이너에 넣었다면 실행 명령에 0.0.0.0 이 들어가 있는지 본다. 기본값 그대로면 잠긴 채로 뜬다.
- 방화벽으로 막아 뒀다는 것만 믿지 않는다. 다른 기계에서 실제로 붙여 보고 응답이 오는지 확인한다.
127.0.0.1 은 루프백만, 0.0.0.0 은 들어오는 대로 다. 이 한 줄만 손에 익혀 두면 포트 매핑을 헛짚느라 시간을 쓰지 않는다. 어디까지 보이게 할지는 앱이 아니라 매핑에서 정하면 된다.