์ด๋ฒ ํฌ์คํ ์์๋ 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 ๋ฐฐํฌ ์๋ํ๋ ์ด๋ ต์ง ์๋ค.
