Docker image 기본 ✏️

·updated
// INDEX

들어가며

도커의 주요 구성요소는 다음과 같다.

  • 도커 이미지
  • 도커 레지스트리
  • 도커 컨테이너

도커 컨테이너를 이해하기 위해선 도커 이미지에 대해 기본적인 이해가 있어야 한다. 그래야 더 재밌는 방식으로 도커 이미지를 만들고 실행할 수 있다.

로컬 환경에 docker 가 설치되어 있다고 가정한다. 필요하다면 docker 설치를 위한 포스트를 작성해봐야겠다.


Docker Image 🤔

컨테이너 기술에서 이미지란 무엇일까?

우리의 친구 ChatGPT 의 답변은 이렇다.

컨테이너 기술에서 이미지란 응용 프로그램과 그 실행에 필요한 모든 파일을 포함한 패키지입니다. 
이미지는 코드, 라이브러리, 설정 파일 등을 담고 있으며, 이를 통해 어디서나 동일한 환경에서 실행될 수 있습니다.
쉽게 말해, 이미지는 컨테이너를 실행하기 위한 템플릿입니다.

아하 ‼️이미지는 컨테이너를 실행하기 위한 템플릿이구나.

사실 위 설명을 바로 이해하기는 헷갈릴 수 있다. 컨테이너에 대한 개념이 부족한 상태에서 컨테이너를 실행하기 위한 템플릿이라는 설명은 머리를 더 혼란스럽게만 한다.

패키지는 뭘까? 동일한 환경에서 실행될 수 있다? 컨테이너는 뭐지? 템플릿은 뭐지? 어떻게 이미지로 컨테이너를 실행할 수 있는거지? 너무 많은 꼬리 질문이 생기기 때문에 이미지에 대해 이해하기 어렵다.

그런데 많은 글에선 이미지를 컨테이너를 실행하기 위한 템플릿이라는 설명부터 시작한다.

다른 방식으로 이미지를 이해하고 시작해보자.

스타벅스 혹은 맥도날드

당신이 프랜차이즈를 보고 이미지와 컨테이너를 떠올리면 좋겠다.
이유는 간단하다. 도커 이미지는 프랜차이즈의 매뉴얼이고, 컨테이너는 프랜차이즈이다.

내가 내일 맥도날드 우리집 앞 점을 연다고 가정해보자.
우리집 앞에 공간만 있다면 본사에서 나와서 모든 것을 맥도날드의 매뉴얼에 따라 만들어 줄 것이다.(물론 입지를 보긴하겠지)
매장 인테리어, 주방 장비, 서비스 절차 등 모든 것이 표준화되어 있다.

그리고 음식을 만드는 재료부터 방법까지 모두 매뉴얼에 적혀있을 것이다.
패티의 두께, 무게, 양상추의 양, 소금의 양, 감자 튀김 튀기는 시간 등 모든 조리법이 규격화 되어 있다.
조리 순서 또한 무조건 매뉴얼화 되어있을 것이다.
참깨빵 위에 순 소고기 패티 두장, 특별한 소스, 양상추, 치즈, 피클, 양파까지!
하나부터 열까지 모든 환경을 갖출 수 있도록 가이드 해주는 것이 바로 매뉴얼이며, 나는 매뉴얼에 따라 오픈 준비를 하고 조리법에 따라 요리하고 손님을 환영하면 된다.

프랜차이즈의 특징을 생각해보자. (이 내용은 컨테이너에서 다시 살펴보자.)

  1. 프랜차이즈의 표준화된 방식을 따라서 주방 시설, 서비스, 음식, 시스템을 갖추게 된다.
  2. 어디서든 동일한 맛을 보장한다. 음식이 입에 안 맞는 나라를 가면 스벅이랑 맥날을 가면 된다.
  3. 모든 과정이 표준화되고 매뉴얼이 존재하기 때문에 새로운 지점을 쉽게 확장할 수 있다.

이것이 가능한 이유는 프랜차이즈는 이 모든것을 순서대로 정의해놓은 매뉴얼이 있기 때문이다.

