개발

Docker 컨테이너 외부 접속 안 될 때? 초보 개발자를 위한 네트워크 문제 해결 가이드

ilboard 2026. 6. 9. 22:25

Docker 컨테이너 외부 접속 안 될 때 — 초보 개발자를 위한 네트워크 문제 해결 가이드

30대 개발자가 집 책상 앞에 앉아 모니터 화면을 보면서 머리를 짚고 고민하는 장면. 화면에는 터미널 창과

Docker를 처음 쓰기 시작하면 거의 모두가 한 번씩 겪는 상황이 있다. 분명히 컨테이너는 뜨고 있는데, 브라우저에서 접속하면 "연결 거부"가 뜨거나 아무것도 안 열린다. docker ps를 쳐보면 컨테이너는 멀쩡히 실행 중이고, 컨테이너 내부에서 curl localhost를 치면 응답이 오는데, 외부에서는 접근이 안 된다.

이 글에서는 Docker 컨테이너 외부 접속이 안 되는 원인을 유형별로 정리하고, 각각 어떻게 해결하는지를 설명한다. 원인은 대부분 몇 가지로 좁혀지는데, 알고 나면 별거 아니다.


Docker 네트워크가 기본적으로 격리되는 이유부터 알아야 한다

Docker를 설치하면 docker0이라는 가상 네트워크 인터페이스가 호스트에 생긴다. 컨테이너를 실행하면 이 브리지 네트워크에 연결되고, 컨테이너에는 172.17.0.x 같은 내부 IP가 할당된다.

이 IP는 호스트 내부에서만 의미가 있다. 외부(인터넷, 다른 기기)에서는 172.17.0.2라는 주소가 아무 의미가 없고, 라우팅도 안 된다. 컨테이너는 기본적으로 브리지 네트워크 안에 격리된 존재다.

그래서 외부에서 컨테이너에 접근하려면 호스트의 포트와 컨테이너의 포트를 연결해주는 작업이 필요하다. 이게 바로 포트 포워딩, 또는 포트 바인딩이라고 부르는 것이다.

Docker에서 기본으로 제공하는 네트워크 드라이버는 세 가지다.

드라이버 특징
bridge 기본값. 가상 브리지를 통해 격리된 네트워크 환경 제공
host 호스트 네트워크를 그대로 공유. 포트 포워딩 불필요하지만 격리 없음
none 네트워크 인터페이스 없음. 완전 격리

실무에서는 대부분 bridge를 쓴다. host 모드는 성능이 좋지만 보안 격리가 없어서 운영 환경에선 신중하게 써야 한다.


가장 흔한 원인 1 — -p 옵션 없이 컨테이너를 실행했다

외부 접속이 안 될 때 가장 먼저 확인할 건 포트 바인딩 여부다.

컨테이너를 실행할 때 -p 옵션을 붙이지 않으면 컨테이너 내부에서 서버가 아무리 돌아도 외부에서는 접근할 방법이 없다. 이게 첫 번째 함정이다.

# 잘못된 방법 — 포트 바인딩 없음
docker run nginx

# 올바른 방법 — 호스트 8080 → 컨테이너 80 연결
docker run -p 8080:80 nginx

-p 8080:80의 의미는 호스트의 8080 포트로 들어온 요청을 컨테이너의 80 포트로 전달한다는 뜻이다. 순서가 호스트:컨테이너이기 때문에 처음엔 헷갈릴 수 있다. 왼쪽이 외부에서 접근하는 포트, 오른쪽이 컨테이너 내부 포트다.

실행 중인 컨테이너의 포트 바인딩 상태는 docker ps로 확인할 수 있다.

docker ps
CONTAINER ID   IMAGE   PORTS
a1b2c3d4e5f6   nginx   0.0.0.0:8080->80/tcp

0.0.0.0:8080->80/tcp가 보이면 제대로 바인딩된 것이다. 포트 컬럼이 비어있거나 80/tcp만 있고 호스트 포트가 없으면 외부에서 접근이 안 된다.

