
Docker 8회차 : 컨테이너 네트워크 (포트, 브리지 네트워크, DNS)
컨테이너를 운영하거나 실습할 때 가장 많이 막히는 지점은 네트워크입니다. 웹 컨테이너를 띄웠는데 브라우저가 안 열리고, DB 컨테이너는 떠 있는데 웹이 접속을 못 하고, 심지어 같은 컴퓨터에서 돌리는데도 “localhost” 때문에 혼란이 생깁니다.
이번 포스팅에서는 초보자 관점에서 네트워크를 딱 두 가지로 나눠 정리합니다. (1) 호스트에서 컨테이너로 들어가는 길(포트 매핑 -p 8080:80), (2) 같은 네트워크 안에서 컨테이너끼리 통신하는 길(브리지 네트워크 + 서비스 이름 기반 DNS)입니다. 마지막에는 실무에서 그대로 쓰는 네트워크 트러블슈팅 체크리스트를 산출물로 제공합니다.
요약
- 포트 매핑
-p 8080:80은 “호스트 8080으로 들어온 트래픽을 컨테이너 80으로 전달”하는 의미입니다. - 컨테이너끼리 통신하려면 “같은 네트워크”에 넣고, 보통 컨테이너 이름으로 DNS가 동작하도록 사용자 정의 브리지 네트워크를 사용합니다.
- “localhost 착각”은 컨테이너 내부의 localhost가 호스트가 아니라 그 컨테이너 자신을 가리킨다는 점에서 발생합니다.
목차
- 핵심 포인트
- 포트 매핑(-p 8080:80)의 의미
- 브리지 네트워크와 서비스 이름 기반 DNS
- “localhost 착각” 완전 정리
- 실습: Web 컨테이너 → DB 컨테이너 접속 구성 만들기
- 산출물: 네트워크 트러블슈팅 체크리스트
- 추가로 생각해볼 점
- 블로그 최적화 정보
핵심 포인트
-p 호스트포트:컨테이너포트는 “호스트에서 컨테이너로 들어가는 문”입니다(브라우저/외부 접근용).- 컨테이너끼리는 보통
-p없이도 통신합니다. 대신 “같은 네트워크”에 있어야 합니다. - 사용자 정의 브리지 네트워크에서는 컨테이너 이름(서비스 이름)으로 DNS가 동작해
db같은 호스트명을 사용할 수 있습니다. - 컨테이너 내부에서
localhost는 “그 컨테이너 자신”입니다. DB를localhost로 접속하려 하면 대부분 실패합니다.
포트 매핑(-p 8080:80)의 의미
docker run -p 8080:80 nginx에서 가장 중요한 것은 콜론(:) 양쪽이 “서로 다른 세계”라는 점입니다.
| 표기 | 의미 | 어디에서 접속? | 예시 |
|---|---|---|---|
-p 8080:80 |
호스트 8080 → 컨테이너 80으로 전달 | 호스트(브라우저, curl)에서 | http://localhost:8080 접속 |
-p 127.0.0.1:8080:80 |
호스트의 로컬(127.0.0.1)에만 바인딩 | 같은 PC에서만 | 외부에서 접근 차단(개발용에 유용) |
여기서 자주 생기는 오해가 하나 더 있습니다. “컨테이너가 80으로 열려 있으니, 호스트에서 80으로 접속하면 되지 않을까?”라고 생각하는 경우입니다. 하지만 호스트에서 접속할 주소는 호스트 포트입니다. 컨테이너 포트(80)는 컨테이너 네임스페이스 안의 포트이므로, 밖에서 접근하려면 반드시 -p로 연결해야 합니다.
포트 매핑이 있어도 접속이 안 될 때 가장 흔한 원인
- 호스트 포트가 이미 사용 중(포트 충돌)입니다.
- 컨테이너 내부 프로세스가
127.0.0.1로만 바인딩되어 외부(컨테이너 밖)에서 접근이 안 됩니다.
초보자 실습에서는 가능한0.0.0.0바인딩을 사용하시는 것이 안전합니다. - 컨테이너가 실제로는 해당 포트에서 리스닝하고 있지 않습니다(앱 미실행/실행 실패).
브리지 네트워크와 서비스 이름 기반 DNS
컨테이너끼리 통신은 “호스트를 거치지 않고” 같은 네트워크 안에서 직접 이뤄지는 것이 일반적입니다. 예를 들어 웹이 DB에 붙는 경우, DB를 굳이 -p 5432:5432로 호스트에 공개할 필요가 없습니다. 같은 네트워크에서 웹이 DB로 직접 접근하면 됩니다.
왜 “사용자 정의 브리지 네트워크”를 쓰나
- 컨테이너들을 같은 네트워크로 묶어 의도한 서비스끼리만 통신하게 만들 수 있습니다.
- 같은 네트워크 안에서는 컨테이너 이름(서비스 이름)으로 DNS가 동작해 IP를 외울 필요가 없습니다.
예: 웹에서 DB를db라는 이름으로 접속 - 운영 관점에서 “구성”이 명확해지고, 트러블슈팅도 쉬워집니다.
정리하면, “호스트에서 웹에 접속”할 때는 포트 매핑이 필요하고, “웹에서 DB에 접속”할 때는 같은 네트워크 + 서비스 이름 DNS를 쓰는 것이 기본 패턴입니다.
“localhost 착각” 완전 정리
네트워크에서 가장 많이 헷갈리는 문장이 바로 이겁니다. “DB 주소를 localhost로 넣었는데 왜 안 되지?”
| 어디에서 보는 localhost? | localhost가 가리키는 대상 | 결과(예시) |
|---|---|---|
| 호스트(내 PC) | 내 PC(호스트) | http://localhost:8080은 호스트 8080으로 접속 |
| 컨테이너 내부 | 그 컨테이너 자신 | 웹 컨테이너에서 localhost:5432는 “웹 컨테이너 내부의 5432”를 의미 |
즉, 웹 컨테이너에서 DB에 접속하려면 localhost가 아니라 DB 컨테이너 이름(예: db)을 사용해야 합니다. 이게 이번 실습의 핵심 목표입니다.
실습: Web 컨테이너가 DB 컨테이너로 접속하는 구성 만들기
실습 목표는 단순합니다. 사용자 정의 브리지 네트워크(app-net)를 만들고, 그 안에 DB 컨테이너(db)와 Web 컨테이너(web)를 넣습니다. Web은 간단한 HTTP 서버를 띄우고, 요청이 오면 DB로 TCP 연결이 되는지를 확인해 결과를 보여줍니다(드라이버 설치 없이도 “네트워크 연결” 자체를 검증할 수 있어 초보자에게 적합합니다).
0) 준비: 작업 폴더 만들기
mkdir docker-net-demo
cd docker-net-demo
1) 사용자 정의 브리지 네트워크 생성
docker network create app-net
docker network ls | head
2) DB 컨테이너 실행(네트워크에만 연결, 포트 매핑 없음)
DB는 “웹 컨테이너만 접근”할 것이므로, 이번 실습에서는 호스트에 포트를 공개하지 않습니다. 이 패턴이 운영에서도 기본에 가깝습니다.
docker run -d --name db --network app-net \
-e POSTGRES_PASSWORD=pass \
-e POSTGRES_DB=appdb \
postgres:16-alpine
docker ps
3) Web 컨테이너용 간단 서버(app.py) 작성
아래 파일을 app.py로 저장합니다. 이 서버는 /check 요청이 오면 DB_HOST:DB_PORT로 TCP 연결을 시도해 성공/실패를 보여줍니다.
import os
import socket
from http.server import BaseHTTPRequestHandler, HTTPServer
DB_HOST = os.environ.get("DB_HOST", "db")
DB_PORT = int(os.environ.get("DB_PORT", "5432"))
LISTEN_PORT = int(os.environ.get("PORT", "8000"))
def try_connect(host: str, port: int, timeout: float = 2.0):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(timeout)
try:
s.connect((host, port))
return True, "connected"
except Exception as e:
return False, f"failed: {e}"
finally:
try:
s.close()
except Exception:
pass
class Handler(BaseHTTPRequestHandler):
def _send(self, code: int, body: str):
self.send_response(code)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.end_headers()
self.wfile.write(body.encode("utf-8"))
def do_GET(self):
if self.path == "/" or self.path.startswith("/help"):
msg = (
"Docker Network Demo\n"
f"- DB_HOST={DB_HOST}\n"
f"- DB_PORT={DB_PORT}\n"
"Try: /check\n"
)
return self._send(200, msg)
if self.path.startswith("/check"):
ok, detail = try_connect(DB_HOST, DB_PORT)
if ok:
return self._send(200, f"OK: {DB_HOST}:{DB_PORT} {detail}\n")
return self._send(500, f"NG: {DB_HOST}:{DB_PORT} {detail}\n")
return self._send(404, "Not Found\n")
if __name__ == "__main__":
print(f"Listening on 0.0.0.0:{LISTEN_PORT} (DB={DB_HOST}:{DB_PORT})")
HTTPServer(("0.0.0.0", LISTEN_PORT), Handler).serve_forever()
4) Web 컨테이너 실행(포트 매핑 + 같은 네트워크 + DB_HOST=db)
Web은 호스트에서 브라우저로 접근해야 하므로 -p 8080:8000으로 포트를 공개합니다. 그리고 DB에 접근해야 하므로 --network app-net에 넣고, DB_HOST=db(DB 컨테이너 이름)를 사용합니다.
docker run -d --name web --network app-net \
-p 8080:8000 \
-e DB_HOST=db -e DB_PORT=5432 \
-v "$(pwd)/app.py:/app/app.py:ro" -w /app \
python:3.12-slim python app.py
docker ps
브라우저에서 아래로 접속해 확인합니다.
http://localhost:8080/(환경변수 안내)http://localhost:8080/check(DB 연결 확인)
5) 서비스 이름 기반 DNS가 실제로 동작하는지 확인
“web 컨테이너에서 db라는 이름이 어떻게 IP로 바뀌는지”를 한 줄로 확인해 보겠습니다(파이썬 표준 라이브러리로 DNS 확인).
docker exec web python -c "import socket; print('db ->', socket.gethostbyname('db'))"
여기서 출력되는 IP는 “app-net 내부에서의 db 컨테이너 IP”입니다. 즉, 웹은 IP를 몰라도 db라는 서비스 이름만 알고 있으면 접속할 수 있습니다.
6) “localhost 착각” 문제를 일부러 만들고 이해하기
이제 일부러 실패하게 만들어 보겠습니다. DB 호스트를 localhost로 바꾸면, 웹 컨테이너는 “자기 자신”의 5432로 붙으려고 하므로 대부분 실패합니다.
docker rm -f web
docker run -d --name web --network app-net \
-p 8080:8000 \
-e DB_HOST=localhost -e DB_PORT=5432 \
-v "$(pwd)/app.py:/app/app.py:ro" -w /app \
python:3.12-slim python app.py
이제 http://localhost:8080/check를 열면 NG가 나올 가능성이 큽니다. 해결은 간단합니다. DB_HOST를 다시 db로 되돌립니다.
docker rm -f web
docker run -d --name web --network app-net \
-p 8080:8000 \
-e DB_HOST=db -e DB_PORT=5432 \
-v "$(pwd)/app.py:/app/app.py:ro" -w /app \
python:3.12-slim python app.py
7) 실습 정리(컨테이너/네트워크 삭제)
docker rm -f web db
docker network rm app-net
산출물: 네트워크 트러블슈팅 체크리스트(포트/방화벽/바인딩/네트워크)
아래 체크리스트는 “안 된다”를 빠르게 “어디가 안 되는지”로 쪼개는 데 목적이 있습니다. 초보자일수록 이 순서대로만 점검해도 해결 속도가 크게 올라갑니다.
| 증상 | 가장 흔한 원인 | 확인 명령 | 해결 힌트 |
|---|---|---|---|
| 호스트에서 웹 접속이 안 됨 | 포트 매핑 누락/오타, 포트 충돌 | docker ps(PORTS), docker port web |
-p 호스트:컨테이너 확인, 호스트 포트 변경(8081 등) |
| 컨테이너는 떠 있는데 서비스가 응답 없음 | 프로세스 미실행, 내부 바인딩(127.0.0.1) | docker logs web, docker exec web ps aux |
앱이 0.0.0.0로 리스닝하는지 확인 |
| 웹이 DB에 접속 못 함 | 다른 네트워크, 잘못된 호스트명(localhost) | docker network inspect app-net, docker exec web python -c ... |
같은 네트워크에 넣고 DB_HOST를 db로 |
| 외부에서만 접속 안 됨 | 호스트 방화벽/보안그룹, 바인딩 제한 | 호스트 방화벽 설정 확인, -p 127.0.0.1:... 여부 확인 |
로컬 바인딩을 전체 바인딩으로 변경(필요 시), 보안 정책 점검 |
체크리스트(짧게 요약)
- 1) 컨테이너가 떠 있나?
docker ps로 Running 확인 - 2) 호스트→컨테이너라면 포트 매핑이 있나?
docker ps의 PORTS 확인 - 3) 컨테이너가 실제로 리스닝하나? 로그(
docker logs)와 프로세스(docker exec ... ps aux) 확인 - 4) 컨테이너→컨테이너라면 같은 네트워크인가?
docker network inspect로 두 컨테이너가 포함됐는지 확인 - 5) DB 주소가 localhost로 되어 있지 않나? 컨테이너 내부에서
localhost는 “자기 자신”임을 재확인 - 6) 바인딩 문제는 아닌가? 앱이
0.0.0.0로 바인딩하는지 점검
추가로 생각해볼 점
- DB를 호스트에 공개하지 않는 것이 기본: DB는 같은 네트워크 안에서만 열고, 웹만
-p로 공개하면 보안과 운영이 쉬워집니다. - “이름으로 붙는다”는 습관: 컨테이너 IP는 바뀔 수 있지만, 서비스 이름(
db)은 구성 안에서 안정적으로 유지됩니다. - 다음 단계(Compose)에서 완성: 지금 만든 구성은
docker compose로 옮기면 더 명확해집니다(같은 네트워크, 서비스 이름 DNS가 자연스럽게 적용).
이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
'Docker' 카테고리의 다른 글
| Docker 10회차 : 운영 관점 기초 (보안/환경변수/헬스체크/리소스/정리) (0) | 2026.01.01 |
|---|---|
| Docker 9회차 : Docker Compose 입문 (멀티 컨테이너 표준 운영) (0) | 2026.01.01 |
| Docker 7회차 : 데이터 보존 (Volumes & Bind Mount) (0) | 2026.01.01 |
| Docker 6회차 : Dockerfile 기초 (나만의 이미지 만들기) (0) | 2026.01.01 |
| Docker 5회차 : 이미지 사용 전략 (pull, tag, inspect, history) (0) | 2026.01.01 |