이를 컴퓨터(혹은 서버) 에 대입해서 생각해보자. 우리의 컴퓨터는 모두 다른 운영체제, 커널 버전, 환경 설정 등을 가진다. 즉 우리가 애플리케이션을 개발하고 실행하는 환경 자체가 모두 다르다.

앞자리 민수는 어제 os 업데이트를 통해서 최신 패치를 받았는데 뒷자리 민석이는 그런거 모른다. 이런 자잘한 차이부터 시작해서 애플리케이션을 실행하는 모든 환경 구성이 다르다. 이렇게 모두가 다른 환경에선 같은 애플리케이션을 실행하더라도 동일한 결과를 보장할 수 없다.

여기서 프랜차이즈 개념을 도입해보자.
운영체제 종류, 운영체제 버전, 런타임, 프로그래밍 언어 버전, 시스템 설정, 환경 변수, 애플리케이션 설정.
우리 컴퓨터에 활용할 수 있는 자원만 있다면 어느 컴퓨터에서든 동일한 설정으로 애플리케이션을 실행할 수 있는 매뉴얼이 내 컴퓨터에 있다고 생각해보자.
나는 그 매뉴얼을 통해 애플리케이션을 실행하면, 어디서 실행하든 동일한 방식의 애플리케이션 실행 상태를 가질 수 있을 것이다.

그 애플리케이션 매뉴얼이 바로 도커 이미지이다.


Docker Image 😎

docker image (이하 이미지) 는 애플리케이션을 컨테이너로 실행하기 위해 모든 환경 설정을 구성해놓은 파일이다.

앞서 설명한 프랜차이즈 예시로 비유를 하자면 다음과 같다.

프랜차이즈 매뉴얼은 프랜차이즈를 성공적으로 오픈하고 운영하기 위해 프랜차이즈에 필요한 모든 정보를 담아놓은 규격이다.

도커 이미지 빌드하는 법

도커에서는 이미지를 만드는 것을 이미지를 빌드한다고 한다.

이는 프랜차이즈 매뉴얼을 만드는 것과 같은 것이다. ‘주방 구조는 이렇게 하고, 빵은 1센치로 자르고, 패티는 119g 으로 분리해서 0.8센치 두께로 굽는다.‘와 같은 매뉴얼은

‘우분투 22.04 환경에 자바 버전 21 을 설치하고 /app 폴더에 my-app 을 빌드하고, 환경변수는 DB_URL 은 aaa 로 설정한 후 빌드한 파일을 실행한다.’ 와 같이 애플리케이션 실행에 필요한 모든 것을 정의하는 매뉴얼을 만드는 것이다.

도커 이미지는 Dockerfile 이란 파일을 작성하고 해당 파일을 빌드해서 만들 수 있다.

FROM openjdk:21-slim

WORKDIR /temp

COPY gradlew build.gradle settings.gradle ./
COPY gradle gradle
COPY src src

RUN chmod +x ./gradlew

RUN ./gradlew bootJar

ENV SERVER_PORT 8080

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "./build/libs/app.jar", "--server.port=${SERVER_PORT}"]

spring boot 로 작성된 웹 애플리케이션을 동일한 환경에서 실행할 수 있는 이미지를 만들수 있는 Dockerfile 예시이다.

각각의 구문은 잠시후에 살펴보도록하자.

Dockerfile 의 문법보다 초점을 맞춰야 할 것은 애플리케이션을 동일한 환경과 구성에서 실행시키기 위한 매뉴얼을 만들기 위해선 Dockerfile 을 이용한다는 점이다. 이미지를 빌드하기 위해선 Dockerfile 을 사용한다.

Dockerfile 은 이미지를 만들기 위한 여려 명령어 구문을 포함하는 텍스트 파일이다.
docker engine 이 이해할 수 있는 문법을 포함하며 지원하는 명령어에 맞는 동작을 수행함으로써 애플리케이션 실행에 필요한 매뉴얼을 만든다.

이미지 빌드

도커는 도커 이미지를 빌드하기 위한 명령어를 제공한다.

docker image build --tag hello-docker:v0.1 --file Dockerfile .

