# [Docker Compose 개념] 서비스 이미지 컨테이너 포트의 관계
- 서비스 이미지 컨테이너 포트의 관계 ← 현재 문서
Compose를 처음 볼 때 헷갈리는 이유는 비슷한 말이 한꺼번에 나오기 때문이다. 서비스, 이미지, 컨테이너, 포트, 빌드가 모두 연결되어 있지만 같은 개념은 아니다.
예시 프로젝트의 app 서비스는 프로젝트 소스코드로 이미지를 빌드한 뒤 컨테이너를 실행한다. 반대로 db 서비스는 이미 만들어진 PostgreSQL 이미지를 받아와 컨테이너를 실행한다. 이 차이를 이해하면 build와 image를 언제 쓰는지 감이 잡힌다.
서비스는 실행 역할이다
현재 Compose 파일의 두 서비스는 역할이 다르다.
| 서비스 | 역할 | 이미지 준비 방식 |
|---|---|---|
app | Spring Boot API 서버 실행 | 현재 프로젝트의 Dockerfile로 직접 빌드 |
db | PostgreSQL 서버 실행 | Docker Hub의 postgres:15-alpine 이미지 사용 |
app은 내가 만든 코드이므로 build: .가 필요하다. db는 공식 PostgreSQL 이미지가 이미 있으므로 image: postgres:15-alpine이면 충분하다.
services:
app:
build: .
db:
image: postgres:15-alpine
build는 "이미지를 어떻게 만들 것인가"이고, image는 "어떤 이미지를 실행할 것인가"다. 직접 만든 앱은 빌드가 필요하고, 이미 배포된 DB나 Redis 같은 인프라 이미지는 보통 image를 사용한다.
Dockerfile은 앱 이미지의 조립법이다
현재 프로젝트의 Dockerfile은 Java 17 이미지를 기반으로 Gradle 빌드를 실행하고, 생성된 JAR을 실행한다.
FROM amazoncorretto:17
WORKDIR /app
COPY gradlew .
COPY gradle gradle
COPY build.gradle .
COPY settings.gradle .
RUN chmod +x gradlew
RUN ./gradlew dependencies --no-daemon || true
COPY src src
RUN ./gradlew clean build -x test --no-daemon
EXPOSE 80
ENV PROJECT_NAME=discodeit
ENV PROJECT_VERSION=0.0.1-SNAPSHOT
ENV JVM_OPTS=""
ENTRYPOINT ["sh", "-c", "java $JVM_OPTS -jar build/libs/${PROJECT_NAME}-${PROJECT_VERSION}.jar"]
이 파일은 "이미지를 만드는 과정"과 "컨테이너 시작 시 실행할 명령"을 함께 정의한다. Compose의 app.build: .는 이 Dockerfile을 사용해 이미지를 만든다.
다만 현재 프로젝트에서는 점검할 지점이 있다. build.gradle의 버전은 2.0-M9이고, settings.gradle의 프로젝트 이름은 discodeit이다. 기본 JAR 이름은 보통 discodeit-2.0-M9.jar 형태가 된다. 그런데 Dockerfile의 ENTRYPOINT는 discodeit-0.0.1-SNAPSHOT.jar를 실행하려고 한다.
이 이름이 맞지 않으면 이미지는 빌드되어도 컨테이너 시작 시 JAR 파일을 찾지 못한다. 실습편에서 이 부분을 app.jar로 고정하는 방식으로 정리한다.
컨테이너 포트와 호스트 포트는 다르다
현재 Compose 파일에는 다음 포트 매핑이 있다.
ports:
- "8081:80"
왼쪽 8081은 호스트 포트다. 내 컴퓨터의 브라우저나 HTTP 클라이언트가 접근하는 포트다. 오른쪽 80은 컨테이너 내부 포트다. Spring Boot 앱이 컨테이너 안에서 실제로 열고 있는 포트다.
즉 이 설정은 다음 의미다.
내 컴퓨터 localhost:8081
-> app 컨테이너 내부 80번 포트
application-prod.yml에는 server.port: 80이 있으므로, 앱 컨테이너 내부 포트가 80인 것은 자연스럽다. 호스트에서 이미 80번 포트를 쓰고 있을 수 있으니 바깥쪽은 8081로 열어둔 것이다.
EXPOSE는 문서화이고 ports는 실제 연결이다
Dockerfile에도 포트 관련 줄이 있다.
EXPOSE 80
EXPOSE는 이 이미지가 80번 포트를 사용할 의도가 있다는 문서화에 가깝다. 이것만으로 호스트에서 접근할 수 있게 되는 것은 아니다. 실제로 호스트 포트를 열려면 Compose의 ports가 필요하다.
| 설정 | 위치 | 의미 |
|---|---|---|
EXPOSE 80 | Dockerfile | 이미지가 80번 포트를 사용한다는 힌트 |
ports: "8081:80" | Compose | 호스트 8081을 컨테이너 80에 실제 연결 |
그래서 EXPOSE가 없어도 ports가 있으면 접근할 수 있고, EXPOSE가 있어도 ports가 없으면 호스트에서 직접 접근할 수 없다.
db 서비스의 포트는 꼭 열어야 하는가
현재 DB 서비스는 다음처럼 호스트 5432를 컨테이너 5432에 연결한다.
db:
ports:
- "5432:5432"
이 설정이 있으면 내 컴퓨터의 DB 클라이언트에서 localhost:5432로 접속할 수 있다. 로컬 개발에서는 편하다.
하지만 앱 컨테이너가 DB에 접속하기 위해 이 포트 매핑이 필요한 것은 아니다. 앱과 DB는 같은 Compose 네트워크 안에 있으므로 db:5432로 통신할 수 있다. 즉 ports는 외부 도구가 DB에 접속해야 할 때만 필요하다.
로컬에서 DBeaver나 IntelliJ Database 탭으로 접속할 일이 많다면 유지해도 된다. 포트 충돌이 자주 나거나 외부 노출을 줄이고 싶다면 제거해도 앱과 DB의 내부 통신은 유지된다.
이 문서의 핵심
Compose에서 헷갈리는 이름들을 한 번에 정리하면 다음과 같다.
| 개념 | 질문 | 예시 |
|---|---|---|
| 서비스 | 어떤 역할을 실행하는가 | app, db |
| 이미지 | 컨테이너의 실행 파일 묶음은 무엇인가 | 앱 이미지, postgres:15-alpine |
| Dockerfile | 앱 이미지를 어떻게 만들 것인가 | Gradle 빌드 후 JAR 실행 |
| 컨테이너 | 이미지로 실제 실행된 프로세스 격리 공간 | app 컨테이너, db 컨테이너 |
| 컨테이너 포트 | 컨테이너 안에서 열린 포트는 무엇인가 | 앱 80, DB 5432 |
| 호스트 포트 | 내 컴퓨터에서 접근하는 포트는 무엇인가 | 앱 8081, DB 5432 |
Compose 파일을 읽을 때는 이 질문을 붙이면 된다. "이 줄은 이미지를 만드는 줄인가, 컨테이너를 실행하는 줄인가, 외부에서 들어오는 문을 여는 줄인가." 그 구분이 되면 설정이 훨씬 덜 무섭다.