현재 수많은 IT 서비스는 컨테이너 이미지 빌드 및 런타임을 수행하는 플랫폼인 Docker를 통해 관리되고 있습니다. 오픈소스로 공개된 빌드팩을 활용하여 이미지를 빌드하는 것도 좋은 방법이지만 배포 환경이나 어플이케이션의 특성에 맞게 커스텀을 적용하기 위해서는 Dockerfile
에 대한 작성법을 익혀야 하는데요. 이번 포스트에서는 Gradle이 적용된 Spring Boot 프로젝트에 Dockerfile을 추가하고 배포하는 과정을 다뤄보았습니다.
다음은 Spring Boot 컨테이너 이미지를 빌드하기 위한 Dockerfile
의 전체 내용입니다. JVM 기반의 언어(Java, Kotlin)로 작성된 Spring Boot는 Jar 빌드와 런타임 과정이 모두 Docker 이미지 빌드 과정에 포함되어야 합니다.
# 빌드 스테이지
FROM eclipse-temurin:17-alpine AS build
RUN apk add --no-cache bash
WORKDIR /app
COPY gradlew .
COPY gradle gradle
COPY build.gradle.kts settings.gradle.kts ./
RUN ./gradlew dependencies --no-daemon
COPY . .
RUN chmod +x ./gradlew
RUN ./gradlew build --no-daemon -x test
# 런타임 스테이지
FROM eclipse-temurin:17-jre-alpine AS runtime
WORKDIR /app
RUN addgroup -g 1000 worker && \
adduser -u 1000 -G worker -s /bin/sh -D worker
COPY --from=build --chown=worker:worker /app/build/libs/*.jar ./main.jar
USER worker:worker
ENV PROFILE=${PROFILE}
EXPOSE 8080
ENTRYPOINT ["java", "-Dspring.profiles.active=${PROFILE}", "-jar", "main.jar"]
1. FROM
은 Docker 이미지의 바탕이 되는 베이스 이미지를 세팅하는 명령어 입니다. 소스 코드를 바탕으로 Jar 파일을 빌드하는 단계이기 때문에 AS build
를 작성하여 빌드 스테이지임을 명시합니다.
빌드 스테이지에서 사용된 이미지는 alpine linux를 바탕으로 한 eclipse-temurin:17-alpine
입니다. 구글, 레드햇, 마이크로소프트 등 여러 글로벌 IT 기업으로 구성된 Adoptium 워킹 그룹이 Java 생태계 활성화를 위해 OpenJDK를 바탕으로 개발한 오픈소스 JDK 구현체이며, 라이선스에 구애 받지 않고 사용할 수 있습니다.
FROM eclipse-temurin:17-alpine AS build
2. Gradle 스크립트 실행을 위해 베이스 이미지에 bash를 설치 합니다. --no-cache
옵션을 사용하면 로컬 캐시가 아닌 최신 패키지 정보를 로드하여 설치를 진행합니다.
RUN apk add --no-cache bash
3. 루트 디렉토리에서의 작업을 피하고 빌드할 대상 어플리케이션을 다른 디렉토리와 분리하기 위해 작업 디렉토리를 /app
으로 설정합니다. /app
은 다른 어플리케이션을 도커 이미지로 빌드할 때에도 관례로 사용되는 디렉토리명입니다.
WORKDIR /app
4. Gradle 빌드에 필요한 파일 및 디렉토리를 빌드 스테이지로 복사합니다.
COPY gradlew .
COPY gradle gradle
COPY build.gradle.kts settings.gradle.kts ./
5. Spring Boot 어플리케이션에서 사용되는 의존성을 다운로드합니다. Docker는 컨테이너 이미지 빌드 시 하나의 명령을 하나의 레이어로 관리하는데, 각각의 레이어는 캐시 처리가 되어 변동 사항이 있는 부분에 대해서만 추가로 빌드하게 됩니다. 즉, 새로 이미지를 빌드 하더라도 모든 의존성을 재설치 하지 않아도 되기 때문에 보다 더 효율적인 작업이 가능합니다.
컨테이너 환경에서는 데몬 형태의 Gradle이 필요하지 않기 때문에 --no-daemon
옵션을 통해 비활성화합니다.
RUN ./gradlew dependencies --no-daemon
6. 소스 코드를 빌드 스테이지로 전부 복사합니다.
COPY . .
7. ./gradlew
에 실행 권한을 부여합니다.
RUN chmod +x ./gradlew
8. Gradle을 통한 jar 빌드 명령을 실행합니다. 컨테이너 환경에서 데몬을 사용하지 않으므로 --no-daemon
옵션을 추가하고, 테스트 과정을 제외하기 위해 -x test
도 작성합니다. 만약 테스트 관련 태스크를 실행해야 하는 경우라면 이 부분을 적절히 수정해야 합니다. 여기서 패키징된 jar 파일은 이어지는 런타임 스테이지에서 활용됩니다.
RUN ./gradlew build --no-daemon -x test
9. 런타임 스테이지에서는 Java 빌드 없이 Jar 파일을 실행하는 과정만 수행하면 되므로 크기가 더 작은 eclipse-temurin:17-jre-alpine
이미지를 사용합니다.
FROM eclipse-temurin:17-jre-alpine AS runtime
10. 작업 디렉토리를 /app
으로 변경합니다. 여기서 가리키는 /app
은 빌드 스테이지의 /app
과는 다른 위치이므로 유의합니다.
WORKDIR /app
11. 어플리케이션을 실행 업무를 수행하는 리눅스 그룹 및 사용자를 worker
라는 이름으로 신규 생성합니다. 1000
은 리눅스에서 루트가 아닌 첫 번째 일반 사용자를 가리키는 UID/GID입니다. 만약 이러한 설정 없이 루트 사용자로 하여금 어플리케이션을 실행하도록 하면 악의적인 공격에 의해 루트 사용자 권한이 탈취되어 시스템 전체에 영향을 끼치는 결과를 초래할 수 있습니다.
RUN addgroup -g 1000 worker && \
adduser -u 1000 -G worker -s /bin/sh -D worker
12. 빌드 스테이지에서 생성된 Jar 파일을 --from=build
옵션을 사용하여 런타임 스테이지로 복사합니다. 이 때, 복사할 파일의 그룹 및 사용자를 앞에서 생성했던 1000번 그룹/사용자 worker
로 세팅합니다.
COPY --from=build --chown=worker:worker /app/build/libs/*.jar ./main.jar
13. 컨테이너를 루트가 아닌 worker:worker
가 실행하도록 설정합니다.
USER worker:worker
14. Spring Boot 런타임에 적용할 프로필을 환경변수로 세팅합니다. 여기서 세팅한 환경변수는 마지막 단계의 Jar 런타임 옵션에 적용됩니다.
ENV PROFILE=${PROFILE}
15. 개방할 포트를 8080
으로 설정합니다. 이는 실제로 컨테이너 포트를 개방하는 작업을 수행하지 않으며, 배포 담당자에게 개방할 포트를 알려주는 역할을 합니다. 컨테이너의 개방 포트가 결정되는 시점은 docker run
명령어를 사용하는 것과 같이 실제 컨테이너로 구동될 때이고 이 때 옵션을 통해 호스트측과 컨테이너측의 포트를 각각 설정합니다.
EXPOSE 8080
16. java -jar
명령에 빌드 스테이지에서 복사한 Jar 파일을 인수로 넘겨 Spring Boot 어플리케이션을 실행합니다. 여기서 JVM 옵션이나 Spring 관련 설정을 옵션으로 추가할 수 있습니다.
ENTRYPOINT ["java", "-Dspring.profiles.active=${PROFILE}", "-jar", "main.jar"]
Docker 이미지를 빌드는 과정에는 소스 코드를 복사하는 과정이 필요합니다. 그런데 이 때, 이미지의 용량을 불필요하게 키우거나 보안 측면에 문제를 야기할 수 있는 파일은 이미지 빌드 과정에서 제외하는 것이 좋습니다. 다음의 내용을 담은 .dockerignore
파일을 루트 디렉토리에 추가합니다.
.gradle
.git
.idea
build
!app/build/libs/*.jar
*.md
*.log
실습은 아래의 Spring Boot 어플리케이션을 통해 진행됩니다. 저장소를 clone 하거나 fork 해주세요.
클라우드타입의 프로젝트 페이지에서 ➕ 버튼을 누르고 Dockerfile를 선택한 후, 미리 fork 해놓은 springboot-crud-gradle 를 선택합니다. Dockerfile의 경로 및 이름의 변경이 필요한 경우 Dockerfile path 필드에 원하는 값을 입력합니다.
배포가 완료되면 접속하기 버튼을 누르고 주소창에 /api/users
경로를 추가하여 접속한 후 초기 데이터가 조회되는지 확인합니다.