명령어를 실행하면 옵션으로 설정한 Dockerfile 을 기반으로 이미지를 빌드한다.

빌드되어 저장된 이미지는 다음 명령어를 실행해서 확인할 수 있다.

docker image ls -a
# docker image 목록
REPOSITORY     TAG               IMAGE ID       CREATED        SIZE
hello-docker   v0.1              dada10250c6d   2 weeks ago    704MB
nginx          latest            0469e929ca63   2 weeks ago    193MB
postgres       16.2-alpine3.19   9107b150d04f   5 months ago   249MB

이미지의 실체

이미지는 애플리케이션을 동일한 환경에서 실행하기 위한 매뉴얼을 만드는 과정이며,
docker engine 이 이해할 수 있는 문법으로 작성된 Dockerfile 을 통해 이미지를 만드는 것을 이해했다.

그렇다면 이미지는 어떤 과정을 통해 빌드될까?

docker image build --tag hello-docker:v1 --file Dockerfile .
[+] Building 2.8s (12/12) FINISHED                                                                                                     docker:default
 => [internal] load build definition from Dockerfile                                                                                             0.1s
 => => transferring dockerfile: 298B                                                                                                             0.1s
 => [internal] load metadata for docker.io/library/openjdk:21-slim                                                                               2.3s
 => [internal] load .dockerignore                                                                                                                0.0s
 => => transferring context: 2B                                                                                                                  0.0s
 => [1/7] FROM docker.io/library/openjdk:21-slim@sha256:7072053847a8a05d7f3a14ebc778a90b38c50ce7e8f199382128a53385160688                         0.0s
 => [internal] load build context                                                                                                                0.3s
 => => transferring context: 1.20kB                                                                                                              0.3s
 => CACHED [2/7] WORKDIR /temp                                                                                                                   0.0s
 => CACHED [3/7] COPY gradlew build.gradle settings.gradle ./                                                                                    0.0s
 => CACHED [4/7] COPY gradle gradle                                                                                                              0.0s
 => CACHED [5/7] COPY src src                                                                                                                    0.0s
 => CACHED [6/7] RUN chmod +x ./gradlew                                                                                                          0.0s
 => CACHED [7/7] RUN ./gradlew bootJar                                                                                                           0.0s
 => exporting to image                                                                                                                           0.0s
 => => exporting layers                                                                                                                          0.0s
 => => writing image sha256:728362d513bffc0b886d001896b3d4371a9aeb3f1ccf308650cd33f8e9aae486                                                     0.0s
 => => naming to docker.io/library/hello-docker:v1   

이미지를 빌드하면 다음과 같은 빌드 프로세스를 사용자가 확인할 수 있다.

터미널에 찍힌 내용을 확인해보면 CACHED 라는 내용도 있고, [4/7] 과 같은 표시도 볼 수 있다.

이것이 어떤 의미인지 확인해보자.


컨테이너의 시작은 리눅스 기반이며, 리눅스의 핵심적인 철학중 하나는 모든것은 파일이다. 라는 것이다.

이미지는 특정 시점의 파일 시스템의 스냅샷으로 이해할 수 있다. 특정 시점에 파일 시스템의 상태를 사진을 찍은것이다.

먼저 위에서 봤던 Dockerfile 예시를 다시 확인해보자.

FROM openjdk:21-slim

WORKDIR /temp

COPY gradlew build.gradle settings.gradle ./
COPY gradle gradle
COPY src src

RUN chmod +x ./gradlew

RUN ./gradlew bootJar

ENV SERVER_PORT 8080

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "./build/libs/app.jar", "--server.port=${SERVER_PORT}"]

FROM 부터 ENTRYPOINT 에 이르기까지 각각의 명령어는 파일 시스템의 변경 사항을 나타내며, 각각의 명령어 별로 명령어가 실행된 파일 시스템의 스냅샷을 계층(layer) 형태로 저장한다. 이 계층은 변경사항을 포함하고 있으며, 각 계층은 이전 계층을 기반으로 만들어진다.