이미 실행 중인 컨테이너에는 포트 바인딩을 추가할 수 없다. 컨테이너를 멈추고 -p 옵션을 붙여서 다시 실행해야 한다. 이 부분은 처음에 꽤 불편하게 느껴지는데, docker-compose를 쓰면 이런 상황이 많이 줄어든다.


가장 흔한 원인 2 — 앱이 127.0.0.1에 바인딩되어 있다

포트 바인딩을 제대로 설정했는데도 외부 접속이 안 되는 경우가 있다. 이때는 컨테이너 내부 앱의 바인딩 주소를 확인해야 한다.

Flask, FastAPI, Express 같은 서버 프레임워크를 실행할 때 기본 설정이 127.0.0.1 또는 localhost인 경우가 많다.

# Flask 기본 실행 — 127.0.0.1에만 바인딩됨
app.run()  # 기본값: host="127.0.0.1"

# 외부에서 접근하려면 0.0.0.0으로 변경
app.run(host="0.0.0.0", port=5000)

127.0.0.1은 루프백 주소라서 컨테이너 자기 자신에서만 접근할 수 있다. Docker가 -p 옵션으로 호스트 포트를 컨테이너 포트에 연결해도, 컨테이너 내부 앱이 127.0.0.1만 듣고 있으면 요청이 앱까지 도달하지 못한다.

0.0.0.0으로 설정하면 컨테이너 내부의 모든 네트워크 인터페이스에서 들어오는 요청을 받겠다는 뜻이다. Docker의 포트 포워딩이 정상 동작하려면 앱이 0.0.0.0에 바인딩되어 있어야 한다.

프레임워크별로 설정 방법이 다르다.

# Flask
flask run --host=0.0.0.0

# FastAPI (uvicorn)
uvicorn main:app --host 0.0.0.0 --port 8000

# Node.js Express — 코드에서 listen 설정
app.listen(3000, '0.0.0.0')

# Django
python manage.py runserver 0.0.0.0:8000

실제로 이 문제를 처음 만나면 꽤 당황하게 된다. 포트 바인딩도 했고, 컨테이너도 살아있는데 왜 안 되냐고 한참 찾다가 바인딩 주소 때문이라는 걸 알게 된다. 디버깅 순서에서 이 부분을 빨리 확인하는 게 시간 절약이 된다.


docker-compose의 ports와 expose, 헷갈리는 차이

깔끔한 개발 환경의 모니터 화면에 docker-compose.yml 파일이 열려 있는 장면. 코드 에디터(VS Code 스타일)에 ports와 expose 설정이 나란히 보임. 화면 분위기는 어두운 다크 테마, 집중된 작업 환경.

docker-compose를 쓰다 보면 portsexpose 두 가지 설정이 헷갈리는 경우가 많다. 이름만 보면 비슷해 보이는데 동작 방식이 완전히 다르다.

# docker-compose.yml

services:
  web:
    image: nginx
    ports:
      - "8080:80"   # 호스트 8080 → 컨테이너 80 (외부 접근 가능)

  api:
    image: my-api
    expose:
      - "3000"      # 같은 네트워크 컨테이너끼리만 접근 가능, 외부에서는 접근 불가

ports: 호스트 포트와 컨테이너 포트를 매핑한다. 외부에서 호스트를 통해 컨테이너에 접근할 수 있다. -p 옵션과 동일한 효과다.

expose: 컨테이너끼리 내부 통신할 때만 포트를 열어준다. 호스트와 매핑되지 않기 때문에 외부에서는 접근이 불가능하다. 예를 들어 web 컨테이너가 api 컨테이너를 내부 네트워크로 호출할 때는 expose만 있어도 되지만, 외부(브라우저 등)에서 api에 직접 접근하려면 ports로 열어야 한다.

실무에서는 nginx 같은 리버스 프록시를 앞단에 두고 외부 포트는 nginx만 열고, 내부 서비스는 expose로만 처리하는 패턴을 자주 쓴다. 이렇게 하면 외부에 노출되는 포트를 최소화할 수 있다.

