[1 Month Docker] 2. Dockerfile, Docker Image

Dockerfile과 Docker Image 개념을 소개하고, 핵심적인 내용을 설명한다. Docker에 대한 간단한 소개의 내용을 기본으로 가정하고 시작한다.


저번 글에선 Container와 Docker를 체험해보았다. Container는 어떠한 스택의 애플리케이션이든 배포 측면에서 일관된 경험을 제공하므로 사용하는 것이 좋지 않을까 생각한다.

Docker로 Container를 실행하려면 Docker Image가 필요한데, 이번 글에서는 최종적으로 Image를 직접 생성한다(공식적으로는 build 한다고 표현함.).

1. 기초 개념 설명

1. Dockerfile: 이미지 빌드 명령어의 입력으로 들어가는 스크립트이다. 아래와 같은 내용을 담는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Parent Image 지정
# Dockerfile은 이미지를 정의하는 파일이다.
# 새 이미지를 만들 때 다른 이미지의 내용에 기반해 덧씌우는 형태이다.
FROM diamol/node

# 환경 변수 3개 설정
# Docker와 같이 컨테이너 환경으로 앱이 배포되는 경우,
# 환경 변수를 arguments로 많이 활용한다.
ENV TARGET="blog.sixeyed.com"
ENV METHOD="HEAD"
ENV INTERVAL="3000"

# Working Directory를 /web-ping으로 지정 (폴더 생성 후 이동함. mkdir && cd)
WORKDIR /web-ping

# Host의 app.js 파일을, Working Directory(.)에 복사
COPY app.js .

# node로 다음의 js를 실행
# CMD 명령어는 컨테이너 실행 시에 1회 수행되는
# container.once('start', callback)과 같다.
CMD ["node", "/web-ping/app.js"]

아마 셸 스크립트에 익숙한 사람은 셸 스크립트와 다름 없다고 생각할 것이다. 맞다. 똑같다. 아마 셸 스크립트가 익숙하지 않으면 Dockerfile에 쉽게 친해질 순 없을텐데, 리눅스 환경 구성 기초 | T 아카데미리눅스 커맨드 라인 & 쉘 스크립트 #1 | ABCD DevOps라는 좋은 자료가 있으니 참고하자.

Dockerfile 안에서만 쓸 수 있는, Dockerfile에서 쓰일 만한, 명령어가 10개 정의돼있다. 이 명령어들이 주축이 돼서 Dockerfile의 내용을 구성하게 된다.

2. Image: 이미지는 Dockerfile에서 기술한 내용이 실행된 모습을 스냅샷 형태로 담은 파일이다.

  • 컨테이너 실행 시 이미지를 통해 Dockerfile에 정의된 내용이 그대로 재현된다.

3. Image 받아오기: 이미지를 직접 생성하지 않고, DockerHub 등의 Docker Registry (이미지 저장 서버)에서 받아올 수도 있다. 단순히 받아오기만 하는 명령어는 docker image pull 이다.

  • docker image pull diamol/ch03-web-ping 을 실행해 DockerHub에서 이미지를 받자.
  • 하나의 이미지를 받는데, 여러 Pull Complete가 표시돼있다. (나중에 설명한다.)

4. Image 빌드: docker image build 명령어를 실행하면, 이미지는 자동으로 빌드된다.

예: docker image build --tag web-ping . => web-ping이라는 이미지를 생성.

  • (Mandatory) .은 Dockerfile 및 COPY 등에서 Host의 기준 디렉토리로 사용된다.
  • (Mandatory) --tag는 이미지의 이름을 지정한다.
  • 주의: 파일을 Windows -> Linux로 복사하는 경우, 권한이 rwxrwx로 지정되는데, 이는 서로 권한 정보가 호환되지 않기 때문이다.
  • 로컬에서 직접 빌드된 이미지는 도커 엔진에 캐시돼 보관된다.
  • 새로운 버전을 빌드하려는 경우, --tag web-ping:v2와 같이 :으로 버전을 구분하여 명시하면 된다.

Docker Image Build Process Example

5. Image 실행(컨테이너로):

  • docker container run {image_name}으로 실행

6. Image Layer:

이미지에는 생성 과정에 대한 메타데이터도 포함된다. 이미지 생성 과정을 통해

  • docker image history web-ping
  • Image History Example