각 계층은 읽기 전용(immutable)으로, 한 번 생성되면 변경할 수 없다. 따라서 이미지를 수정하기 위해선 새로운 빌드 과정이 필요하다. 이러한 특성 덕분에 도커 이미지는 일관성과 신뢰성을 유지할 수 있다.

도커 이미지는 각 계층을 합쳐서 하나의 통합된 파일 시스템(union file system)을 구성한다. 컨테이너가 실행될 때, 쓰기 가능한 계층이 추가로 제공되며, 이 계층은 컨테이너 내부의 파일 시스템의 변경사항을 저장한다.

각각의 계층은 고유한 식별자를 가진다. 이 식별자는 해당 계층의 내용을 기반으로 생성된 해시값이다. 도커는 내부적으로 명령어를 해시하고, 계층의 결과를 해시하여 내부적으로 캐시에 사용한다. 새로운 명령어가 들어왔을때 명령어 해시값과 이전 레이어의 해시값을 통해 캐시키를 생성하고 해당 키로 된 캐시가 존재한다면 해당 레이어를 다시 사용한다. 이런 방식은 불필요한 재실행을 방지하고 이미지의 빌드 속도를 높인다.

여기서 중요한 점은 선행 명령어의 변경이 파일 시스템에 변화를 준다면, 그 이후의 모든 레이어는 그 변화를 반영한 새로운 파일 시스템 상태를 기반으로 빌드되어야 한다. 결과적으로, 선행 명령어가 변경되면 해당 명령어 이후의 모든 레이어의 해시값이 변경되어 도커는 기존 캐시를 사용할 수 없게 된다.

이미지에 대해서 자세한 내용을 알고 싶은 경우 다음 명령어를 사용할 수 있다.

docker image inspect hello-docker:v0.1

위 명령어를 실행하면 빌드된 도커 이미지의 여러 정보에 대해 조회가 가능하다. 아래는 이미지를 구성하는 각각의 스냅샷인 레이어에 대한 해시값을 나타낸다.

docker image layer


Dockerfile

도커 이미지는 Dockerfile 을 통해 생성된다. 즉, 이미지를 빌드하기 위해선 Dockerfile 의 작성법을 알고 있어야 한다.

Dockerfile 에서 사용하는 명령어 중에서 많이 사용하는 명령어는 다음과 같다.

# 기본이 되는 베이스 이미지 설정
FROM <기본 이미지>

# 호스트 경로에 있는 파일을 도커 컨테이너 내부 파일 시스템으로 복사
COPY <호스트 경로> <컨테이너 경로>

# 도커 내부에서 현재 작업 경로를 설정
WORKDIR <컨테이너 경로> 

# 환경 변수 설정
ENV <키>=<값>

# 빌드 시에만 사용할 변수 설정
ARG <이름>[=<기본값>]

# 외부로 노출할 포트 (EXPOSE 는 표현일 뿐이며 없어도 됨.) 
# 실제 포트를 열기 위해 -p 옵션을 통해 포트를 오픈해야 함.
EXPOSE <포트번호>

# 컨테이너를 실행할때 기본적으로 실행할 명령어
# 여러개가 있다면 마지막 CMD 만 유효
CMD [<명령어>]

# 컨테이너 시작 시 이미지에서 실행될 기본 실행 파일 지정
ENTRYPOINT [<명령어>]

상세한 차이점과 사용 방식은 워낙 방대하기 때문에 필요한 부분에 대해 공식문서를 참고할 수 있다.


docker image tag

이미지에 변경사항이 있어서 새로운 레이어가 추가되거나 기존의 레이어가 없어지는 경우 태그를 달아서 관리할 수 있다.

컨트롤러에 다음과 같은 api 가 추가된 상황을 생각해보자.

// ... in HelloContoller
@GetMapping("/")
public ResponseEntity<String> welcome() {
	return ResponseEntity.ok("Welcome to my new world");
}

이런 경우 새로운 태그를 달아서 이미지를 빌드할 수 있다.

docker image build --tag hello-docker:v1.0 --file Dockerfile .

기존의 버전인 v0.1 에서 v1.0 로 버전이 바뀌었다.

