이번 포스팅에서는 Spring Boot Docker 배포에 대해서 정리하고자 한다. 로컬에서 잘 동작하던 Spring Boot 애플리케이션을 서버에 올릴 때 가장 많이 선택하는 방식이 Docker 컨테이너 배포다. Dockerfile 작성부터 docker-compose로 데이터베이스와 함께 실행하는 방법까지, 실제 동작하는 설정 파일을 중심으로 살펴본다.
Spring Boot Docker 배포를 위한 프로젝트 구조
Spring Boot Docker 배포에 앞서 기본 프로젝트 구조를 확인한다.
my-spring-app/
├── src/
├── build.gradle (또는 pom.xml)
├── Dockerfile
├── docker-compose.yml
├── docker-compose.override.yml # 로컬 개발용 오버라이드
└── .env # 환경변수 (git에 포함하지 않음)PlaintextGradle 기준 의존성은 다음과 같다.
// build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// 데이터베이스 드라이버 (운영: MySQL, 로컬: H2)
runtimeOnly 'com.mysql:mysql-connector-j'
runtimeOnly 'com.h2database:h2'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}GroovyDockerfile 작성
기본 Dockerfile
가장 단순한 형태의 Dockerfile이다. JAR 파일을 그대로 복사해서 실행한다.
# 기반 이미지: Eclipse Temurin JDK 21 (공식 OpenJDK 배포판)
FROM eclipse-temurin:21-jre-jammy
# 컨테이너 내 작업 디렉터리 설정
WORKDIR /app
# Gradle 빌드 결과물 복사 (JAR 파일명은 프로젝트에 따라 다름)
COPY build/libs/*.jar app.jar
# 컨테이너 외부에서 접근할 포트 선언 (실제 포트 매핑은 docker run 또는 compose에서 설정)
EXPOSE 8080
# 컨테이너 시작 시 실행할 명령어
ENTRYPOINT ["java", "-jar", "app.jar"]Dockerfile이 방식은 간단하지만 이미지 크기가 불필요하게 커질 수 있다. 전체 JDK 대신 JRE를 사용한 것만으로도 이미지 크기를 상당히 줄일 수 있다.
멀티스테이지 빌드 Dockerfile
Spring Boot Docker 배포에서 권장하는 방식은 멀티스테이지 빌드다. 빌드 환경과 실행 환경을 분리해서 최종 이미지를 최소화한다.
# ── 1단계: 빌드 스테이지 ──────────────────────────────────────
# Gradle 빌드에 필요한 JDK가 포함된 이미지 사용
FROM eclipse-temurin:21-jdk-jammy AS builder
WORKDIR /app
# 의존성 캐싱: gradle wrapper와 빌드 스크립트를 먼저 복사
# 소스 코드가 바뀌어도 의존성이 변하지 않으면 이 레이어는 캐시 재사용
COPY gradlew .
COPY gradle gradle
COPY build.gradle .
COPY settings.gradle .
# 의존성 다운로드만 먼저 수행 (캐시 레이어 분리)
RUN ./gradlew dependencies --no-daemon
# 실제 소스 코드 복사 후 빌드
COPY src src
RUN ./gradlew bootJar --no-daemon -x test
# ── 2단계: 실행 스테이지 ──────────────────────────────────────
# 빌드 산출물만 가져오고 JDK는 포함하지 않음 (이미지 크기 감소)
FROM eclipse-temurin:21-jre-jammy
WORKDIR /app
# 보안을 위해 root 대신 별도 사용자로 실행
RUN addgroup --system spring && adduser --system --ingroup spring spring
USER spring:spring
# 빌드 스테이지에서 생성된 JAR만 복사
COPY --from=builder /app/build/libs/*.jar app.jar
# Spring Boot Actuator health check 설정 (컨테이너 상태 확인용)
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
CMD curl -f http://localhost:8080/actuator/health || exit 1
EXPOSE 8080
# exec 형식 사용: SIGTERM을 Java 프로세스가 직접 받아 graceful shutdown 처리
ENTRYPOINT ["java", \
"-XX:+UseContainerSupport", \
"-XX:MaxRAMPercentage=75.0", \
"-Djava.security.egd=file:/dev/./urandom", \
"-jar", "app.jar"]Dockerfile-XX:+UseContainerSupport는 JVM이 컨테이너의 메모리 제한을 인식하게 해주는 옵션이다. Java 10 이후로는 기본값이지만 명시적으로 선언해두면 의도가 명확해진다. -XX:MaxRAMPercentage=75.0은 컨테이너에 할당된 메모리의 75%를 JVM 힙으로 사용하도록 설정한다.
Spring Boot Docker 배포를 위한 application.yml 설정
환경별로 다른 설정을 주입받을 수 있도록 application.yml을 구성한다.
# src/main/resources/application.yml
spring:
application:
name: my-spring-app
datasource:
# 환경변수로 주입받음 (Docker 환경에서는 컨테이너명을 호스트로 사용)
url: ${DB_URL:jdbc:h2:mem:testdb}
username: ${DB_USERNAME:sa}
password: ${DB_PASSWORD:}
driver-class-name: ${DB_DRIVER:org.h2.Driver}
jpa:
hibernate:
ddl-auto: ${JPA_DDL_AUTO:create-drop}
show-sql: false
properties:
hibernate:
format_sql: true
server:
port: 8080
# Graceful shutdown: 처리 중인 요청이 완료될 때까지 기다렸다가 종료
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
# Spring Boot Actuator: 컨테이너 헬스체크 및 모니터링용
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: when-authorizedYAML${DB_URL:jdbc:h2:mem:testdb} 패턴은 환경변수 DB_URL이 없으면 기본값으로 H2 인메모리 DB를 사용한다는 의미다. 로컬 개발 시에는 H2, Docker 환경에서는 MySQL로 자동 전환된다.
docker-compose.yml 작성
Spring Boot Docker 배포에서 데이터베이스와 애플리케이션을 함께 관리하려면 docker-compose가 편리하다.
# docker-compose.yml
services:
app:
build:
context: . # Dockerfile이 위치한 디렉터리
dockerfile: Dockerfile
container_name: spring-app
ports:
- "8080:8080" # 호스트:컨테이너 포트 매핑
environment:
# 애플리케이션 설정값을 환경변수로 주입
DB_URL: jdbc:mysql://db:3306/mydb?useSSL=false&allowPublicKeyRetrieval=true&characterEncoding=UTF-8
DB_USERNAME: ${MYSQL_USER}
DB_PASSWORD: ${MYSQL_PASSWORD}
DB_DRIVER: com.mysql.cj.jdbc.Driver
JPA_DDL_AUTO: update
# Spring 프로파일 활성화
SPRING_PROFILES_ACTIVE: prod
depends_on:
db:
# db 컨테이너가 healthy 상태가 될 때까지 대기 (단순 started 보다 안전)
condition: service_healthy
networks:
- backend-network
# JVM 메모리 제한 (컨테이너 메모리 1GB 할당)
deploy:
resources:
limits:
memory: 1g
db:
image: mysql:8.0
container_name: spring-db
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: mydb
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
ports:
- "3306:3306" # 로컬에서 DB 직접 접근이 필요한 경우에만 노출
volumes:
# named volume으로 데이터 영속성 보장 (컨테이너 재시작 시 데이터 유지)
- mysql-data:/var/lib/mysql
# 초기화 SQL 스크립트 자동 실행
- ./docker/mysql/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s # MySQL 초기 기동 시간 확보
networks:
- backend-network
volumes:
mysql-data: # 명시적으로 선언된 named volume
networks:
backend-network:
driver: bridgeYAMLdepends_on의 condition: service_healthy는 MySQL이 실제로 쿼리를 받을 준비가 됐을 때 Spring Boot를 기동시킨다. 단순히 depends_on: db만 쓰면 MySQL 컨테이너가 시작됐지만 아직 준비되지 않은 상태에서 Spring Boot가 접속을 시도하다 실패하는 문제가 생긴다.
.env 파일
민감한 설정값은 .env 파일에 분리한다. 이 파일은 .gitignore에 반드시 추가해야 한다.
docker-compose.yml 파일이 실행될 때 해당 디렉토리의 .env 파일의 내용을 환경 변수로 인식한다.
.env 파일에 비밀번호와 같이 민감한 정보가 포함될 수 있기 때문에 외부 레포지토리에 커밋을 하지 않는 것이 좋다.
# .env (git에 포함하지 않음)
MYSQL_ROOT_PASSWORD=rootpassword123
MYSQL_USER=appuser
MYSQL_PASSWORD=apppassword123ShellScript# .env.example (git에 포함 - 팀원에게 필요한 변수 목록 공유)
MYSQL_ROOT_PASSWORD=
MYSQL_USER=
MYSQL_PASSWORD=ShellScriptSpring Boot Docker 배포 실행
이미지 빌드 및 실행
# 프로젝트 빌드 (테스트 제외)
./gradlew bootJar -x test
# docker-compose로 전체 스택 실행 (백그라운드)
docker-compose up -d --build
# 로그 확인
docker-compose logs -f app
# 특정 서비스만 재시작
docker-compose restart app
# 전체 종료 (볼륨 유지)
docker-compose down
# 전체 종료 + 볼륨 삭제 (DB 데이터 초기화)
docker-compose down -vShellScript컨테이너 상태 확인
# 실행 중인 컨테이너 목록
docker-compose ps
# 헬스체크 상태 포함 상세 정보
docker inspect spring-app | grep -A 10 Health
# Actuator health 엔드포인트로 앱 상태 확인
curl http://localhost:8080/actuator/healthShellScript로컬 개발 환경 분리 (docker-compose.override.yml)
운영용 docker-compose.yml을 그대로 두고, 로컬 개발에서만 필요한 설정은 override 파일로 분리한다.
# docker-compose.override.yml (로컬 개발용, git에 포함 가능)
services:
app:
# 로컬에서는 이미지 빌드 없이 JAR 직접 마운트해서 빠른 개발 가능
volumes:
- ./build/libs:/app/libs
environment:
SPRING_PROFILES_ACTIVE: local
# 로컬에서는 SQL 로깅 활성화
SPRING_JPA_SHOW_SQL: "true"
db:
ports:
# 로컬에서는 DB 포트를 외부에 노출해 DB 클라이언트 직접 접근 허용
- "3306:3306"YAMLdocker-compose.override.yml은 docker-compose up 시 자동으로 병합된다. Docker Compose 공식 문서에서 merge 동작 방식을 확인할 수 있다.
Spring Boot Docker 배포 시 자주 겪는 문제
JVM 힙 메모리 부족
컨테이너 메모리를 제한했는데 OOMKilled가 발생한다면 -XX:MaxRAMPercentage를 낮추거나 컨테이너 메모리 제한을 늘려야 한다.
# 컨테이너 메모리 사용량 모니터링
docker stats spring-appShellScriptMySQL 연결 실패 (Communications link failure)
depends_on에 healthcheck condition을 설정했는데도 연결 실패가 발생하는 경우가 있다. Spring Boot에 재시도 로직을 추가하는 것이 안전하다.
# application.yml
spring:
datasource:
hikari:
# DB 연결 실패 시 최대 대기 시간 (MySQL 기동 완료 전에 연결 시도 대비)
connection-timeout: 30000
initialization-fail-timeout: 60000YAMLSpring Boot 3.x에서는 spring-retry를 활용한 DataSource 연결 재시도도 고려해볼 수 있다.
이미지 크기 최적화
멀티스테이지 빌드 후에도 이미지가 크다면 Spring Boot의 레이어드 JAR 방식을 활용하면 레이어 캐싱 효율을 높일 수 있다.
# 레이어드 JAR 분해 후 복사하는 방식
FROM eclipse-temurin:21-jre-jammy AS builder
WORKDIR /app
COPY build/libs/*.jar app.jar
# JAR를 레이어별로 분해 (의존성, 스냅샷, 애플리케이션 코드 등)
RUN java -Djarmode=layertools -jar app.jar extract
FROM eclipse-temurin:21-jre-jammy
WORKDIR /app
# 변경 빈도가 낮은 레이어부터 복사 (캐시 효율 극대화)
COPY --from=builder /app/dependencies/ ./
COPY --from=builder /app/spring-boot-loader/ ./
COPY --from=builder /app/snapshot-dependencies/ ./
COPY --from=builder /app/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]Dockerfile마치며
지금까지 Spring Boot Docker 배포에 대해서 정리해 보았다. 멀티스테이지 빌드로 이미지 크기를 줄이고, docker-compose의 healthcheck로 서비스 기동 순서를 제어하는 부분이 실무에서 가장 자주 놓치는 포인트인 것 같다. 이 구성을 기반으로 GitHub Actions나 Jenkins 파이프라인에 연결하면 Spring Boot Docker 배포 자동화도 어렵지 않다.