Docker Image는 Image Layer라는 더 작은 개념으로 구성되며, Dockerfile의 각 명령(CREATED BY) 마다 Layer가 생성된다.

  • 이미지는 각 Layer의 논리적인 집합이다.
  • Layer는 도커 엔진에 물리적인 파일의 형태로 캐시되는 단위이다.
  • 이미지 간에 Layer가 공유되므로 전체 용량 부하를 낮출 수 있다.
    • docker image ls로 논리적인 용량을 확인할 수 있지만, docker system df로 이미지가 차지하는 물리적인 용량을 확인할 수 있다.
    • docker system df

이런 Image Layer 캐시를 활용하려면 조건이 필요한데: Layer 이전의 Layer 들의 내용과 순서가 바뀌지 않아야 한다.

  • 이전 내용이 바뀌었는데, 이 명령(Layer)을 실행한 결과가 같음을 보장할 수 없다.
  • 만약 내용을 바꾸는 경우, 이 Layer에 의존하고 있던 모든 이미지에 영향을 끼친다.
  • 그러므로, 이전 Layer가 변경되는 경우, 이후 Layer는 캐시로 사용될 수 없게 되고, 새로 Layer를 생성하게 된다.

7. Layer 캐시 최적화 전략: Layer 캐시 활용을 통해 전체 용량과 이미지 빌드 시간을 줄일 수 있다.

  • 이미지에서 변하지 않는 부분을 최대한 먼저 실행해 새로 빌드할 Layer 수를 줄인다.

  • 캐시 사용 가능 여부는 Instruction의 내용과 Arguments(명령어 내용일 수도 있지만, COPY와 같은 경우 파일의 내용까지.)로 Hash 값을 만들고 비교하여 결정한다.

  • Hash가 일치하는 경우 빌드하지 않고 도커 엔진에 캐시된 Layer를 사용한다. 일치하지 않는 경우, 해당 Layer부터 최종 Layer까지 새로 빌드한다. (뒷 Layer의 해시가 같아도, 재사용할 수 없다.)

  • docker build image cache

  • app.js 파일을 수정한 후 (nano app.js) 빌드한 모습이다. COPY app.js를 수행하는 step 6가 다시 Layer를 만듦을 확인할 수 있고, 이후 Layer인 step 7은 바뀐 내용이 없지만 앞 Layer가 바뀌어서 다시 만들어짐을 확인할 수 있다.

8. Layer 캐시 최적화 예시:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FROM diamol/node

# 시작 시 실행될 명령어를 지정하는 것이므로, 어디에 놓아도 상관 없다.
# 캐시를 위해 앞에 놓는다.
CMD ["node", "/web-ping/app.js"]

# 환경 변수 3개를 한 번에 등록해 Layer 개수를 줄였다.
# 개수를 줄인 것과 캐시 최적화는 큰 연관은 없지만...
ENV TARGET="blog.sixeyed.com" \
METHOD="HEAD" \
INTERVAL="3000"

WORKDIR /web-ping

COPY app.js .

이제 docker image build -t web-ping:v3를 실행해보자. 환경 변수 개수가 줄어들어 7단계에서 5단계로 줄었음을 확인할 수 있다.

이제부턴 app.js를 수정해도 마지막 Layer만 바뀐다.


2. 실습

1. 목표:

diamol/ch03-lab 폴더의 이미지에서 /diamol/ch03.txt 파일을 수정하고 새 Image를 생성하라. 이 때 Dockerfile을 수정해서는 안 된다.

2. 힌트:

  • -it으로 컨테이너에 키보드 I/O 가능
  • 컨테이너 파일 시스템이 Exit 상태에도 제거되지 않음을 활용
  • docker container --help로 모르는 명령어에 대해 공부할 것

3. 처음 생각한 접근 방법:

  1. Container에서 일단 파일을 수정한다.
  2. 컨테이너로 이미지를 생성해낸다. 명령어를 찾아보자.

4. 실제 수행 과정:


1. 일단 이미지를 빌드함

cd ../../lab (빌드를 위해 lab 폴더로 이동)

docker build image -t ch03-lab . (빌드 성공)


2. 이제 컨테이너를 실행해야 함

docker container run ch03-lab (실패)

docker container ls (없었음)

cat Dockerfile (CMD 등 명령어 실행이 없고, COPY 뿐이었음)


3. 컨테이너에서 수행할 명령어로 주어 실행해야 함

docker container run ch03-lab /bin/bash (실패)

docker container run ch03-lab /bin/sh (이미지에 bash가 없었음..)

vi ch03.txt (텍스트 파일 수정)

exit (sh 나옴)


4. 정지된 컨테이너를 이미지로 빌드해야 함