도커 컨테이너 이미지를 조회하면 두가지 버전의 이미지가 존재함을 알 수 있다.

$ docker image ls -a

REPOSITORY            TAG               IMAGE ID       CREATED          SIZE
hello-docker          v1.0              d7dde0bb6bcb   11 seconds ago   681MB
hello-docker          v0.1                d742cc4f47cb   33 minutes ago   681MB

사용자는 원하는 버전의 이미지를 태그에 맞춰서 실행할 수 있다.

코드에서 변경 사항이 있거나 도커 파일에서 이미지를 빌드하는 방식이 변경되었을 때 이미지 태그를 지정해서 사용이 가능하며, 동일한 태그를 덮어쓰거나 새로운 태그를 만들어 사용할 수 있다.

이는 효율적으로 도커 이미지를 관리하는 방식이며, 태그에 종속성의 버전이나, 특징을 기술해서 명확함을 높일 수 있다.


Docker Registry

Docker Registry 는 Docker 이미지를 저장하고 배포하는 중앙 저장소 역할을 하는 시스템이다. 개발자는 빌드한 Docker 이미지를 Docker Registry 에 저장, 관리, 공유할 수 있다. 공식 DockerHub 와 같은 공개 레지스트리와 조직 내에서만 접근 가능한 프라이빗 레지스트리가 있다.

DockerHub 와 같은 공개 레지스트리는 누구나 접근 가능하다. 라이브러리 공식 이미지 등 공개적으로 사용할 수 있는 이미지를 저장하고 공유한다. 프라이빗 레지스트리는 조직 내부에서만 접근 가능한 레지스트리로 특정 조직의 상업용 애플리케이션 전용 이미지등을 저장하고 관리한다. gitlab 의 Container Registry 등 여러 종류가 있다.

Docker Hub Container Image Library | App Containerization

DockerHub 사용법

mysql 의 공식 이미지를 검색해보자.

DockerHub image search

저장소의 이름은 2가지 형태를 가진다.

  1. mysql
  • 해당 이미지는 도커 공식 이미지이다.
  • 공식 이미지는 앞에 저장소 명이 따로 붙지 않고 바로 이미지 명으로 시작한다.
  • 또한 초록색으로 생긴 (추후 바뀔 수도) docker official image 배지를 달아준다.
  • https://hub.docker.com/search?image_filter=official
    • 해당 링크에선 모든 official 이미지에 대한 정보를 볼 수 있다.
    • 탑티어는 memcached, nginx, postgres, ubuntu 등이다.
  1. circleci/mysql
  • 해당 이미지는 도커에서 제공하는 공식 이미지는 아니다.
  • 특정 publisher (혹은 user) - 여기선 circleci 에 의해 운영되는 저장소에서 만든 mysql 이미지를 의미한다.
  • 도커에서 제공하는 공식 이미지가 아니지만 많은 사용을 한다.
  • 특정한 런타임이 추가되었거나 파일 혹은 설정이 더 들어있는 이미지인 경우가 있다.

mysql image overview page

공식 이미지 페이지로 들어오면 가이드 문서가 쭉 정리되어 있어 사용자는 가이드를 읽고 도커 이미지를 사용할 수 있다.

우측 상단엔 이미지를 가지고 오기 위한 도커 명령어를 복사할 수 있으며 내려보면 어떤 식으로 mysql 컨테이너를 실행할 수 있는지 가이드도 잘 나와있다.

mysql overview

태그 탭에 들어가보자.

앞서 말한것처럼 이미지 태그는 각각의 버전마다 다른 변경 사항을 저장하거나 나타낼 때 사용한다.

다양한 버전의 태그와 함께 어떤 아키텍처에서 빌드되었고 취약성에 대한 정보와 함께 이미지 pull 을 위한 명령어, 이미지 사이즈등이 있다.

mysql docker image hub tags tabs

이미지에 들어가면 도커 이미지가 가지는 레이어를 알 수 있다.

details page of specific image&#x27;s tag

앞서 본 도커 파일에서 본 명령어들에 대해 어떻게 구성이 되어있는지 볼 수 있다.