본문 바로가기
Docker

Docker 8회차 : 컨테이너 네트워크 (포트, 브리지 네트워크, DNS)

by 마틴블레이크 2026. 1. 1.
반응형

Docker 8회차 : 컨테이너 네트워크 (포트, 브리지 네트워크, DNS)

컨테이너를 운영하거나 실습할 때 가장 많이 막히는 지점은 네트워크입니다. 웹 컨테이너를 띄웠는데 브라우저가 안 열리고, DB 컨테이너는 떠 있는데 웹이 접속을 못 하고, 심지어 같은 컴퓨터에서 돌리는데도 “localhost” 때문에 혼란이 생깁니다.

이번 포스팅에서는 초보자 관점에서 네트워크를 딱 두 가지로 나눠 정리합니다. (1) 호스트에서 컨테이너로 들어가는 길(포트 매핑 -p 8080:80), (2) 같은 네트워크 안에서 컨테이너끼리 통신하는 길(브리지 네트워크 + 서비스 이름 기반 DNS)입니다. 마지막에는 실무에서 그대로 쓰는 네트워크 트러블슈팅 체크리스트를 산출물로 제공합니다.

요약

  • 포트 매핑 -p 8080:80은 “호스트 8080으로 들어온 트래픽을 컨테이너 80으로 전달”하는 의미입니다.
  • 컨테이너끼리 통신하려면 “같은 네트워크”에 넣고, 보통 컨테이너 이름으로 DNS가 동작하도록 사용자 정의 브리지 네트워크를 사용합니다.
  • “localhost 착각”은 컨테이너 내부의 localhost가 호스트가 아니라 그 컨테이너 자신을 가리킨다는 점에서 발생합니다.

목차

핵심 포인트

  • -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가 자연스럽게 적용).

 

이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

반응형