이번 실습에서는 SpringBoot 애플리케이션을 실행하기 위한 Docker 이미지를 작성하고 Container화 하는 과정을 실습하겠습니다. 일단 Dockerfile을 직접 작성하여 이미지를 만들어보고, 그런 다음 docker 대신 gradle plugin을 사용하여 이미지를 생성해 보겠습니다.

Docker 설치

다음 포스트를 참고하여 로컬 pc에 Docker를 설치합니다.

스프링 프로젝트 생성

“Hello Docker World” 를 출력하는 아주 간단한 스프링 프로젝트를 생성합니다. 빠른 실습을 위해 https://start.spring.io/에서 프로젝트를 생성합니다. 아래와 같이 선택하고 Generate를 눌러 프로젝트 파일을 다운로드합니다. 압축을 풀고 IDE에서 프로젝트를 열면 바로 실습 가능한 상태가 됩니다.

Intelij에서 프로젝트 열기

File – New – Project From Existing Sources.. – 압축 해제한 디렉터리 선택 – Import Project from external model – Gradle 선택

환경 세팅이 완료되면 DemoApplication에 아래와 같이 GetMapping을 추가합니다.

@RestController
@SpringBootApplication
public class DemoApplication {
    @GetMapping("/")
    public String home() {
        return "Hello Docker World";
    }

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

여기까지 진행하고 서버를 실행한 다음 메인 화면에 진입하면 다음과 같이 출력되는 것을 확인할 수 있습니다.

이제 Docker 이미지를 생성하기 위해 필요한 Jar 파일을 생성합니다. Jar파일은 build/libs 디렉터리에 생성됩니다.

$ ./gradlew bootJar

Docker 이미지 생성 & Container화 하기

Docker에는 이미지 생성을 위해 사용되는 간단한 “Dockerfile” 파일 형식이 있습니다. 더 자세한 내용은 공식 문서를 참고하시기 바랍니다.
https://docs.docker.com/engine/reference/builder/

프로젝트 root에 Dockerfile을 생성하고 다음과 같이 작성합니다.

Dockerfile

FROM openjdk:8-jdk-alpine
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

FROM
FROM 키워드는 Docker에게 주어진 이미지를(태그포함) 빌드시 기반으로 사용하도록 지시합니다.
실습에서는 openjdk 중 tag가 8-jdk-alpine인 jdk를 기반으로 하여 docker 이미지를 만듭니다.
ARG
빌드시 사용할 환경 변수를 선언합니다. 실습에서는 Spring Jar파일이 생성되는 위치를 변수로 선언합니다.
COPY
Jar파일을 app.jar 이름으로 복사합니다. 이는 실행할 jar 파일명을 통일하기 위해서입니다. Container화 할 때 Jar파일명이 매번 달라지면 실행하기 어렵기 때문입니다.
ENTRYPOINT
이미지를 Container로 띄울 때 Jar파일이 실행되어 Spring 서버가 구동되도록 Command를 설정합니다. shell 스크립트를 직접 작성하고 ENTRYPOINT에 shell을 선언하는 것도 가능합니다.

run.sh

#!/bin/sh
exec java -jar /app.jar

Dockerfile

FROM openjdk:8-jdk-alpine
ARG JAR_FILE=build/libs/*.jar
COPY run.sh .
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["run.sh"]

Dockerfile이 위치한 디렉터리에서 docker build 명령을 수행하면 Docker 이미지가 생성됩니다.

docker build -t [생성할이미지명<group>/<artifact>] [Dockerfile위치]

$ docker build -t demo/spring-docker .
Sending build context to Docker daemon  21.12MB
Step 1/4 : FROM openjdk:8-jdk-alpine
8-jdk-alpine: Pulling from library/openjdk
e7c96db7181b: Already exists
f910a506b6cb: Already exists
c2274a1a0e27: Pull complete
Digest: sha256:94792824df2df33402f201713f932b58cb9de94a0cd524164a0f2283343547b3
Status: Downloaded newer image for openjdk:8-jdk-alpine
 ---> a3562aa0b991
Step 2/4 : ARG JAR_FILE=build/libs/*.jar
 ---> Running in 719b39a34530
Removing intermediate container 719b39a34530
 ---> e711175de48c
Step 3/4 : COPY ${JAR_FILE} app.jar
 ---> 344b952c9e0e
Step 4/4 : ENTRYPOINT ["java","-jar","/app.jar"]
 ---> Running in 6bb18f5bf98f
Removing intermediate container 6bb18f5bf98f
 ---> 494acd780777
Successfully built 494acd780777
Successfully tagged demo/spring-docker:latest

docker images 명령으로 생성된 이미지를 확인할 수 있습니다.

$ docker images | grep demo
demo/spring-docker                   latest                                           494acd780777        About a minute ago   121MB

이제 이미지가 만들어졌으니 docker run 명령으로 docker 이미지를 Container 인스턴스로 런칭합니다. -p 옵션을 주면 Container 내부 8080 port가 호스트 PC의 8080 port로 포워딩되도록 처리할 수 있습니다. Container는 기본적으로 외부와 격리되어 있기 때문에 이 설정을 통해서 Container 내부의 Spring 서버에 접속할 수 있습니다.

$ docker run -p 8080:8080 demo/spring-docker

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.3.1.RELEASE)

2020-07-14 15:21:46.515  INFO 1 --- [           main] com.docker.demo.DemoApplication          : Starting DemoApplication on b031289edbab with PID 1 (/app.jar started by root in /)
2020-07-14 15:21:46.518  INFO 1 --- [           main] com.docker.demo.DemoApplication          : No active profile set, falling back to default profiles: default
2020-07-14 15:21:47.555  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2020-07-14 15:21:47.567  INFO 1 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2020-07-14 15:21:47.568  INFO 1 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.36]
2020-07-14 15:21:47.624  INFO 1 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2020-07-14 15:21:47.624  INFO 1 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1053 ms
2020-07-14 15:21:47.837  INFO 1 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2020-07-14 15:21:47.988  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2020-07-14 15:21:47.995  INFO 1 --- [           main] com.docker.demo.DemoApplication          : Started DemoApplication in 2.051 seconds (JVM running for 2.448)

Container가 구동되면 docker ps 명령으로 인스턴스를 확인할 수 있습니다.

$ docker ps | grep demo
b031289edbab        demo/spring-docker     "java -jar /app.jar"     6 minutes ago       Up 6 minutes        0.0.0.0:8080->8080/tcp

만약 서버 실행 시 JAVA OPTION을 설정해야 하면 ENTRYPOINT에 변수를 선언하고 docker run 명령행에 -e 옵션으로 변수 값을 주입할 수 있습니다.

Dockerfile

FROM openjdk:8-jdk-alpine
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","${JAVA_OPTS}","-jar","/app.jar"]
$ docker -t demo/spring-docker .
$ docker run -p 9000:9000 -e JAVA_OPTS=-Dserver.port=9000 demo/spring-docker

Container를 중지하고 싶으면 ctrl+c를 누르면 됩니다. 만약 서버 데몬으로 띄우고 싶으면 -d 옵션을 주고 docker run을 하면 됩니다. 서버 데몬으로 띄운 경우 Container를 종료하고 싶으면 docker stop [Container ID] 명령을 실행하면 됩니다.

$ docker run -d -p 8080:8080 demo/spring-docker

로컬에서 Jar를 실행했을 때와 동일하게 브라우저로 테스트합니다. 역시 동일한 결과가 나옴을 확인할 수 있습니다. Root가 아닌 다른 사용자로 Spring 서버를 구동시키려면 다음과 같이 Dockerfile에 내용을 추가합니다.

FROM openjdk:8-jdk-alpine
RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

적용된 내용을 실행하려면 기존의 container를 종료하고 다시 rebuild 하여 이미지를 재생성한 다음 실행합니다.

$ docker stop [Container ID]
$ docker build -t demo/spring-docker .
$ docker run -d -p 8080:8080 demo/spring-docker

Container 접속

Container 내부에 명령을 내리고 싶으면 docker exec 명령을 사용하면 됩니다. 일단 docker ps로 container id를 확인하고 docker exec 명령으로 shell을 실행합니다. app.jar가 Root에 복사되어 있는 것을 확인할 수 있고 ps 명령으로 Spring 서버가 데몬으로 띄워져 있는 것도 확인할 수 있습니다.

$ docker ps              
CONTAINER ID        IMAGE                COMMAND                CREATED             STATUS              PORTS                    NAMES
a6ae69cf6790        demo/spring-docker   "java -jar /app.jar"   27 seconds ago      Up 26 seconds       0.0.0.0:8080->8080/tcp   interesting_moser
$ docker exec -it a6ae69cf6790 /bin/sh
/ $ ls
app.jar  bin      dev      etc      home     lib      media    mnt      opt      proc     root     run      sbin     srv      sys      tmp      usr      var
~ $ ps -ef | grep app.jar
    1 spring    0:09 java -jar /app.jar
  152 spring    0:00 grep app.jar

리소스 최적화

Spring Boot jar 파일에서 종속성과 응용 프로그램 리소스는 명확하게 분리되어 있으며 이 사실을 이용하여 성능을 향상시킬 수 있습니다. 핵심은 컨테이너 파일 시스템에 레이어를 만드는 것입니다.

아래 명령을 실행하여 build/dependency 디렉터리를 만들고 /libs 디렉터리에 위치한 jar파일의 압축을 해제합니다.

$ mkdir -p build/dependency && (cd build/dependency; jar -xf ../libs/*.jar)

레이어를 만들기 위해 Dockerfile을 다음과 같이 수정합니다. 압축 해제한 내용을 복사하여 각각의 레이어( 종속 library, meta, 응용프로그램 classes)를 app 하위에 구성합니다. 이렇게 하면 응용 프로그램 종속성이 변경되지 않으면 첫 번째 계층 (BOOT-INF/lib)은 변경되지 않으므로 빌드 속도가 빨라지고 기본 레이어가 이미 캐시 되어있는 한 런타임 시 컨테이너 시작 속도도 빨라지게 됩니다.

FROM openjdk:8-jdk-alpine
RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring
ARG DEPENDENCY=build/dependency
COPY ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY ${DEPENDENCY}/META-INF /app/META-INF
COPY ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","com.docker.demo.DemoApplication"]

위의 Dockerfile은 Jar 파일이 이미 명령 행에 의해 빌드되었다고 가정했습니다. 다단계 빌드를 사용하여 docker에서 해당 단계를 수행하여 결과를 한 이미지에서 다른 이미지로 복사할 수도 있습니다.

# syntax=docker/dockerfile:experimental
FROM openjdk:8-jdk-alpine AS build
WORKDIR /workspace/app

COPY . /workspace/app
RUN --mount=type=cache,target=/root/.gradle ./gradlew clean build
RUN mkdir -p build/dependency && (cd build/dependency; jar -xf ../libs/*.jar)

FROM openjdk:8-jdk-alpine
RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring
ARG DEPENDENCY=build/dependency
COPY ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY ${DEPENDENCY}/META-INF /app/META-INF
COPY ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","com.docker.demo.DemoApplication"]

빌드시 다음과 같이 실행합니다.

$ DOCKER_BUILDKIT=1 docker build -t demo/spring-docker .

이러한 기능은 실험 단계에 있지만 빌드 킷을 켜고 끄는 옵션은 사용 중인 docker버전에 따라 다릅니다. 자세한 내용은 사용 중인 버전의 설명서를 확인하십시오. (위의 예는 docker 18.0.6에 맞습니다)

Plugins 으로 빌드하기

빌드에서 docker를 직접 호출하지 않으려면 Maven 및 Gradle 용 plugin이 많이 있습니다. 스프링 부트 플러그인 스프링 부트 2.3을 사용하면 스프링 부트로 직접 Maven 또는 Gradle에서 이미지를 빌드할 수 있습니다. Spring Boot jar 파일을 이미 빌드하고 있다면 플러그인만 직접 호출하면 됩니다.

$ ./gradlew bootBuildImage
> Task :bootBuildImage
Building image 'docker.io/library/demo:0.0.1-SNAPSHOT'

 > Pulling builder image 'gcr.io/paketo-buildpacks/builder:base-platform-api-0.3' ..................................................
 > Pulled builder image 'gcr.io/paketo-buildpacks/builder@sha256:75ad081f1523bca5f59c0b937b592e7bbc317f0a00967df5d4283fff3e3031f3'
 > Pulling run image 'gcr.io/paketo-buildpacks/run:base-cnb' ..................................................
 > Pulled run image 'gcr.io/paketo-buildpacks/run@sha256:ded4206a449ec79c448a52f0178af3bf983ffce2338d76b7eec3f3181e57efb0'
 > Executing lifecycle version v0.8.1
// 생략...
Successfully built image 'docker.io/library/demo:0.0.1-SNAPSHOT'

그리고 이미지 생성을 위한 Dockerfile이 필요하지 않습니다. 결과는 기본적으로 docker.io//:latest라는 이미지입니다. 이미지 이름은 빌드시 옵션으로 변경할 수 있습니다.

$ ./gradlew bootBuildImage --imageName=demo/spring-docker

Palantir Gradle Plugin

Palantir Gradle Plugin은 Dockerfile과 함께 작동하며 Dockerfile을 생성할 수 있으며 명령 줄에서 Dockerfile을 실행하는 것처럼 Docker를 실행합니다.

build.gradle

buildscript, apply plugin, task unpack, docker가 추가되었습니다.

buildscript {
	dependencies {
		classpath('gradle.plugin.com.palantir.gradle.docker:gradle-docker:0.13.0')
	}
}

plugins {
	id 'org.springframework.boot' version '2.3.1.RELEASE'
	id 'io.spring.dependency-management' version '1.0.9.RELEASE'
	id 'java'
}

apply plugin: 'com.palantir.docker'

group = 'com.docker'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	testImplementation('org.springframework.boot:spring-boot-starter-test') {
		exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
	}
}

test {
	useJUnitPlatform()
}

task unpack(type: Copy) {
	dependsOn bootJar
	from(zipTree(tasks.bootJar.outputs.files.singleFile))
	into("build/dependency")
}

docker {
	name "${project.group}/${bootJar.baseName}"
	copySpec.from(tasks.unpack.outputs).into("dependency")
	buildArgs(['DEPENDENCY': "dependency"])
}

Dockerfile

# syntax=docker/dockerfile:experimental
#FROM openjdk:8-jdk-alpine AS build
#WORKDIR /workspace/app
#
#COPY . /workspace/app
#RUN --mount=type=cache,target=/root/.gradle ./gradlew clean build
#RUN mkdir -p build/dependency && (cd build/dependency; jar -xf ../libs/*.jar)

FROM openjdk:8-jdk-alpine
RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring
ARG DEPENDENCY=build/dependency
COPY ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY ${DEPENDENCY}/META-INF /app/META-INF
COPY ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","com.docker.demo.DemoApplication"]

gradle task를 실행하여 이미지를 생성합니다.

$ ./gradlew docker
BUILD SUCCESSFUL in 1s
7 actionable tasks: 3 executed, 4 up-to-date
$ docker images | grep com.docker        
com.docker/demo                                                         latest                                           14bdb1fba469        8 minutes ago       121MB

Jib Maven and Gradle Plugins

구글은 Jib이라는 오픈 소스 툴을 가지고 있는 데,이 툴은 docker를 실행하기 위해 docker가 필요하지 않습니다. docker 빌드에서 얻은 것과 동일한 표준 출력을 사용하여 이미지를 빌드하지만 요청하지 않으면 docker를 사용하지 않습니다. 따라서 docker가 설치되지 않은 환경에서 작동합니다. 또한 Maven으로 빌드된 이미지를 얻기 위해 Dockerfile이나 pom.xml에 아무것도 필요하지 않습니다 (Gradle은 최소한 build.gradle에 플러그인을 설치해야 합니다). Jib의 또 다른 흥미로운 기능은 레이어에 대한 것으로 위에서 생성 한 멀티 레이어 Dockerfile과 약간 다른 방식으로 레이어를 최적화합니다.

build.gradle

plugins {
  ...
  id 'com.google.cloud.tools.jib' version '1.8.0'
}
$ ./gradlew jibDockerBuild --image=demo/spring-docker
docker images | grep demo
demo/spring-docker                                                      latest                                           5c1eb054f60a        50 years ago        142MB

SpringBoot 2.3 Docker Support

SpringBoot 2.3 버전부터 별도의 plugin을 사용하지 않고도 Docker이미지를 만들 수 있도록 지원합니다. 따라서 plugin을 통해 docker의 특정 기능을 사용하지 않는다면 Spring 자체에서 제공하는 build 명령을 통해 Docker 이미지를 생성하면 됩니다. gradle의 경우 아래와 같이 bootBuildImage task를 실행하면 Docker이미지를 생성할 수 있습니다. 로그의 내용이 길어 생략하였지만, 로그 내용을 자세히 보면 Docker이미지를 작성하기 위해 어떤 과정을 거치는지 꽤 자세하게 볼 수 있으므로 한 번쯤은 확인해 보시기 바랍니다.

>gradlew.bat bootBuildImage

> Task :bootBuildImage
Building image 'docker.io/library/demo:0.0.1-SNAPSHOT'

 > Pulling builder image 'gcr.io/paketo-buildpacks/builder:base-platform-api-0.3' ..................................................
 > Pulled builder image 'gcr.io/paketo-buildpacks/builder@sha256:882afe04df440ae44557be22bcc00f9d011d96263afeb57e1f55468c543952a9'
 > Pulling run image 'docker.io/paketobuildpacks/run:base-cnb' ..................................................
 > Pulled run image 'paketobuildpacks/run@sha256:1303a41dfeebb0450640655ad464c66af5c2a500e20ad86d5687f00c4805d971'
 > Executing lifecycle version v0.8.1
 > Using build cache volume 'pack-cache-5cbe5692dbc4.build'

 > Running creator
    [creator]     ===> DETECTING
    [creator]     5 of 16 buildpacks participating
    [creator]     paketo-buildpacks/bellsoft-liberica 2.13.0
    [creator]     paketo-buildpacks/executable-jar    2.1.1
    [creator]     paketo-buildpacks/apache-tomcat     1.5.0
    [creator]     paketo-buildpacks/dist-zip          1.4.0
    [creator]     paketo-buildpacks/spring-boot       2.5.0
    [creator]     ===> ANALYZING
    [creator]     Previous image with name "docker.io/library/demo:0.0.1-SNAPSHOT" not found
    [creator]     ===> RESTORING
    [creator]     ===> BUILDING
    [creator]
    [creator]     Paketo BellSoft Liberica Buildpack 2.13.0
    [creator]       https://github.com/paketo-buildpacks/bellsoft-liberica
    [creator]       Build Configuration:
    [creator]         $BP_JVM_VERSION              8.*             the Java version
    [creator]       Launch Configuration:
    [creator]         $BPL_JVM_HEAD_ROOM           0               the headroom in memory calculation
    [creator]         $BPL_JVM_LOADED_CLASS_COUNT  35% of classes  the number of loaded classes in memory calculation
    [creator]         $BPL_JVM_THREAD_COUNT        250             the number of threads in memory calculation
    [creator]         $JAVA_OPTS                                   the flags that influence JVM memory configuration at launch
    [creator]       BellSoft Liberica JRE 8.0.265: Contributing to layer
    [creator]         Downloading from https://github.com/bell-sw/Liberica/releases/download/8u265+1/bellsoft-jre8u265+1-linux-amd64.tar.gz
    [creator]         Verifying checksum
    [creator]         Expanding to /layers/paketo-buildpacks_bellsoft-liberica/jre
    [creator]         Writing env.launch/JAVA_HOME.override
    [creator]         Writing env.launch/MALLOC_ARENA_MAX.override
    [creator]         Writing profile.d/active-processor-count.sh
    [creator]       Memory Calculator 4.1.0: Contributing to layer
    [creator]         Downloading from https://github.com/cloudfoundry/java-buildpack-memory-calculator/releases/download/v4.1.0/memory-calculator-4.1.0.tgz
    [creator]         Verifying checksum
    [creator]         Expanding to /layers/paketo-buildpacks_bellsoft-liberica/memory-calculator
    [creator]         Writing profile.d/memory-calculator.sh
    [creator]       Class Counter: Contributing to layer
    [creator]         Copying to /layers/paketo-buildpacks_bellsoft-liberica/class-counter
    [creator]       JVMKill Agent 1.16.0: Contributing to layer
    [creator]         Downloading from https://github.com/cloudfoundry/jvmkill/releases/download/v1.16.0.RELEASE/jvmkill-1.16.0-RELEASE.so
... 생략
    [creator]     Paketo Spring Boot Buildpack 2.5.0
    [creator]       https://github.com/paketo-buildpacks/spring-boot
    [creator]       Build Configuration:
    [creator]         $BP_BOOT_NATIVE_IMAGE                  the build to create a native image (requires GraalVM)
    [creator]         $BP_BOOT_NATIVE_IMAGE_BUILD_ARGUMENTS  the arguments to pass to the native-image command
    [creator]       Launch Configuration:
    [creator]         $BPL_SPRING_CLOUD_BINDINGS_ENABLED     whether to auto-configure Spring Boot environment properties from bindings
    [creator]       Web Application Type: Reusing cached layer
    [creator]       Spring Cloud Bindings 1.4.0: Reusing cached layer
    [creator]       Image labels:
    [creator]         org.springframework.boot.spring-configuration-metadata.json
    [creator]         org.springframework.boot.version
    [creator]     ===> EXPORTING
    [creator]     Reusing layer 'launcher'
    [creator]     Reusing layer 'paketo-buildpacks/bellsoft-liberica:class-counter'
    [creator]     Reusing layer 'paketo-buildpacks/bellsoft-liberica:java-security-properties'
    [creator]     Reusing layer 'paketo-buildpacks/bellsoft-liberica:jre'
    [creator]     Reusing layer 'paketo-buildpacks/bellsoft-liberica:jvmkill'
    [creator]     Reusing layer 'paketo-buildpacks/bellsoft-liberica:link-local-dns'
    [creator]     Reusing layer 'paketo-buildpacks/bellsoft-liberica:memory-calculator'
    [creator]     Reusing layer 'paketo-buildpacks/bellsoft-liberica:openssl-certificate-loader'
    [creator]     Reusing layer 'paketo-buildpacks/bellsoft-liberica:security-providers-configurer'
    [creator]     Reusing layer 'paketo-buildpacks/executable-jar:class-path'
    [creator]     Reusing layer 'paketo-buildpacks/spring-boot:spring-cloud-bindings'
    [creator]     Reusing layer 'paketo-buildpacks/spring-boot:web-application-type'
    [creator]     Reusing 1/1 app layer(s)
    [creator]     Reusing layer 'config'
    [creator]     *** Images (7810720fb70c):
    [creator]           docker.io/library/demo:0.0.1-SNAPSHOT

Successfully built image 'docker.io/library/demo:0.0.1-SNAPSHOT'


BUILD SUCCESSFUL in 6s
4 actionable tasks: 1 executed, 3 up-to-date

[참고]

https://spring.io/guides/gs/spring-boot-docker/
https://spring.io/guides/topicals/spring-boot-docker