Docker Commit Reference를 참고해서 빌드 명령어 학습

docker container ls --all 로 종료된 컨테이너 ID 확인 (67a)

docker image commit 67a ch03-lab:v2 (무슨 해시값이 출력됨..)

docker image ls (v2로 생성됨을 확인)

docker container run ch03-lab:v2 cat ch03.txt (파일 갱신됨을 확인)

끝!


Lab 하면서 배운 점:

docker commit 명령어로 컨테이너 내용으로 이미지를 빌드할 수 있다는 점.

  • 다만 이렇게 되면 Dockerfile은 없는게 아닌가?

docker container run {IMAGE} {COMMAND}로 명령어를 실행할 수 있음

  • 다만 이는 이미지에서 수행하는 명령어가 없는 경우에 한한 것 같고, docker container exec으로 셸을 띄우는 것이 일반적인 것 같다.

TO DO:

  • 컨테이너에서 Commit으로 생성한 이미지에서 Dockerfile을 추출할 수 있을지 확인해보기

[1 Month Docker] 1. Docker의 기본 컨셉과 Hello World

Docker의 기본 컨셉을 다루고, 간단한 Hello World를 실습한다.


build, share, run:

  • build: (생략)
  • share: DockerHub에서 이미지를 공유할 수 있다.
  • run: 공유된 이미지를 통해 누구나 컨테이너를 실행할 수 있다.

이미지? 일단 Docker의 재사용 단위라고 생각하자.

도커 컨테이너? 애플리케이션을 담은 박스.

  • 이 박스에는 기기명, IP 주소, 스토리지가 딸린, Docker에서 만들어낸 논리적인 가상 컴퓨터가 있다.

  • 애플리케이션은 이 컴퓨터에서 실행된다.

  • 박스 안의 애플리케이션은 박스 밖을 볼 수 없다.

  • 이 박스는 여러 개가 동시에 실행될 수도 있다.

  • 박스는 같은 실제 컴퓨터를 공유하면서 격리된 환경을 갖는다.

일관된 작업 방식: 아무리 애플리케이션이 복잡하더라도 Docker Image 단위로 Share, Run 만 하면 된다. 몇 개의, 어떤 컴포넌트, 설정 파일, 라이브러리를 사용하는지는 중요하지 않다.

Portability: Docker가 있는 컴퓨터에선 명령어 하나로 곧바로 설치가 가능하다.

효율적인 자원 활용: 도커는 VM이 그렇듯, 여러 애플리케이션을 동시에 실행하는 것으로 컴퓨터 자원을 최대한 활용할 수 있다. 다만 VM보다 나은 점을 아래 표로 정리했다.

사용 자원 Docker VM
Guest OS 사용 여부 No (커널 공유) Yes
가상화 리소스 비용 매우 낮음 (커널 공유) 독립적인 OS 수준
Gust OS Update 다운로드 Base Image 교체 수동 설치
아주 작은 앱 띄우기 Yes No
인수인계/배포 비용 A Dockerfile hours of installation

책에서는 Guest OS License 비용 문제에서도 차이가 난다고 언급했지만, Docker Image 형태로 쓴다고 해서 License 비용이 낮아지거나 사라지지는 않을 것 같다. 반대로 대수가 늘어나기 때문에 Open Source 기반으로 사용하지 않을까 생각이 든다.

네이티브 vs Docker vs KVM(VM 계열) 벤치마크 p.19 참고

주요 도커 명령어:

명령어 기능
docker container ls 실행 중인 컨테이너의 목록 표시
docker container ls –all 전체 컨테이너의 목록 표시 (종료된 것 포함)
docker container run –detach {IMG} 컨테이너를 백그라운드로 실행
docker container run –publish 8088:80 {IMG} Host의 8088 포트로 Listen하여 컨테이너의 80포트로 전달
docker container inspect {ID} 컨테이너의 상세 정보를 JSON으로 출력
docker container stats {ID} 컨테이너가 사용하는 Host 자원 출력
docker container rm (–force) {ID} 컨테이너를 완전히 제거 (실행 중인 경우 force)
docker container rm –force $(docker container ls –all – quiet) 모든 컨테이너를 강제 제거

종료된 컨테이너는 제거된 것이 아니어서 계속 용량을 차지하며, 아래 작업이 가능하다.

  • 그대로 다시 실행
  • 컨테이너 내의 App이 생성한 로그를 확인
  • 파일을 Host에서 or Host로 복사