services:
  nginx:
    image: nginx
    ports:
      - "80:80"      # 외부에서 접근 가능

  app:
    image: my-app
    expose:
      - "8000"       # nginx에서만 내부적으로 접근

처음에는 "그냥 다 ports 쓰면 되지 않나"라고 생각할 수 있는데, 외부에 불필요한 포트를 열어두면 보안상 좋지 않다. 규모가 커질수록 이 구분이 중요해진다.


Ubuntu + Docker 조합에서 방화벽(UFW)이 말썽인 경우

Ubuntu 서버에 Docker를 올리고 UFW 방화벽도 쓴다면 특이한 문제를 만날 수 있다. UFW에서 포트를 막았는데도 Docker 컨테이너가 외부에 노출되는 상황, 또는 반대로 UFW 설정을 바꿨는데 Docker 컨테이너 접속이 안 되는 상황이다.

이게 발생하는 이유는 Docker가 iptables를 직접 조작하기 때문이다. UFW도 내부적으로 iptables를 쓰는데, Docker가 자기 방식으로 iptables 규칙을 끼워넣으면서 UFW 설정이 Docker 포트에는 적용되지 않는 상황이 생긴다.

상황 1: UFW로 막았는데도 Docker 포트가 뚫려있다

Docker는 DOCKER-USER 체인을 거치도록 트래픽을 라우팅하는데, UFW 규칙은 이 체인 앞에서 처리되지 않을 수 있다. 해결 방법 중 하나는 /etc/docker/daemon.json을 수정하는 것이다.

{
  "iptables": false
}

단, 이렇게 하면 Docker의 네트워크 기능 일부가 제한될 수 있어서 신중하게 적용해야 한다. 운영 환경에서 쓴다면 ufw-docker 유틸리티를 쓰는 방법도 있다.

상황 2: UFW가 꺼져있는데 외부에서 접속이 안 된다

이 경우는 대부분 클라우드 서버(AWS EC2, GCP, NCP 등)의 인바운드 규칙을 확인해야 한다. 서버 자체 방화벽과 별개로 클라우드 콘솔에서 보안 그룹이나 네트워크 ACL에서 포트를 열어줘야 한다. Docker 설정이 다 맞아도 클라우드 레벨에서 포트가 막혀있으면 접속이 안 된다.

# 현재 UFW 상태 확인
sudo ufw status

# 특정 포트 열기
sudo ufw allow 8080/tcp

# UFW 상태가 inactive면 비활성화 상태
# 이 경우 클라우드 보안 그룹 설정을 확인

컨테이너 간 통신이 안 될 때 — 같은 네트워크에 있는지 확인

외부 접속이 아니라 컨테이너끼리 통신이 안 되는 경우도 많다. 예를 들어 web 컨테이너에서 db 컨테이너로 연결이 안 된다거나, api에서 redis로 연결이 안 되는 상황이다.

Docker 컨테이너는 같은 네트워크에 속해있어야 서로 통신할 수 있다. docker-compose를 쓰면 기본적으로 같은 파일에 정의된 서비스들이 자동으로 같은 네트워크에 묶인다. 그래서 docker-compose를 쓰면 이 문제가 덜 발생한다.

docker run으로 각각 컨테이너를 띄웠다면 수동으로 같은 네트워크에 연결해줘야 한다.

# 사용자 정의 네트워크 생성
docker network create my-network

# 각 컨테이너를 해당 네트워크에 연결해서 실행
docker run --network my-network --name db postgres
docker run --network my-network --name web my-web-app

같은 네트워크에 속한 컨테이너끼리는 IP 대신 컨테이너 이름으로 통신할 수 있다. 예를 들어 web 컨테이너에서 db 컨테이너에 접근할 때 db:5432처럼 이름을 호스트명으로 쓸 수 있다. 이게 사용자 정의 네트워크의 편한 점인데, 기본 bridge 네트워크에서는 컨테이너 이름 기반 DNS가 동작하지 않는다.

