안녕하세요. "도커 교과서" 라는 책을 읽고 공부한 내용을 정리해 보겠습니다. 이 포스트는 1장 부터 4장 까지의 내용을 정리하였습니다.
도커란?
"컨테이너" 라는 단위로 애플리케이션을 실행하는 기능을 제공하는 플랫폼입니다. 명령 한 줄로 애플리케이션의 빌드와 실행을 할 수 있는 편리한 도구입니다.
도커를 사용하면 어떤 장점이 있을까요?
컨테이너에서 애플리케이션을 실행하면 애플리케이션을 실행하는 환경에 관계없이 동일한 환경에서 애플리케이션을 실행할 수 있습니다. 도커를 사용하지 않았을 때에는 윈도우 또는 리눅스같은 운영체제에 Node.js, JRE, JDK 등의 런타임을 설치하고, 애플리케이션 빌드 환경을 구축합니다. 그리고 애플리케이션을 빌드하고 실행합니다. 도커를 사용하면 이러한 과정을 어느 컴퓨터에서 실행하던지 동일하게 자동으로 수행할 수 있습니다.
윈도우
윈도우에서는 도커 데스크탑을 설치합니다.
https://docs.docker.com/desktop/install/windows-install
맥OS
맥OS에서는 도커 데스크탑을 설치합니다.
https://docs.docker.com/desktop/install/mac-install
리눅스
리눅스에서는 도커 데스크탑이 아닌 도커 엔진을 설치합니다. 공식 문서를 참고하여 우분투, 데비안 등 배포판에 맞는 문서를 보고 설치합니다.
https://docs.docker.com/engine/install/ubuntu
https://docs.docker.com/engine/install/debian
https://docs.docker.com/engine/install/centos
설치가 완료되면 터미널에서 docker version
명령으로 도커 엔진이 잘 동작중인지 확인합니다.
도커에서 제공하는 hello-world 이미지를 컨테이너로 실행해볼까요? docker run
명령어를 통해 이미지를 컨테이너로 실행할 수 있습니다.
run
다음에 오는 문자열은 도커 이미지 이름입니다.
docker run hello-world
아래와 같은 결과가 나오면 정상입니다.
Hello from Docker!
This message shows that your installation appears to be working correctly.
To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
(arm64v8)
3. The Docker daemon created a new container from that image which runs the
executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it
to your terminal.
To try something more ambitious, you can run an Ubuntu container with:
$ docker run -it ubuntu bash
Share images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/
For more examples and ideas, visit:
https://docs.docker.com/get-started/
컨테이너는 그럼 무엇일까요? 컨테이너는 애플리케이션과 애플리케이션을 실행할 컴퓨터가 들어있는 하나의 상자라고 볼 수 있습니다. 한 컴퓨터에서 이러한 상자들이 여러 개 실행될 수 있습니다. 이 상자들은 독립적인 환경을 갖고 있습니다. 따라서 가상머신을 생각해 볼 수 있지만 컨테이너는 가상머신과는 다릅니다.
출처: https://www.docker.com/resources/what-container
가상머신은 각각의 가상머신이 자신만의 운영체제를 가지고 있지만, 컨테이너는 호스트 컴퓨터의 운영체제를 공유합니다.
위에서 실행했던 hello-world 컨테이너는 메시지를 출력하고 종료되는 컨테이너 였습니다. 아래 명령어를 통해 종료된 컨테이너를 확인해 볼 수 있습니다.
docker ps -a
docker ps
명령어를 사용하면 컨테이너에 부여된 ID, 이름, 이미지 등을 확인할 수 있습니다.
컨테이너를 마치 원격 컴퓨터에 접속하듯 터미널을 통해 사용해 볼 수 있습니다. 우분투에 터미널로 접속하듯이 사용해보도록 하겠습니다.
docker run -it ubuntu
hostname
exit
아래의 도커 명령어를 사용하여 컨테이너에 대한 여러 정보를 얻을 수 있습니다.
컨테이너에서 실행 중인 프로세스 목록
docker top {CONTAINER_ID}
컨테이너에서 수집된 로그
docker logs {CONTAINER_ID}
컨테이너의 상세 정보
docker inspect {CONTAINER_ID}
앞에서 실행한 hello-world 컨테이너는 실행 한 후 실행 중인 컨테이너 목록을 확인하는 docker ps
명령어로 조회해보면 나오지 않습니다. 컨테이너 내부의 애플리케이션이 종료되면 컨테이너의 상태도 종료가됩니다.
모든 컨테이너의 목록을 확인하는 docker ps -a
명령어로 조회해보면 hello-world 컨테이너가 출력되고 상태가 Exited로 되어있습니다. 컨테이너가 종료되도 컨테이너는 삭제되지 않습니다.
컨테이너를 통해 간단한 웹 사이트를 호스팅 해보겠습니다. nginx 웹서버 이미지를 이용해 보겠습니다. 도커 이미지는 도커 허브에 공개되어 있습니다. (https://hub.docker.com/_/nginx)
아래 명령어를 터미널에서 입력해봅시다.
docker run -d --name nginx -p 8080:80 nginx
브라우저에서 http://localhost:8080 으로 접속하면 "Welcome to nginx!" 화면이 뜰겁니다.
위 커맨드에서 -d 옵션, --name 옵션, -p 옵션을 사용했습니다.
앞의 예제에서 컨테이너 실행시 컨테이너의 포트를 호스트 컴퓨터의 포트에 공개했습니다. 왜 이런 작업이 필요할까요?
앞에서 사용했던 docker run
명령어는 필요한 이미지 중 로컬 컴퓨터에 없는 이미지가 있으면 이미지를 저장소에서 다운받습니다.
또는 docker pull
명령어를 통해 이미지를 명시적으로 받을 수도 있습니다.
도커는 저장소에서 이미지를 찾을 때 가장 먼저 도커 허브에서 이미지를 찾습니다.
docker pull ubuntu:20.04
도커 이미지 목록을 조회하여 이미지가 추가되었는지 확인합니다.
docker images
docker pull
명령어를 통해 이미지를 내려받는 과정을 보면 아래와 같이 하나의 이미지에 대해 여러 개의 파일을 다운받는 것을 볼 수 있습니다. 각각의 파일을 이미지 레이어라고 부릅니다. 도커 이미지는 물리적으로 여러 개의 작은 파일로 구성되어 있습니다.
docker pull jenkins/jenkins:lts
lts: Pulling from jenkins/jenkins
b04fae59f135: Pull complete
3120cb9083ba: Pull complete
3c0a0e518a2a: Pull complete
30e5f4a71251: Pull complete
59f917facca4: Pull complete
f6b1a644069b: Pull complete
a841abad8e1a: Pull complete
7cdc2776c2ce: Pull complete
f810d9f04fb9: Pull complete
e560a1287994: Pull complete
724c63a33c93: Pull complete
adb7543bd1cd: Pull complete
bc5fb2c193f0: Pull complete
Digest: sha256:90f7f78a7e114516216b6c0a06e00c597d6490597caec9d7bd7a95ce5c7dada0
Status: Downloaded newer image for jenkins/jenkins:lts
docker.io/jenkins/jenkins:lts
FROM gradle:7.6.1 AS build
WORKDIR /src # 작업 디렉터리 /src 생성 및 지정
COPY . . # Dockerfile이 위치한 경로의 모든 파일을 컨테이너의 작업 디렉터리(/src)로 복사
RUN gradle bootJar
# 멀티 스테이지 빌드
FROM amazoncorretto:17
WORKDIR /app
COPY --from=build /src/build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
인스트럭션을 하나씩 알아보겠습니다.
FROM: 모든 이미지는 다른 이미지로부터 출발합니다. 베이스 이미지를 지정합니다. 이 스크립트에서는 애플리케이션을 빌드할 때 gradle 이미지를, 애플리케이션을 실행할 때 amazoncorretto(JDK) 이미지를 사용합니다. 이처럼 여러 개의 FROM을 사용하는 것을 멀티 스테이지 빌드라고 합니다. (https://docs.docker.com/build/building/multi-stage/)
WORKDIR: 컨테이너 이미지 파일 시스템에 디렉터리를 만들고, 해당 디렉터리를 작업 디렉터리로 지정하는 인스트럭션입니다.
COPY: 로컬 파일 시스템의 파일 혹은 디렉터리를 컨테이너 이미지 파일 시스템에 복사하는 인스트럭션입니다. COPY {원본경로} {복사경로}
형식으로 사용합니다. 이 스크립트에서는 Dockerfile이 위치한 경로의 모든 파일을 컨테이너의 작업 디렉터리로 복사했습니다.
RUN: 컨테이너에서 명령어를 실행합니다. 이 스크립트에서는 쉘에서 gradle bootJar
명령어를 실행하여 스프링 애플리케이션 jar 파일을 생성하였습니다.
EXPOSE: EXPOSE 명령어는 컨테이너가 실행될 때 지정된 포트에서 수신 대기함을 Docker에 알립니다. EXPOSE 명령은 실제로 포트를 게시하지 않습니다. 이미지를 빌드하는 사람과 컨테이너를 실행하는 사람 사이에서 게시할 포트에 대한 일종의 문서 역할을 합니다. 컨테이너를 실행할 때 실제로 포트를 공개하려면 docker run
에서 -p 옵션을 사용하여 포트를 공개하거나 -P 옵션을 사용하여 노출된 모든 포트를 공개해야합니다.
CMD: 도커가 이미지로부터 컨테이너를 실행했을 때 실행할 명령을 지정하는 인스트럭션입니다. docker run
으로 실행시 사용자가 직접 실행할 명령어를 입력시 Dockerfile의 CMD는 실행되지 않습니다. docker run {이미지 이름} echo hello
실행시 Dockerfile의 CMD 대신 echo hello
가 실행됩니다.
ENTRYPOINT: CMD와 동일하게 컨테이너가 실행됬을 때 실행할 명령을 지정합니다. CMD와 다른 점으로 docker run
으로 실행시 명령어를 입력하면 ENTRYPOINT의 파라미터로 인식합니다. docker run {이미지 이름} echo hello
실행시 echo hello는 ENTRYPOINT의 파라미터로 전달됩니다.
더 많은 인스트럭션은 https://docs.docker.com/engine/reference/builder 를 참고합니다.
이미지를 빌드하려면 Dockerfile 스크립트와 이미지 이름, 패키징에 필요한 파일의 경로가 필요합니다.
예제 프로젝트(https://github.com/nefertirii/docker-example/tree/227aedc0b7570fd16f19c8893f63a3489cc297f2)를 다운받아 backend 디렉터리로 이동합니다.
Dockerfile과 소스 코드가 준비되어있습니다.
터미널에서 Dockerfile이 있는 경로에서 docker build
명령어를 사용하여 Dockerfile로 이미지를 빌드합니다.
docker build -t spring-backend .
-t 옵션은 이미지의 이름을 지정하고, 그 다음 인자는 Dockerfile 및 이미지에 포함시킬 파일의 경로입니다. "." 은 현재 작업 디렉터리를 의미합니다.
docker images
명령어로 이미지가 빌드되었는지 확인합니다.
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
spring-backend latest ac5a108f9cd9 10 minutes ago 540MB
도커 이미지에는 패키징에 포함시킨 모든 파일이 들어있습니다. 또한 여러 메타데이터 정보도 들어있습니다. 이 정보 중에는 이미지가 어떻게 빌드되었는지에 대한 간단한 이력도 포함됩니다. 이 정보를 이용하면 이미지를 구성하는 각 레이어가 무엇이고 어떤 명령으로 빌드되었는지 알 수 있습니다.
위에서 생성한 spring-backend 도커 이미지의 히스토리를 확인해보겠습니다.
docker history spring-backend:latest
IMAGE CREATED CREATED BY SIZE COMMENT
ac5a108f9cd9 3 hours ago ENTRYPOINT ["java" "-jar" "app.jar"] 0B buildkit.dockerfile.v0
<missing> 3 hours ago EXPOSE map[8080/tcp:{}] 0B buildkit.dockerfile.v0
<missing> 3 hours ago COPY /src/build/libs/*.jar app.jar # buildkit 44.8MB buildkit.dockerfile.v0
<missing> 4 hours ago WORKDIR /app 0B buildkit.dockerfile.v0
<missing> 3 days ago /bin/sh -c #(nop) ENV JAVA_HOME=/usr/lib/jv… 0B
<missing> 3 days ago /bin/sh -c #(nop) ENV LANG=C.UTF-8 0B
<missing> 3 days ago |1 version=17.0.7.7-1 /bin/sh -c set -eux … 300MB
<missing> 3 days ago /bin/sh -c #(nop) ARG version=17.0.7.7-1 0B
<missing> 3 days ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B
<missing> 3 days ago /bin/sh -c #(nop) COPY dir:dfd59a801cb05cf6d… 195MB
위의 그림에서 amazoncorretto 이미지는 운영체제와 JDK를 포함합니다. 우리가 만든 spring-backend 이미지는 amazoncorretto 이미지를 기반으로 하므로 해당 이미지의 모든 레이어를 포함합니다.
그렇다면 spring-backend 이미지의 용량은 얼마나 될까요?
docker images
명령어를 입력해보면 아래와 같이 기반 이미지 용량 495MB에 app.jar 파일 용량 44.8MB을 더한 540MB으로 표시됩니다.
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
another-spring-backend latest 826fbb6df779 3 seconds ago 540MB
spring-backend latest ac5a108f9cd9 4 hours ago 540MB
<none> <none> 0c1c30848360 4 hours ago 540MB
<none> <none> 71c2ce6dd573 5 hours ago 540MB
amazoncorretto 17 aaa5b1888a74 3 days ago 495MB
hello-world latest b038788ddb22 5 weeks ago 9.14kB
another-spring-backend 이미지도 마찬가지로 기반 이미지 용량 495MB에 app.jar 파일 용량 44.8MB을 더한 540MB으로 표시됩니다. 이렇게 보면 기반이미지 495MB, spring-backend 540MB, another-spring-backend 540MB 용량을 차지할 것 처럼 보입니다.
하지만 이는 이미지의 논리적 용량이지 실제로 차지하는 디스크 용량을 나타내는 것이 아닙니다. spring-backend 540MB, another-spring-backend 이미지는 기반 이미지인 amazoncorretto 이미지의 레이어를 공유하므로 실제로 디스크 용량을 훨씬 덜 차지합니다.
docker system df
명령어를 입력하면 실제 디스크 용량을 확인할 수 있습니다.
docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 6 1 674.1MB 629.3MB (93%)
Containers 1 0 0B 0B
Local Volumes 0 0 0B 0B
Build Cache 34 0 811.2MB 811.2MB
출력 결과를 보면 이미지들의 실제 용량은 674.1MB를 차지하는 것으로 나옵니다. 즉 spring-backend, another-spring-backend는 기반 이미지의 레이어를 공유한다는 것입니다.
이미지 레이어를 여러 이미지가 공유한다면, 공유되는 레이어는 수정할 수 없어야 합니다. 만약 이미지의 레이어를 수정할 수 있다면 수정한 내용이 레이어를 공유하는 다른 이미지에도 영향을 미치게 됩니다. 따라서 도커는 이미지 레이어를 읽기 전용으로 만들어 이런 문제를 방지합니다. 이미지를 빌드하면서 레이어가 만들어지면 레이어는 다른 이미지에서 재사용 될 수 있지만 레이어를 수정할 수는 없습니다.
Dockerfile 스크립트의 인스트럭션은 각각 하나의 이미지 레이어와 1:1로 연결됩니다. 이미지 빌드시 인스트럭션의 결과가 이전에 캐시된 레이어에 있다면 캐시된 레이어를 재사용합니다.
아래는 spring-backend 이미지의 Dockerfile 스크립트입니다.
FROM gradle:7.6.1 AS build
WORKDIR /src # 작업 디렉터리 /src 생성 및 지정
COPY . . # Dockerfile이 위치한 경로의 모든 파일을 컨테이너의 작업 디렉터리(/src)로 복사
RUN gradle bootJar
# 멀티 스테이지 빌드
FROM amazoncorretto:17
WORKDIR /app
COPY --from=build /src/build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
해당 이미지를 빌드하면 아래와 같이 출력됩니다.
docker build -t spring:1 .
[+] Building 66.3s (13/13) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 251B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/amazoncorretto:17 0.0s
=> [internal] load metadata for docker.io/library/gradle:7.6.1 1.7s
=> [build 1/4] FROM docker.io/library/gradle:7.6.1@sha256:80f1f49b69b2d94d1bc2c63a9ad8268c3c06129b224f4b2577fb944ae26e1226 42.2s
=> => resolve docker.io/library/gradle:7.6.1@sha256:80f1f49b69b2d94d1bc2c63a9ad8268c3c06129b224f4b2577fb944ae26e1226 0.0s
=> => sha256:f76840a01c8dcffa48f41af385088d66b51ca6214ebb03f5ffb6c72b1e376fe6 1.79kB / 1.79kB 0.0s
=> => sha256:498a0978395375469510ee1ec67911c5fc47525d29edceb6407fe24c94ba23e0 191.40MB / 191.40MB 35.9s
=> => sha256:80f1f49b69b2d94d1bc2c63a9ad8268c3c06129b224f4b2577fb944ae26e1226 1.21kB / 1.21kB 0.0s
=> => sha256:6c7698a779f6d8c45a39a6721fb5cce267d66ff8ab5181c55aa6d02c8ddacd01 28.39MB / 28.39MB 6.7s
=> => sha256:b3eb20d332dad785127ed3f3e6dcc4b0d15bd0c80eed668d36d238b6c154998a 18.46MB / 18.46MB 11.3s
=> => sha256:f3a54ae46b7b05d61cce6ffe7724ecec8fb77191aae25628e1fa7cddfa24c9d4 10.26kB / 10.26kB 0.0s
=> => sha256:611408d965705e52bf288c19198044f6d0623af144aa26e3c18f917ea296215e 175B / 175B 7.0s
=> => extracting sha256:6c7698a779f6d8c45a39a6721fb5cce267d66ff8ab5181c55aa6d02c8ddacd01 0.9s
=> => sha256:53b4b9e222f66a540b194cd9c75a811c4f3762826a015ca45f37e771f97b0b78 4.37kB / 4.37kB 7.2s
=> => sha256:0a93e4039b0d410743a10a3bf95b8ef5f1bbb45e6caa43364a38c9240292a0ee 51.10MB / 51.10MB 18.3s
=> => sha256:cce21c45d77e8bc478d3c19b5af3f9bc8a137cf56fed46c308a81162248120e8 122.08MB / 122.08MB 29.1s
=> => extracting sha256:b3eb20d332dad785127ed3f3e6dcc4b0d15bd0c80eed668d36d238b6c154998a 0.6s
=> => extracting sha256:498a0978395375469510ee1ec67911c5fc47525d29edceb6407fe24c94ba23e0 2.8s
=> => extracting sha256:611408d965705e52bf288c19198044f6d0623af144aa26e3c18f917ea296215e 0.0s
=> => extracting sha256:53b4b9e222f66a540b194cd9c75a811c4f3762826a015ca45f37e771f97b0b78 0.0s
=> => extracting sha256:0a93e4039b0d410743a10a3bf95b8ef5f1bbb45e6caa43364a38c9240292a0ee 1.5s
=> => extracting sha256:cce21c45d77e8bc478d3c19b5af3f9bc8a137cf56fed46c308a81162248120e8 1.5s
=> [stage-1 1/3] FROM docker.io/library/amazoncorretto:17 0.0s
=> [internal] load build context 1.2s
=> => transferring context: 45.20MB 1.2s
=> [build 2/4] WORKDIR /src 0.1s
=> [build 3/4] COPY . . 0.1s
=> [build 4/4] RUN gradle bootJar 20.4s
=> [stage-1 2/3] COPY --from=build /src/build/libs/*.jar app.jar 0.1s
=> [stage-1 3/3] WORKDIR /app 0.0s
=> exporting to image 0.1s
=> => exporting layers 0.1s
=> => writing image sha256:82b380afaf34fb346b73e902d1308f265be6951cd9ec8895266dabccd63afb4f 0.0s
=> => naming to docker.io/library/spring:1 0.0s
Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
동일한 이미지를 이름만 바꿔 다시 빌드해봅시다. 아래와 같이 CACHED로 표시된 부분은 캐시된 이미지 레이어를 재사용한 것입니다. 따라서 빌드가 빠르게 수행됩니다.
docker build -t spring:2 .
[+] Building 0.8s (13/13) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 37B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/amazoncorretto:17 0.0s
=> [internal] load metadata for docker.io/library/gradle:7.6.1 0.7s
=> [stage-1 1/3] FROM docker.io/library/amazoncorretto:17 0.0s
=> [build 1/4] FROM docker.io/library/gradle:7.6.1@sha256:80f1f49b69b2d94d1bc2c63a9ad8268c3c06129b224f4b2577fb944ae26e1226 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 6.31kB 0.0s
=> CACHED [build 2/4] WORKDIR /src 0.0s
=> CACHED [build 3/4] COPY . . 0.0s
=> CACHED [build 4/4] RUN gradle bootJar 0.0s
=> CACHED [stage-1 2/3] COPY --from=build /src/build/libs/*.jar app.jar 0.0s
=> CACHED [stage-1 3/3] WORKDIR /app 0.0s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:82b380afaf34fb346b73e902d1308f265be6951cd9ec8895266dabccd63afb4f 0.0s
=> => naming to docker.io/library/spring:2 0.0s
Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
이번에는 spring-backend 프로젝트의 소스 코드를 변경하고 이미지를 다시 빌드하면 어떻게 될까요?
소스코드가 변경되었기 때문에 새로운 이미지 레이어가 생깁니다. 도커의 이미지 레이어가 변경되면 변경된 레이어보다 위에 오는 레이어는 재사용할 수 없습니다.
소스코드를 수정한 뒤 다시 빌드해봅시다. 소스 코드가 변경되었으므로 app.jar 파일이 변경됩니다.
따라서 COPY --from=build /src/build/libs/*.jar app.jar
인스트럭션의 결과가 달라지므로 이전에 캐시된 레이어를 재사용하지 않습니다. 따라서 해당 인스트럭션 다음의 인스트럭션은 수정된 것이 없더라도 재실행됩니다.
따라서 아래 로그에서 COPY, WORKDIR 인스트럭션에 CACHED가 빠진 것을 볼 수 있습니다.
docker build -t spring:3 .
[+] Building 23.0s (13/13) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 37B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/amazoncorretto:17 0.0s
=> [internal] load metadata for docker.io/library/gradle:7.6.1 0.7s
=> CACHED [stage-1 1/3] FROM docker.io/library/amazoncorretto:17 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 6.69kB 0.0s
=> [build 1/4] FROM docker.io/library/gradle:7.6.1@sha256:80f1f49b69b2d94d1bc2c63a9ad8268c3c06129b224f4b2577fb944ae26e1226 0.0s
=> CACHED [build 2/4] WORKDIR /src 0.0s
=> [build 3/4] COPY . . 0.2s
=> [build 4/4] RUN gradle bootJar 21.5s
=> [stage-1 2/3] COPY --from=build /src/build/libs/*.jar app.jar 0.1s
=> [stage-1 3/3] WORKDIR /app 0.0s
=> exporting to image 0.1s
=> => exporting layers 0.1s
=> => writing image sha256:aa750785e5efea06cb5cdf755190d1aaa1460a541112acb5e486e7def7f1102a 0.0s
=> => naming to docker.io/library/spring:3 0.0s
Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
이번에는 Dockerfile을 아래와 같이 수정해보겠습니다.
FROM gradle:7.6.1 AS build
WORKDIR /src
COPY . .
RUN gradle bootJar
FROM amazoncorretto:17
WORKDIR /app
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
COPY --from=build /src/build/libs/*.jar app.jar
캐시를 초기화하고 이미지를 다시 빌드해봅니다.
모든 이미지를 삭제합니다.
docker rmi -f $(docker images -a)
빌드 캐시를 삭제합니다.
docker builder prune
docker build -t spring:1 .
[+] Building 68.2s (14/14) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 251B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/amazoncorretto:17 0.0s
=> [internal] load metadata for docker.io/library/gradle:7.6.1 2.3s
=> [auth] library/gradle:pull token for registry-1.docker.io 0.0s
=> [stage-1 1/3] FROM docker.io/library/amazoncorretto:17 0.0s
=> [build 1/4] FROM docker.io/library/gradle:7.6.1@sha256:80f1f49b69b2d94d1bc2c63a9ad8268c3c06129b224f4b2577fb944ae26e1226 42.4s
=> => resolve docker.io/library/gradle:7.6.1@sha256:80f1f49b69b2d94d1bc2c63a9ad8268c3c06129b224f4b2577fb944ae26e1226 0.0s
=> => sha256:b3eb20d332dad785127ed3f3e6dcc4b0d15bd0c80eed668d36d238b6c154998a 18.46MB / 18.46MB 5.4s
=> => sha256:80f1f49b69b2d94d1bc2c63a9ad8268c3c06129b224f4b2577fb944ae26e1226 1.21kB / 1.21kB 0.0s
=> => sha256:f76840a01c8dcffa48f41af385088d66b51ca6214ebb03f5ffb6c72b1e376fe6 1.79kB / 1.79kB 0.0s
=> => sha256:f3a54ae46b7b05d61cce6ffe7724ecec8fb77191aae25628e1fa7cddfa24c9d4 10.26kB / 10.26kB 0.0s
=> => sha256:6c7698a779f6d8c45a39a6721fb5cce267d66ff8ab5181c55aa6d02c8ddacd01 28.39MB / 28.39MB 14.1s
=> => sha256:498a0978395375469510ee1ec67911c5fc47525d29edceb6407fe24c94ba23e0 191.40MB / 191.40MB 36.3s
=> => sha256:611408d965705e52bf288c19198044f6d0623af144aa26e3c18f917ea296215e 175B / 175B 5.6s
=> => sha256:53b4b9e222f66a540b194cd9c75a811c4f3762826a015ca45f37e771f97b0b78 4.37kB / 4.37kB 5.9s
=> => sha256:0a93e4039b0d410743a10a3bf95b8ef5f1bbb45e6caa43364a38c9240292a0ee 51.10MB / 51.10MB 13.6s
=> => sha256:cce21c45d77e8bc478d3c19b5af3f9bc8a137cf56fed46c308a81162248120e8 122.08MB / 122.08MB 35.7s
=> => extracting sha256:6c7698a779f6d8c45a39a6721fb5cce267d66ff8ab5181c55aa6d02c8ddacd01 0.8s
=> => extracting sha256:b3eb20d332dad785127ed3f3e6dcc4b0d15bd0c80eed668d36d238b6c154998a 0.6s
=> => extracting sha256:498a0978395375469510ee1ec67911c5fc47525d29edceb6407fe24c94ba23e0 2.8s
=> => extracting sha256:611408d965705e52bf288c19198044f6d0623af144aa26e3c18f917ea296215e 0.0s
=> => extracting sha256:53b4b9e222f66a540b194cd9c75a811c4f3762826a015ca45f37e771f97b0b78 0.0s
=> => extracting sha256:0a93e4039b0d410743a10a3bf95b8ef5f1bbb45e6caa43364a38c9240292a0ee 1.5s
=> => extracting sha256:cce21c45d77e8bc478d3c19b5af3f9bc8a137cf56fed46c308a81162248120e8 1.4s
=> [internal] load build context 1.3s
=> => transferring context: 45.20MB 1.3s
=> [stage-1 2/3] WORKDIR /app 0.1s
=> [build 2/4] WORKDIR /src 0.1s
=> [build 3/4] COPY . . 0.1s
=> [build 4/4] RUN gradle bootJar 21.5s
=> [stage-1 3/3] COPY --from=build /src/build/libs/*.jar app.jar 0.1s
=> exporting to image 0.1s
=> => exporting layers 0.1s
=> => writing image sha256:b36fe5a249d0d6ebbd861da30c5d50e82ef0109040f12f2855aee0c88c305bb9 0.0s
=> => naming to docker.io/library/spring:1 0.0s
Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
동일한 이미지로 다시 빌드해봅시다.
docker build -t spring:2 .
[+] Building 0.8s (13/13) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 37B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/amazoncorretto:17 0.0s
=> [internal] load metadata for docker.io/library/gradle:7.6.1 0.7s
=> [stage-1 1/3] FROM docker.io/library/amazoncorretto:17 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 6.31kB 0.0s
=> [build 1/4] FROM docker.io/library/gradle:7.6.1@sha256:80f1f49b69b2d94d1bc2c63a9ad8268c3c06129b224f4b2577fb944ae26e1226 0.0s
=> CACHED [stage-1 2/3] WORKDIR /app 0.0s
=> CACHED [build 2/4] WORKDIR /src 0.0s
=> CACHED [build 3/4] COPY . . 0.0s
=> CACHED [build 4/4] RUN gradle bootJar 0.0s
=> CACHED [stage-1 3/3] COPY --from=build /src/build/libs/*.jar app.jar 0.0s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:b36fe5a249d0d6ebbd861da30c5d50e82ef0109040f12f2855aee0c88c305bb9 0.0s
=> => naming to docker.io/library/spring:2 0.0s
Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
이번에는 소스코드를 수정하고 빌드해봅시다. 이번에는 CACHED [stage-1 2/3] WORKDIR /app
부분을 보면 캐시된 이미지 레이어를 사용합니다.
docker build -t spring:3 .
[+] Building 21.9s (13/13) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 37B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/amazoncorretto:17 0.0s
=> [internal] load metadata for docker.io/library/gradle:7.6.1 0.7s
=> [internal] load build context 0.0s
=> => transferring context: 6.69kB 0.0s
=> [stage-1 1/3] FROM docker.io/library/amazoncorretto:17 0.0s
=> [build 1/4] FROM docker.io/library/gradle:7.6.1@sha256:80f1f49b69b2d94d1bc2c63a9ad8268c3c06129b224f4b2577fb944ae26e1226 0.0s
=> CACHED [build 2/4] WORKDIR /src 0.0s
=> [build 3/4] COPY . . 0.2s
=> [build 4/4] RUN gradle bootJar 20.4s
=> CACHED [stage-1 2/3] WORKDIR /app 0.0s
=> [stage-1 3/3] COPY --from=build /src/build/libs/*.jar app.jar 0.1s
=> exporting to image 0.1s
=> => exporting layers 0.1s
=> => writing image sha256:c5ee9f587a5bf16a10b66aac79d324ba5f51c894eab32b61a9c8e780f921fb6c 0.0s
=> => naming to docker.io/library/spring:3 0.0s
Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
따라서 Dockerfile 스크립트의 인스트럭션은 잘 수정하지 않는 인스트럭션이 앞으로 오고 자주 수정되는 인스트럭션이 뒤로 오도록 배치하는 것이 좋습니다. 이렇게 해야 캐시에 저장된 이미지 레이어를 되도록 많이 재사용할 수 있습니다.
최적화한 Dockerfile을 다시 보면, EXPOSE, ENTRYPOINT 인스트럭션은 COPY 인스트럭션보다 수정할 일이 적으므로 앞에 배치합니다.
COPY 인스트럭션은 소스 코드가 변경되면 app.jar가 변경되므로 복사할 파일의 내용이 변경됬기 때문에 재사용되지 않고 다시 실행됩니다.
이렇게하면 소스코드가 변경되어도 마지막 COPY 인스트럭션을 제외한 WORKDIR, EXPOSE, ENTRYPOINT 인스트럭션을 재사용 할 수 있습니다.
최종 수정한 예제 코드는 https://github.com/nefertirii/docker-example/tree/2819ddc4a6ee5564a89583947fb3934c330274e4 를 참고하시기 바랍니다.
위의 spring-backend 이미지의 Dockerfile 스크립트를 다시 한 번 보겠습니다. 이 스크립트에는 빌드가 여러 단계로 나뉘는 멀티 스테이지 빌드가 적용되었습니다.
FROM gradle:7.6.1 AS build
WORKDIR /src
COPY . .
RUN gradle bootJar
# 멀티 스테이지 빌드
FROM amazoncorretto:17
WORKDIR /app
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
COPY --from=build /src/build/libs/*.jar app.jar
각 빌드 단계는 FROM 인스트럭션으로 시작됩니다. FROM 뒤에 AS를 이용해 이름을 붙일 수도 있습니다.
위의 스크립트는 크게 두 단계로 나뉩니다. 첫 번째 단계인 build 단계에서 소스 코드를 빌드하여 jar 파일을 생성합니다. 마지막 단계인 두 번째 단계에서 build 단계에서 만들어진 jar 파일을 복사합니다. 각 빌드 단계는 독립적으로 실행되지만, 앞 단계에서 만들어진 디렉터리나 파일을 복사할 수 있습니다. 최종 산출물은 마지막 단계의 내용물을 담은 도커이미지 입니다.
정리하면, 빌드 단계에서는 빌드에 필요한 빌드 도구가 설치된 기반 이미지를 사용하여 소스 코드를 빌드합니다. 그리고 실행 단계에서는 애플리케이션을 실행할 런타임이 들어있는 기반이미지를 사용하고, 앞에서 빌드한 애플리케이션을 복사합니다.
이렇게 멀티 스테이지 빌드를 사용하면 어떤 점이 좋을까요?
첫 번째로, 도커가 설치되어 있고 소스 코드와 Dockerfile만 있다면 애플리케이션을 실행할 수 있습니다. 빌드 도구와 자바 런타임을 설치할 필요가 없습니다. 깃허브 같은 코드 저장소에서 다운로드 받은 다음 도커 명령어만 사용하면 애플리케이션을 빌드하고 실행할 수 있습니다.
두 번째로, 최종적으로 생성되는 애플리케이션 이미지에 빌드 도구는 포함되지 않습니다. 멀티 스테이지 빌드에서 생성되는 이미지에는 마지막 단계의 내용물만 포함되므로 빌드 도구 없이 런타임과 애플리케이션만 포함됩니다. 따라서 빌드에 필요한 환경과 실행에 필요한 환경을 분리하여 애플리케이션 실행에 필요한 환경만 이미지에 담아 용량을 줄일 수 있습니다.