강의에서는 각 서비스 폴더에서 직접 docker build를 실행하고, 이후 docker compose up으로 컨테이너를 띄우는 방식을 사용했다. 그런데 실제로 써보니 불편한 부분이 있었다.
서비스가 두 개뿐인데도 아래처럼 명령어를 여러 번 실행해야 했다.
# service-b 폴더에서
docker build -t img-service-b .
# service-a 폴더에서
docker build -t img-service-a .
# 상위 폴더에서
docker compose up -d
"이미지 빌드와 컨테이너 실행을 한 번에 할 수 없을까?" 라는 생각에서 찾아보니, docker-compose.yml에 build 옵션을 추가하면 가능했다.
build.context기존 docker-compose.yml은 이미 만들어진 이미지 이름만 참조했다.
# 기존 방식: 미리 빌드된 이미지를 그냥 가져다 씀
services:
service-a:
image: img-service-a
여기에 build.context를 추가하면 이미지가 없을 때(또는 --build 옵션 사용 시) 자동으로 빌드까지 처리해준다.
services:
service-a:
build:
context: ./com.service.a # 이 경로의 Dockerfile을 사용해서 이미지를 빌드한다
image: img-service-a # 빌드 후 이 이름으로 태깅
context는 Dockerfile이 있는 디렉터리의 경로다. docker-compose.yml 파일 위치를 기준으로 상대 경로로 지정한다.
프로젝트 구조는 아래와 같다고 가정한다.
my-project/ ← docker-compose.yml 위치
├── com.service.a/
│ ├── Dockerfile
│ ├── build/libs/*.jar
│ └── ...
└── com.service.b/
├── Dockerfile
├── build/libs/*.jar
└── ...
# version 필드는 최신 Docker Compose에서 사용하지 않아도 된다 (아래 참고)
version: '3.8'
services:
service-a:
build:
# docker-compose.yml 기준으로 com.service.a 폴더의 Dockerfile을 사용해 빌드
context: ./com.service.a
# 빌드된 이미지에 붙일 이름 (docker images 명령어로 확인 가능)
image: img-service-a
ports:
- "18080:8080"
environment:
# 컨테이너 이름(service-b)으로 서비스 간 통신
- SERVICE_B_URL=http://service-b:8080
depends_on:
# service-b 컨테이너가 먼저 시작되도록 순서 지정
- service-b
service-b:
build:
context: ./com.service.b
image: img-service-b
ports:
- "18081:8080"
networks:
default:
# Docker Compose가 자동으로 브리지 네트워크를 생성
# 같은 네트워크 안에서 서비스 이름으로 서로 호출 가능
driver: bridge
build.context가 참조하는 각 서비스 폴더 안에는 아래와 같은 Dockerfile이 있다.
# 베이스 이미지: Eclipse Temurin 17 JDK (Ubuntu Jammy 기반)
# openjdk:17-jdk-slim은 2022년 7월 이후 공식 업데이트가 중단된 deprecated 이미지다
# 현재(2026년 기준) Java 컨테이너 베이스 이미지 표준은 Eclipse Temurin이다
FROM eclipse-temurin:17-jdk-jammy
# Spring Boot가 내부적으로 사용하는 임시 파일 경로를 볼륨으로 지정
# 컨테이너가 재시작되어도 /tmp 데이터가 호스트에 유지될 수 있게 한다
# 필수는 아니지만 Spring Boot 공식 가이드에서 권장하는 패턴이다
VOLUME /tmp
# 빌드 시점에 주입되는 인수(ARG): JAR 파일 경로를 변수로 관리
# *.jar 와일드카드를 사용하므로 버전명이 바뀌어도 Dockerfile 수정 없이 동작한다
ARG JAR_FILE=build/libs/*.jar
# 호스트의 JAR 파일을 컨테이너 내부에 app.jar라는 이름으로 복사
# ${JAR_FILE}은 위에서 선언한 ARG 값을 참조한다
COPY ${JAR_FILE} app.jar
# 컨테이너가 시작될 때 실행할 명령어
# CMD와 달리 ENTRYPOINT는 docker run 시 인수를 추가해도 이 명령어가 덮어써지지 않는다
# exec 형식(JSON 배열)을 사용하면 shell을 거치지 않아 시그널(SIGTERM 등) 처리가 안정적이다
ENTRYPOINT ["java", "-jar", "/app.jar"]
openjdk:17-jdk-slim을 그대로 쓰면 실행은 되지만, Docker Hub에서 아래와 같은 안내를 확인할 수 있다.
"Deprecated: Users should use eclipse-temurin, amazoncorretto, or other maintained images."
Eclipse Temurin은 Adoptium 프로젝트(구 AdoptOpenJDK)에서 관리하는 OpenJDK 공식 빌드다. jammy는 Ubuntu 22.04 LTS(코드명 Jammy Jellyfish) 기반이라는 의미로, 안정성과 보안 패치 측면에서 신뢰할 수 있는 선택이다.
| 이미지 | 상태 | 비고 |
|---|---|---|
openjdk:17-jdk-slim | Deprecated | 2022년 7월 이후 업데이트 없음 |
eclipse-temurin:17-jdk-jammy | 유지보수 중 | 현재 권장 |
eclipse-temurin:17-jre-jammy | 유지보수 중 | 실행만 필요하다면 JRE가 이미지 크기 면에서 유리 |
둘 다 컨테이너 시작 시 실행할 명령어를 지정한다는 점은 같다. 차이는 다음과 같다.
| ENTRYPOINT | CMD | |
|---|---|---|
docker run 시 인수 추가 | 기존 명령어 유지, 인수만 추가됨 | 전체 명령어가 덮어써짐 |
| 주 용도 | 항상 실행되어야 하는 고정 명령어 | 기본값 제공 (재정의 가능) |
Spring Boot JAR 실행처럼 "무조건 이 명령어로 시작해야 한다"는 경우엔 ENTRYPOINT가 적합하다.
JAR 빌드는 여전히 수동으로 해야 한다. Gradle 빌드까지 Compose에 포함시키려면 멀티스테이지 Dockerfile이 필요한데, 지금 단계에서는 아래 방법으로 충분하다.
# 1. 각 서비스 JAR 빌드 (순서 무관)
cd com.service.a && ./gradlew clean bootJar
cd ../com.service.b && ./gradlew clean bootJar
# 2. 상위 폴더로 이동 후 이미지 빌드 + 컨테이너 실행 한 번에
cd ..
docker compose up --build -d
--build 옵션이 핵심이다. 이 옵션을 붙이면 기존 이미지가 있더라도 매번 새로 빌드한 뒤 컨테이너를 실행한다. 코드를 수정하고 재배포할 때 유용하다.
--build없이docker compose up -d만 실행하면, 이미 빌드된 이미지가 있을 경우 그것을 그대로 사용한다. 코드를 바꿨는데 반영이 안 된다면 이 옵션을 빠뜨렸을 가능성이 높다.
직접 실행해보니 아래와 같은 로그가 출력되었다. 각 단계가 무엇을 의미하는지 살펴보면 이렇다.
1단계 — Dockerfile 로드
[service-a] load build definition from Dockerfile
[service-b] load build definition from Dockerfile
각 context 경로에서 Dockerfile을 읽어들인다.
2단계 — 베이스 이미지 준비
[service-b] load metadata for eclipse-temurin:17-jdk-jammy
#8 CACHED
CACHED라고 표시된 것은 이미 로컬에 해당 이미지가 있어서 Docker Hub에서 다시 받지 않은 것이다. 처음 실행할 때는 다운로드가 일어난다.
3단계 — JAR 파일 복사
[service-a 2/2] COPY build/libs/*.jar app.jar
[service-b 2/2] COPY build/libs/*.jar app.jar
각 서비스의 build/libs/ 디렉터리에서 JAR 파일을 이미지 내부로 복사한다. Dockerfile의 COPY ${JAR_FILE} app.jar 구문이 실행되는 단계다.
4단계 — 이미지 생성 및 태깅
naming to docker.io/library/img-service-a:latest
naming to docker.io/library/img-service-b:latest
image: img-service-a에 지정한 이름으로 빌드된 이미지가 태깅된다.
5단계 — 네트워크 및 컨테이너 생성
Network week-_default Created
Container week--service-b-1 Created
Container week--service-a-1 Created
Docker Compose가 자동으로 브리지 네트워크를 생성한다. 네트워크 이름은 docker-compose.yml이 위치한 디렉터리 이름 + _default 형식으로 자동 결정된다. 여기서는 폴더 이름이 week-프로젝트관리심화였기 때문에 week-_default가 된 것이다.
컨테이너도 depends_on 설정에 따라 service-b가 먼저, service-a가 나중에 생성된다.
실행 확인
CONTAINER ID IMAGE PORTS
e4e0cc598afc img-service-a 0.0.0.0:18080->8080/tcp
9c4a9f004eeb img-service-b 0.0.0.0:18081->8080/tcp
두 컨테이너가 모두 Up 상태로 올라와 있으면 성공이다.
| 기존 방식 | build context 방식 | |
|---|---|---|
| 이미지 빌드 | 각 서비스 폴더에서 docker build 개별 실행 | docker compose up --build로 한 번에 처리 |
| 관리 위치 | 폴더마다 분산 | docker-compose.yml 하나로 통합 |
| 코드 변경 후 재배포 | 빌드 → 태깅 → compose up 순서 직접 관리 | --build 옵션 하나로 처리 |
서비스가 늘어날수록 이 차이가 더 크게 느껴진다.
build.context를 추가하는 것은 작은 변경이지만, 워크플로우(workflow, 작업 흐름)를 상당히 단순하게 만들어준다. 코드를 수정하고 나서 매번 여러 폴더를 돌아다니며 docker build를 실행하는 것보다, 상위 폴더에서 docker compose up --build -d 한 번으로 끝나는 쪽이 훨씬 효율적이다.
강의에서는 Docker의 각 단계를 분리해서 이해시키는 목적으로 순서대로 진행했지만, 실제로 개발하면서는 이처럼 더 편한 방법을 찾아 적용해보는 과정 자체가 좋은 학습이 된다.