현재 컨테이너가 어떤 네트워크에 속해있는지 확인하는 방법은 다음과 같다.

# 컨테이너 네트워크 정보 확인
docker inspect <컨테이너_이름> | grep -A 20 "Networks"

# 특정 네트워크에 속한 컨테이너 목록 확인
docker network inspect my-network

실전 디버깅 순서와 자주 하는 실수 정리

외부 접속이 안 될 때 순서대로 확인하면 대부분 원인을 찾을 수 있다.

1단계: 컨테이너가 실행 중인가

docker ps

STATUS 컬럼에서 Up이 표시되어야 한다. Exited라면 컨테이너가 종료된 것이다. 로그를 확인한다.

docker logs <컨테이너_이름>

2단계: 포트 바인딩이 되어있는가

docker ps 결과의 PORTS 컬럼에 0.0.0.0:{호스트포트}->{컨테이너포트}/tcp 형식이 있는지 확인한다. 없으면 -p 옵션을 붙여서 다시 실행해야 한다.

3단계: 컨테이너 내부에서 앱이 응답하는가

# 컨테이너 내부 진입
docker exec -it <컨테이너_이름> bash

# 내부에서 직접 요청
curl http://localhost:8000

내부에서는 응답이 오는데 외부에서 안 된다면 앱의 바인딩 주소가 127.0.0.1일 가능성이 높다.

4단계: 호스트에서 접근해보기

# 호스트에서 직접 요청 (서버에 SSH 접속한 상태에서)
curl http://localhost:8080

호스트에서는 되는데 외부에서 안 된다면 방화벽 또는 클라우드 보안 그룹 설정을 확인한다.

자주 하는 실수 목록

  • -p 옵션에서 포트 순서를 반대로 쓰는 것 (컨테이너:호스트로 쓰는 경우)
  • 앱이 127.0.0.1에 바인딩되어 있는 것 확인 안 한 채 Docker 탓만 하는 것
  • docker-compose에서 expose만 쓰고 ports를 안 쓰는 것
  • 클라우드 서버 보안 그룹에서 포트를 안 열어놓은 것
  • 포트를 변경했는데 컨테이너를 재시작하지 않은 것

마무리

Docker 컨테이너 외부 접속 문제는 대부분 네 가지 안에서 원인이 나온다. 포트 바인딩 누락, 앱의 바인딩 주소 문제, docker-compose 설정 혼동, 방화벽·보안 그룹 설정. 이 네 가지를 순서대로 확인하는 습관만 들이면 대부분 해결된다.

처음에는 Docker 네트워크 구조가 낯설어서 원인을 찾기가 어렵지만, 한 번 이해하고 나면 같은 실수를 반복하지 않는다. 컨테이너가 기본적으로 격리된 환경이라는 걸 머릿속에 두고, 외부 접속이 필요할 때마다 "포트 연결을 어떻게 열었나"를 먼저 확인하는 게 시작점이다.

앞으로 docker-compose로 멀티 컨테이너 환경을 구성하거나, nginx를 앞단에 두는 리버스 프록시 구성을 다루게 되면 이 네트워크 개념이 더 자주 쓰인다.


Sources:
- Docker Container 외부 접속 - velog
- Docker 컨테이너에서 외부 인터넷 접속이 되지 않을때 - albear.app
- Host와 Container 내부의 app들이 통신이 안되는 이유 - velog
- Docker 컨테이너 네트워크 설정 포트포워딩 - wikidocs
- Docker 네트워크 사용법 - Dale Seo
- Docker Container 외부 접속 Port Binding - velog
- ports와 expose 차이 - velog
- UFW + Docker 방화벽 문제 - velog
- Docker 네트워크 완전 이해 bridge host overlay - understandkorea
- Dockerfile에서 포트포워딩을 제대로 했는데 웹앱 접속이 안 되는 문제 - siyeon-park