# [Docker Compose 개념] 서비스 이미지 컨테이너 포트의 관계

Docker Compose 개념편

- Compose 파일이 표현하는 실행 환경

- 서비스 이미지 컨테이너 포트의 관계 ← 현재 문서

- 네트워크와 볼륨이 상태를 만드는 방식

- 환경 변수와 시작 순서 설계

Compose를 처음 볼 때 헷갈리는 이유는 비슷한 말이 한꺼번에 나오기 때문이다. 서비스, 이미지, 컨테이너, 포트, 빌드가 모두 연결되어 있지만 같은 개념은 아니다.

예시 프로젝트의 app 서비스는 프로젝트 소스코드로 이미지를 빌드한 뒤 컨테이너를 실행한다. 반대로 db 서비스는 이미 만들어진 PostgreSQL 이미지를 받아와 컨테이너를 실행한다. 이 차이를 이해하면 buildimage를 언제 쓰는지 감이 잡힌다.

서비스는 실행 역할이다

현재 Compose 파일의 두 서비스는 역할이 다르다.

서비스역할이미지 준비 방식
appSpring Boot API 서버 실행현재 프로젝트의 Dockerfile로 직접 빌드
dbPostgreSQL 서버 실행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의 ENTRYPOINTdiscodeit-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 80Dockerfile이미지가 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 파일을 읽을 때는 이 질문을 붙이면 된다. "이 줄은 이미지를 만드는 줄인가, 컨테이너를 실행하는 줄인가, 외부에서 들어오는 문을 여는 줄인가." 그 구분이 되면 설정이 훨씬 덜 무섭다.

참고한 공식 문서