Docker로 프론트엔드 개발 서버나 백엔드 서버를 띄우다 보면 이런 상황을 자주 만난다.
컨테이너는 정상적으로 실행 중인데,
브라우저에서localhost:3000또는localhost:5173으로 접속하면 열리지 않는다.
컨테이너 로그를 보면 서버는 분명히 떠 있다.
터미널에는 ready, listening, server running 같은 메시지도 나온다.
그런데 내 브라우저에서는 접속이 안 된다.
이 문제는 보통 애플리케이션 코드 문제가 아니라,
포트, port mapping, localhost, 0.0.0.0의 의미를 헷갈려서 생긴다.
Docker에서 서버 접속 문제를 이해하려면 “컨테이너 안의 localhost는 누구인가?”부터 정리해야 한다.
이 글에서는 컨테이너 내부 포트와 호스트 포트의 차이, -p 8080:3000의 의미, EXPOSE와 ports의 차이, 그리고 Vite dev server에서 --host 0.0.0.0이 필요한 이유를 정리한다.
1. 포트는 하나의 컴퓨터 안에서 프로세스를 구분하는 번호다
먼저 포트의 의미부터 정리해보자.
포트는 하나의 컴퓨터 안에서 네트워크 요청을 어떤 프로세스에게 전달할지 구분하는 번호다.
예를 들어 내 컴퓨터에서 여러 서버가 동시에 실행되고 있다고 해보자.
My Computer
├── React Dev Server : 5173번 포트
├── Backend API Server: 8080번 포트
└── Database Server : 3306번 포트브라우저에서 다음 주소로 접속하면:
http://localhost:5173운영체제는 5173번 포트를 보고, 이 요청을 해당 포트를 사용 중인 프로세스에게 전달한다.
즉, 포트는 네트워크 요청이 도착했을 때
같은 컴퓨터 안의 여러 프로세스 중 누구에게 보낼지 결정하는 번호다.
IP 주소가 “어느 컴퓨터로 갈 것인가”를 나타낸다면,
포트 번호는 “그 컴퓨터 안의 어느 프로세스로 갈 것인가”를 나타낸다.
더 자세히 보기: localhost는 무엇일까?
localhost는 현재 자기 자신을 가리키는 이름이다. 일반적으로 127.0.0.1이라는 IP 주소를 의미한다.
localhost
= 127.0.0.1
= 자기 자신예를 들어 내 MacBook에서 다음 주소에 접속한다고 해보자.
http://localhost:3000이때 localhost는 내 MacBook 자기 자신을 의미한다. 따라서 내 MacBook의 3000번 포트에서 실행 중인 프로세스를 찾는다.
하지만 Docker Container 안에서 localhost를 사용하면 의미가 달라진다. 컨테이너 안의 localhost는 내 MacBook이 아니라 컨테이너 자기 자신이다.
이 차이가 Docker 네트워크 문제를 이해하는 핵심이다.
2. 컨테이너 내부 포트와 호스트 포트는 다르다
Docker Container는 호스트 머신 위에서 실행되지만, 네트워크 관점에서는 독립된 환경을 가진다.
컨테이너는 자기만의 네트워크 공간을 가진 것처럼 동작하고, 그 안에서 프로세스가 특정 포트를 사용한다.
Host Machine
└── Docker Container
└── App Server
└── 3000번 포트에서 실행여기서 중요한 점은 컨테이너 내부의 3000번 포트가 곧바로 호스트 머신의 3000번 포트와 같지 않다는 것이다.
컨테이너 내부 포트와 호스트 포트는 서로 다른 네트워크 공간에 있다.
예를 들어 컨테이너 안에서 서버가 3000번 포트로 실행 중이라고 해보자.
Container
└── App Server
└── listens on port 3000그렇다고 해서 내 MacBook 브라우저에서 바로 다음 주소로 접속할 수 있는 것은 아니다.
http://localhost:3000왜냐하면 내 브라우저의 localhost는 호스트 머신, 즉 내 MacBook을 가리키기 때문이다.
브라우저는 내 MacBook의 3000번 포트를 찾는다. 하지만 서버는 MacBook의 3000번 포트가 아니라 컨테이너 내부의 3000번 포트에서 실행 중이다.
Browser on Host
└── localhost:3000
└── Host Machine의 3000번 포트를 찾음
Container
└── localhost:3000
└── Container 자기 자신의 3000번 포트를 찾음따라서 호스트에서 컨테이너 내부 서버로 접근하려면 호스트 포트와 컨테이너 포트를 연결해줘야 한다.
이때 사용하는 것이 port mapping이다.
3. -p 8080:3000의 의미
Docker에서 포트 매핑은 -p 옵션으로 설정한다.
docker run -p 8080:3000 my-app이 명령의 의미는 다음과 같다.
-p 8080:3000
왼쪽 8080 = Host Port
오른쪽 3000 = Container Port즉, 호스트 머신의 8080번 포트로 들어온 요청을 컨테이너 내부의 3000번 포트로 전달하겠다는 뜻이다.
Browser
└── http://localhost:8080
↓
Host Machine
└── 8080번 포트
↓ port mapping
Docker Container
└── 3000번 포트
└── App Server따라서 다음처럼 실행했다면:
docker run -p 8080:3000 my-app브라우저에서는 다음 주소로 접속해야 한다.
http://localhost:8080컨테이너 내부 서버가 3000번 포트에서 실행 중이더라도, 호스트에서 접근할 때는 매핑된 호스트 포트인 8080을 사용한다.
-p 호스트포트:컨테이너포트순서로 읽으면 된다.
- 왼쪽 포트는 내 컴퓨터, 즉 호스트에서 열리는 포트다.
- 오른쪽 포트는 컨테이너 내부에서 서버가 실제로 사용하는 포트다.
- 브라우저에서는 왼쪽 호스트 포트로 접속한다.
- Docker는 그 요청을 오른쪽 컨테이너 포트로 전달한다.
더 자세히 보기: docker compose에서는 어떻게 쓸까?
docker compose에서는 ports 옵션으로 포트 매핑을 설정한다.
services:
app:
image: my-app
ports:
- "8080:3000"이 의미도 동일하다.
ports:
- "8080:3000"
왼쪽 8080 = Host Port
오른쪽 3000 = Container Port따라서 브라우저에서는 다음 주소로 접속한다.
http://localhost:8080컨테이너 내부에서 서버는 여전히 3000번 포트로 실행 중이다. 다만 호스트에서 접근할 때는 8080번 포트를 통해 들어간다.
4. EXPOSE와 ports는 다르다
Dockerfile을 보면 다음과 같은 명령을 자주 볼 수 있다.
EXPOSE 3000처음에는 EXPOSE 3000을 쓰면 자동으로 호스트에서 3000번 포트로 접속할 수 있다고 생각하기 쉽다.
하지만 그렇지 않다.
EXPOSE는 포트를 실제로 외부에 열어주는 명령이 아니다. 이 컨테이너가 어떤 포트를 사용할 예정인지 알려주는 문서화에 가까운 설정이다.
즉, Dockerfile에 다음처럼 작성해도:
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
EXPOSE 3000
CMD ["npm", "start"]실제로 호스트에서 접근하려면 docker run 시점에 -p 옵션을 지정해야 한다.
docker run -p 8080:3000 my-appEXPOSE 3000은 “이 컨테이너는 3000번 포트를 사용한다”는 정보를 남긴다.
반면 -p 8080:3000은 “호스트 8080번 포트를 컨테이너 3000번 포트로 연결한다”는 실제 네트워크 설정을 만든다.
| 구분 | EXPOSE | -p / ports |
|---|---|---|
| 위치 | Dockerfile | docker run 또는 docker compose |
| 역할 | 컨테이너가 사용할 포트 문서화 | 호스트 포트와 컨테이너 포트 연결 |
| 실제 외부 접근 | 직접 열지 않음 | 외부 접근 가능하게 함 |
| 예시 | EXPOSE 3000 | -p 8080:3000 |
- EXPOSE는 “이 포트를 쓸 예정”이라는 선언이고,
- ports는 “이 포트를 실제로 연결”하는 설정이다.
5. 컨테이너 안의 localhost는 호스트가 아니다
Docker에서 가장 헷갈리는 부분이 바로 localhost다.
일반적으로 로컬 개발을 할 때는 다음처럼 생각한다.
내 MacBook
└── localhost:5173
└── Vite Dev Server이때 localhost는 내 MacBook 자기 자신이다.
하지만 컨테이너 안에서는 다르다.
Docker Container
└── localhost
└── Container 자기 자신즉, 컨테이너 내부에서 localhost는 호스트 머신이 아니다. 컨테이너 자기 자신이다.
내 브라우저에서의 localhost는 내 MacBook이고, 컨테이너 안에서의 localhost는 컨테이너 자기 자신이다.
이 차이를 모르면 다음과 같은 상황이 생긴다.
Container 안에서 서버 실행
└── localhost:5173에 바인딩됨
Host 브라우저에서 접속
└── http://localhost:5173 접속 시도
결과
└── 접속 안 됨서버가 컨테이너 안에서 localhost에만 바인딩되어 있으면,
그 서버는 컨테이너 내부에서만 접근 가능한 상태가 될 수 있다.
호스트에서 port mapping을 해도, 애플리케이션 서버가 컨테이너 외부 네트워크 인터페이스에서 들어오는 요청을 받지 않으면 접속이 되지 않을 수 있다.
이때 필요한 개념이 0.0.0.0이다.
6. 0.0.0.0은 모든 네트워크 인터페이스에서 요청을 받겠다는 의미다
서버를 실행할 때는 보통 어떤 주소에 바인딩할지 결정한다.
예를 들어 서버가 127.0.0.1 또는 localhost에 바인딩되면, 자기 자신으로 들어오는 요청만 받는다.
Server listens on 127.0.0.1:5173
의미
└── 자기 자신에서 오는 요청만 받음반면 0.0.0.0에 바인딩하면 모든 네트워크 인터페이스에서 들어오는 요청을 받을 수 있다.
Server listens on 0.0.0.0:5173
의미
└── 가능한 모든 네트워크 인터페이스에서 요청을 받음Docker 컨테이너 안에서 서버를 외부에서 접근 가능하게 하려면 서버가 localhost가 아니라 0.0.0.0에 바인딩되어 있어야 하는 경우가 많다.
-
localhost또는127.0.0.1→ 자기 자신에서 오는 요청만 받는다. -
0.0.0.0→ 가능한 모든 네트워크 인터페이스에서 들어오는 요청을 받는다.
Docker 컨테이너 안에서는 외부에서 들어오는 요청을 받기 위해 서버를 0.0.0.0에 바인딩해야 하는 경우가 많다.
중요한 점은 0.0.0.0이 브라우저에서 접속할 주소라는 뜻은 아니라는 점이다.
브라우저에서는 여전히 다음처럼 접속한다.
http://localhost:5173또는 port mapping을 다르게 했다면:
http://localhost:80800.0.0.0은 서버가 “어디에서 들어오는 요청을 받을 것인가”를 설정하는 바인딩 주소다. 브라우저에서 입력하는 접속 주소가 아니다.
0.0.0.0은 접속 주소라기보다, 서버가 외부 요청을 받을 수 있도록 열어두는 바인딩 주소다.
더 자세히 보기: 127.0.0.1, localhost, 0.0.0.0
127.0.0.1, localhost, 0.0.0.0은 비슷해 보이지만 의미가 다르다.
| 주소 | 의미 | Docker에서 주의할 점 |
|---|---|---|
localhost | 자기 자신을 가리키는 이름 | 컨테이너 안에서는 컨테이너 자기 자신 |
127.0.0.1 | loopback 주소 | 외부 네트워크 인터페이스 요청을 받지 않음 |
0.0.0.0 | 모든 네트워크 인터페이스 | 컨테이너 외부에서 들어오는 요청을 받을 때 필요 |
localhost와 127.0.0.1은 보통 자기 자신만을 의미한다. 반면 0.0.0.0은 서버가 여러 인터페이스에서 요청을 받을 수 있게 한다.
Docker에서 외부 접근이 필요한 서버는
localhost가 아니라0.0.0.0에 바인딩해야 하는 경우가 많다.
7. Vite dev server에서 --host 0.0.0.0이 필요한 이유
프론트엔드 개발에서 이 문제를 가장 자주 만나는 사례가 Vite dev server다.
Vite 프로젝트를 Docker Container 안에서 실행한다고 해보자.
npm run devVite는 기본 설정에서 개발 서버를 localhost에 바인딩할 수 있다. 로컬 PC에서 직접 실행할 때는 문제가 없다.
MacBook
└── Vite Dev Server
└── localhost:5173브라우저도 같은 MacBook에서 실행되기 때문에 다음 주소로 접속하면 된다.
http://localhost:5173하지만 Docker Container 안에서 Vite dev server를 실행하면 상황이 달라진다.
MacBook
└── Docker Container
└── Vite Dev Server
└── localhost:5173- 여기서 Vite의
localhost는 MacBook이 아니라 컨테이너 자기 자신이다. - 즉, Vite dev server가 컨테이너 내부의 loopback 주소에만 바인딩될 수 있다.
- 이 경우 호스트에서 포트를 매핑해도 브라우저에서 접속이 안 될 수 있다.
services:
frontend:
build: .
ports:
- "5173:5173"
command: npm run dev포트 매핑은 되어 있지만,
Vite dev server가 컨테이너 안의 localhost에만 바인딩되어 있으면 외부 요청을 받지 못할 수 있다.
이때는 Vite dev server를 0.0.0.0으로 열어야 한다.
npm run dev -- --host 0.0.0.0또는 package.json script를 다음처럼 작성할 수 있다.
{
"scripts": {
"dev": "vite --host 0.0.0.0"
}
}그리고 compose에서는 다음처럼 사용할 수 있다.
services:
frontend:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "5173:5173"
volumes:
- .:/app
- /app/node_modules
command: npm run dev이제 브라우저에서는 다음 주소로 접속한다.
http://localhost:5173여기서 전체 흐름은 다음과 같다.
Browser
└── http://localhost:5173
↓
Host Machine
└── 5173번 포트
↓ port mapping
Docker Container
└── 5173번 포트
└── Vite Dev Server
└── 0.0.0.0:5173에 바인딩Vite가 컨테이너 안에서 localhost에만 바인딩되면, 컨테이너 외부에서 들어오는 요청을 받지 못할 수 있다.
Docker 환경에서 호스트 브라우저가 접근하려면:
- Docker에서 포트 매핑을 해야 하고
- Vite dev server는
0.0.0.0에 바인딩되어 있어야 한다.
즉, Docker 개발 환경에서는 다음 두 조건을 함께 확인해야 한다.
1. 포트 매핑
Host 5173 → Container 5173
2. 서버 바인딩
Vite Dev Server → 0.0.0.0:5173둘 중 하나라도 빠지면 컨테이너는 실행 중인데 브라우저에서 접속이 안 되는 상황이 생길 수 있다.
8. 컨테이너는 떴는데 접속이 안 될 때 확인할 것
컨테이너는 실행 중인데 브라우저에서 접속이 안 된다면 다음 순서로 확인하면 된다.
1. 컨테이너가 실행 중인지 확인
docker ps컨테이너가 실행 중인지, 어떤 포트가 매핑되어 있는지 확인한다.
0.0.0.0:5173->5173/tcp이런 형태가 보이면 호스트 5173번 포트가 컨테이너 5173번 포트로 매핑되어 있다는 뜻이다.
2. 포트 매핑이 되어 있는지 확인
docker run을 사용한다면 -p 옵션이 있는지 확인한다.
docker run -p 5173:5173 my-vite-appdocker compose를 사용한다면 ports 설정이 있는지 확인한다.
services:
frontend:
ports:
- "5173:5173"3. 서버가 컨테이너 내부에서 어느 포트로 실행 중인지 확인
컨테이너 내부 서버가 실제로 몇 번 포트에서 실행되는지 확인해야 한다.
예를 들어 서버가 컨테이너 내부에서 3000번 포트로 실행 중인데
호스트 5173번 포트를 컨테이너 5173번 포트로 연결하면 접속되지 않는다.
App Server
└── Container port 3000에서 실행 중
ports
└── "5173:5173"
결과
└── 컨테이너 5173번 포트에는 서버가 없음이 경우에는 다음처럼 맞춰야 한다.
services:
app:
ports:
- "5173:3000"브라우저에서는 localhost:5173으로 접속하고,
Docker는 요청을 컨테이너 내부의 3000번 포트로 전달한다.
4. 서버가 0.0.0.0에 바인딩되어 있는지 확인
Vite, Next.js, Express, Spring Boot 등 서버가 어떤 host로 바인딩되어 있는지 확인해야 한다.
Vite라면 다음처럼 실행할 수 있다.
npm run dev -- --host 0.0.0.0Express라면 다음처럼 작성할 수 있다.
app.listen(3000, "0.0.0.0", () => {
console.log("Server is running on port 3000");
});Spring Boot는 기본적으로 외부 인터페이스에서 접근 가능한 형태로 실행되는 경우가 많지만,
설정에 따라 server.address를 확인해야 할 수 있다.
server.address=0.0.0.0
server.port=8080컨테이너 접속 문제는 보통 “포트 매핑이 되었는가?”와 “서버가 외부 요청을 받을 수 있게 바인딩되었는가?”를 함께 봐야 한다.
9. EXPOSE, ports, host 설정을 구분하자
Docker에서 서버 접속 문제를 해결하려면 세 가지를 구분해야 한다.
1. EXPOSE
이 컨테이너가 어떤 포트를 사용할 예정인지 표시
2. ports / -p
호스트 포트와 컨테이너 포트를 연결
3. host binding
서버가 어떤 네트워크 인터페이스에서 요청을 받을지 결정이 세 가지는 서로 다른 역할을 한다.
| 구분 | 예시 | 역할 |
|---|---|---|
| EXPOSE | EXPOSE 5173 | 컨테이너가 사용할 포트를 문서화 |
| ports | "5173:5173" | 호스트 포트와 컨테이너 포트를 연결 |
| host binding | --host 0.0.0.0 | 서버가 외부 요청을 받을 수 있게 바인딩 |
예를 들어 Vite를 Docker에서 실행하려면 다음이 함께 맞아야 한다.
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
EXPOSE 5173
CMD ["npm", "run", "dev"]{
"scripts": {
"dev": "vite --host 0.0.0.0"
}
}services:
frontend:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "5173:5173"
volumes:
- .:/app
- /app/node_modules이 구조에서 각각의 역할은 다음과 같다.
EXPOSE 5173
└── 이 컨테이너가 5173번 포트를 사용할 예정임을 표시
ports: "5173:5173"
└── Host 5173 → Container 5173 연결
vite --host 0.0.0.0
└── Vite 서버가 컨테이너 외부 요청을 받을 수 있게 바인딩- 포트는 하나의 컴퓨터 안에서 프로세스를 구분하는 번호다.
- 컨테이너 내부 포트와 호스트 포트는 다르다.
-p 8080:3000에서 왼쪽은 호스트 포트, 오른쪽은 컨테이너 포트다.- 브라우저에서는 호스트 포트로 접속한다.
EXPOSE는 포트를 실제로 열어주는 설정이 아니라 문서화에 가깝다.- 실제 외부 접근을 위해서는
-p또는 compose의ports가 필요하다. - 컨테이너 안의
localhost는 호스트가 아니라 컨테이너 자기 자신이다. - Docker 안에서 Vite dev server를 외부에서 접근하려면
--host 0.0.0.0이 필요할 수 있다. - 접속 문제는 포트 매핑과 서버 바인딩을 함께 확인해야 한다.
결국 Docker에서 컨테이너 서버 접속을 이해할 때 핵심은 다음이다.
포트 매핑은 호스트와 컨테이너의 포트를 연결하는 것이고, 0.0.0.0 바인딩은 컨테이너 안의 서버가 외부 요청을 받을 수 있게 여는 것이다.
따라서 컨테이너는 떴는데 브라우저에서 접속이 안 된다면 먼저 다음 두 가지를 확인하면 된다.
1. Host Port → Container Port가 올바르게 매핑되어 있는가?
2. 컨테이너 안의 서버가 0.0.0.0으로 바인딩되어 외부 요청을 받을 수 있는가?이 두 가지가 맞아야 호스트 브라우저에서 Docker Container 안의 개발 서버에 정상적으로 접근할 수 있다.