컨테이너의 네트워크:

  • 기본적으로, 각 컨테이너는 Host 네트워크에 대해 격리된다. 컨테이너는 Host 내의 가상 사설망으로 구성된다.

  • Docker는 Host의 네트워크 트래픽을 가로채 컨테이너로 보낼 수 있다.

Docker가 컨테이너를 실행하는 방법:

Docker Engine은 Docker Backend이다. Docker API(HTTP 기반의 REST API)를 제공한다. 이미지 재사용에 관한 기능은 직접 하고, 컨테이너는 containerd에 기반해 관리한다고 한다. containerd는 CNCF에 의해 관리되는 오픈소스 프로젝트이다.

Docker CLI: Docker의 Frontend이다. Docker Engine과 소통하는 방법을 제공한다.

기타 정보:

Docker는 가장 인기가 많은 컨테이너 플랫폼이지만, 다른 기술도 있으며 컨테이너 기술로 인해 플랫폼에 락인될 걱정은 하지 않아도 된다.

Docker는 이미지를 사용해 컨테이너를 실행한다. 이 때 이미지가 로컬에 있어야 한다. docker container run을 할 때에 없으면 docker pull을 받게 된다. 한 번 다운로드한 이미지는 재사용한다.

도커 컨테이너 Id는 컨테이너의 hostname이 된다.

컨테이너를 선택할 때, 이름 앞 몇글자만 입력해도 된다. 예: f1695...일 때, docker container top f1만 해도 된다.


실습

솔루션

목표: 실행 중인 Apache 컨테이너에서 index.html을 변경하라.

힌트:

  • 컨테이너는 독립된 파일 시스템을 가지며, 컨테이너 내의 웹 서버 또한 컨테이너의 파일 시스템의 파일을 제공한다.

  • docker container 명령어를 통해 컨테이너에서 수행할 수 있는 명령어 목록을 볼 수 있다.

  • docker {command} --help를 통해 해당 명령어의 상세 설명을 확인할 수 있다.

  • diamol/ch02-hello-diamol-web 이미지는 /usr/local/apache2/htdocs 폴더 내의 파일을 정적으로 제공한다. (윈도우의 경우, C:\user\local\apache2\htdocs 폴더.)


내 풀이

풀이 과정을 서술함.

1. 제공된 컨테이너 트러블 슈팅: 일단 ch02-hello-diamol-web 의 기본 포트인 8088은 접속할 수가 없었다. 그래서 DockerHub 가서 Apache 이미지를 받아서 실행해봤다. 8080 포트로 잘 되더라. 이 때 명령어가 $ docker run -dit --name my-apache-app -p 8080:80 -v "$PWD":/usr/local/apache2/htdocs/ httpd:2.4 였는데, 배운 점:

  • -dit: --detach --interactive의 약자인데, -dit가 필요한 이유를 보면, bash 스크립트가 엔트리 포인트인 경우 -d만 하면 정지된 상태에서 아무것도 못한다고 한다. -it를 줘서 셸이 있어야 스크립트가 실행된다고 한다.
  • -p: --publish의 약자이다.
  • -v: 아직 안 배웠지만, 볼륨 개념일 것으로 추정된다.

도커 자체의 네트워크 문제가 아님을 알고, 80으로 하니까 잘 됐는데, 이유는 모르겠다.

2. 컨테이너 셸 접속: 일단 docker container exec -it --tty {id} /bin/bash 로 접속할 순 있었다. (나오는건 exit 치면 된다.)

3. 직접 파일 수정: 무슨 망할 기반 이미지를 쓰는지 vi 밖에 지원을 하지 않아서 직접 수정은 포기했다. 파일을 복사해야 하는데, 어떻게 하는지 모르겠다.

4. 파일 복사 방법: Dockerfile을 수정하는 게 가장 쉬울 것 같았지만, 제공되지 않아서 할 수 없었다. 복사를 해야 하는데, 호스트에서 컨테이너로 파일 복사하기docker cp 명령어를 배워서 수행했고, 성공했다.


매우 작은 작업이었지만 너무 오랜 기간이 걸렸다. 아무래도 기록하면서 하니까 오래 걸리고, 책의 내용을 요약했음에도 불구하고 며칠만에 다시 보는거여서 오래 걸렸다.

많이 헤맨 덕분에, docker container ls, docker container rm, docker container exec, docker container run은 정말 많이 사용해서 다행이다.


참고 자료: Docker In A Month of Lunches (Manning, 2020)


추가로 읽을 것: Docker와 VM