보통 개발한 애플리케이션을 컨테이너화할 때 다음과 같은 방법으로 진행합니다.
1. 기본 이미지(CentOS, ubuntu 등)로 컨테이너 생성
2. 애플리케이션 및 환경 설치
3. 컨테이너를 이미지로 commit (myimage:0.0)
위 방법 사용 시 애플리케이션이 동작하는 환경을 구성하기 위해 일일이 수작업으로 패키지를 설치하고 소스코드를 깃에서 복제하거나 호스트에서 복사해야 합니다.
도커는 위와 같은 일련의 과정을 손쉽게 기록하고 수행할 수 있는 빌드 명령어를 제공합니다.
완성된 이미지를 생성하기 위해 컨테이너에 설치해야 하는 패키지, 추가해야 하는 소스코드, 실행해야 하는 명령어, 쉘스크립트 등을 하나의 파일에 기록해 두면 도커는 이 파일을 읽어 컨테이너에서 작업을 수행한 뒤 이미지로 만들어냅니다.
이러한 작업을 기록한 파일의 이름을 Dockerfile이라고 하며, 빌드 명령어는 Dockerfile을 읽어 이미지를 생성합니다.
Dockerfile을 이용하면 직접 컨테이너를 생성하고 이미지로 커밋하는 번거로운 과정을 덜 수 있을뿐더러 Git과 같은 개발도구를 통해 애플리케이션의 빌드 및 배포를 자동화할 수 있습니다.
1. Dockerfile 작성
Dockerfile은 컨테이너에서 수행해야 할 작업을 명시합니다.
[root@localhost ~]# mkdir dockerfile && cd dockerfile
[root@localhost dockerfile]# echo test >> test.html
[root@localhost dockerfile]# vi Dockerfile
#Dockerfile
FROM ubuntu:18.04
MAINTAINER sotest
LABEL "purpose"="practice"
RUN apt update
RUN apt install apache2 -y
ADD test.html /var/www/html
WORKDIR /var/www/html
RUN ["/bin/bash", "-c", "echo hello >> test2.html"]
EXPOSE 80
CMD apachectl -DFOREGROUND
위 Dockerfile은 아파치 웹 서버를 설치한 뒤 로컬의 test.html파일을 웹 서버로 접근할 수 있는 컨테이너의 디렉토리인 /var/www/html에 복사합니다.
Dockerfile은 한 줄이 하나의 명령어가 되고, 명령어(RUN, ADD, FROM 등)를 명시한 뒤에 옵션을 추가하는 방식입니다.
명령어는 소문자로 표기해도 상관없지만 일반적으로 대문자로 표기합니다.
# FROM
- 생성할 이미지의 베이스가 될 이미지
- Dockerfile 작성 시 반드시 한 번 이상 입력되어야 하며, 이미지 이름의 포맷은 docker run 에서 사용할 때와 같음
- 사용하려는 이미지가 도커에 없다면 자동으로 pull 함
# MAINTAINER
- 이미지를 생성한 개발자의 정보
- MAINTAINER는 docker 1.13.0 버전 이후로 사용하지 않음
- LABEL maintainer "test <test@test.com>" 형식으로 교체해 표현할 수 있음
# LABEL
- 이미지에 메타데이터를 추가
- 키:값 형태로 저장되며, 여러 개의 메타데이터가 저장될 수 있음
- 추가된 메타데이터는 docker inspect 명령어로 이미지의 정보를 구해서 확인할 수 있음
# RUN
- 이미지를 만들기 위해 컨테이너 내부에서 명령어를 실행
- 위 예시에서는 apt update 와 apt install apache2 명령어를 실행
- dockerfile 빌드 과정에서는 별도의 입력이 불가능하기 때문에 -y 같은 옵션을 주어야 함
- 별도의 입력을 받아야 하는 RUN이 있다면 빌드명령어는 이를 오류로 간주하고 빌드를 종료
- RUN ["/bin/bash", "-c", "echo hello >> test2.html"] 은 /bin/bash 셸을 이용해 echo hello >> test2.html 을 실행
- RUN ["실행 가능한 파일", "명령1", "명령2", ...] 처럼 배열의 형태로 사용가능
# ADD
- 파일에 이미지를 추가
- Dockerfile이 위치한 디렉토리에서 파일을 가져옴
- 위 예시에서는 Dockerfile이 위치한 디렉토리에서 test.html 파일을 이미지의 /var/www/html 디렉토리에 추가
- 배열의 형태로 사용 가능하며 배열의 마지막 원소가 컨테이너에 추가될 위치가 됨("a","b","c","d" -> d가 위치가됨)
# WORKDIR
- 명령어를 실행할 디렉토리
- cd를 입력하는 것과 같은 기능을 함
- WORKDIR /test 후 RUN touch test가 실행되면 /test 디렉토리 하단에 test라는 파일이 생성됨
# EXPOSE
- Dockerfile의 빌드로 생성된 이미지에서 노출할 포트 설정
- 단지 이 명령으로 노출된 포트가 호스트의 포트와 반드시 바인딩되는 것은 아님
- EXPOSE는 RUN명령에서 모든 노출된 컨테이너의 포트를 호스트에 퍼블리시하는 -P 플래그와 함께 사용됨(이후 설명)
# CMD
- 컨테이너가 시작될 때 마다 실행할 명령어를 설정
- Dockerfile에서 한 번만 사용할 수 있음
- 위 예시에서는 apachectl -DFOREGROUND 커맨드가 내장되어 컨테이너가 시작될 때 자동으로 아파치 웹서버 실행됨
위 도커파일을 해석하면 다음과 같습니다.
먼저 FROM으로 Dockerfile에서 사용할 베이스 이미지를 ubuntu:18.04로 설정합니다.
그리고 이미지 MAINTAINER의 이름을 sotest로 설정하고 생성될 이미지의 라벨을 purpose=practice로 설정합니다.
이어서 RUN으로 apt update 와 apt install apache2 -y 명령어를 차례대로 실행하고 ADD로 Dockerfile이 위치한 디렉토리에서 test.html 파일을 이미지의 /var/www/html 디렉토리에 추가합니다.
WORKDIR로 작업 디렉토리를 /var/www/html로 변경한 뒤 RUN으로 /bin/bash를 이용해 echo hello >> test2.html 을 실행합니다.
이후 EXPOSE로 컨테이너가 사용해야 할 포트를 80번으로 설정하고 CMD로 컨테이너의 명령어를 apachectl -DFOREGROUND로 설정해 이미지 빌드를 마칩니다.
2. Dockerfile 빌드
[root@localhost dockerfile]# docker build -t mybuild:0.0 ./
Sending build context to Docker daemon 3.072kB
Step 1/10 : FROM ubuntu:18.04
---> 81bcf752ac3d
Step 2/10 : MAINTAINER sotest
---> Running in 2ea9fd77b97d
Removing intermediate container 2ea9fd77b97d
---> 4fbae4297a30
Step 3/10 : LABEL "purpose"="practice"
---> Running in bff6e887c7f4
Removing intermediate container bff6e887c7f4
---> 2b7981731d8d
Step 4/10 : RUN apt update
---> Running in 89f44efbe078
...
...
...
...
...
Successfully built e2c408c0f2d8
Successfully tagged mybuild:0.0
[root@localhost dockerfile]#
Dockerfile을 빌드하였습니다.
-t 옵션은 생성될 이미지의 이름을 설정합니다. 해당 옵션 미사용 시 무작위 16진수의 이름으로 이미지가 저장됩니다.
build 명령어의 끝에는 Dockerfile이 저장된 경로를 입력합니다.
[root@localhost dockerfile]# docker run -d -P --name myserver mybuild:0.0
5b8fd6bb1fdb39334552ef2374db04c8005f0ae70d9c853700ed1f76c0b23333
[root@localhost dockerfile]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
5b8fd6bb1fdb mybuild:0.0 "/bin/sh -c 'apachec…" 5 seconds ago Up 4 seconds 0.0.0.0:49153->80/tcp, :::49153->80/tcp myserver
Dockerfile로 생성된 이미지로 컨테이너를 생성하였습니다.
-P 옵션은 EXPOSE의 모든 포트를 호스트에 연결하도록 설정합니다.
Dockerfile에서 EXPOSE를 80번으로 설정했으며 이는 컨테이너의 80번 포트를 사용한다는 의미가 됩니다.
EXPOSE를 이용해 이미지가 실제로 사용될 때 어떤 포트가 사용돼야 하는지 명시할 수 있습니다.
[root@localhost dockerfile]# docker port myserver
80/tcp -> 0.0.0.0:49153
80/tcp -> :::49153
docker ps 혹은 docker port 명령어로 컨테이너와 연결된 호스트의 포트를 확인할 수 있습니다.
[root@localhost dockerfile]# docker images --filter "label=purpose=practice"
REPOSITORY TAG IMAGE ID CREATED SIZE
mybuild 0.0 e2c408c0f2d8 2 hours ago 196MB
Dockerfile에 이미지의 라벨을 purpose=practice로 설정했으므로 위와같이 필터에 이 라벨을 적용할 수 있습니다.
라벨은 부가적인 정보를 부여함으로써 원하는 조건의 컨테이너, 이미지 등을 쉽게 찾을 수 있도록 도와줍니다.
따라서 반드시 기억하는 것이 좋습니다.
3. 빌드 과정
이미지 빌드를 시작하면 도커는 가장 먼저 빌드 컨텍스트를 읽어 들입니다.
■ 빌드 컨텍스트
- 이미지를 생성하는데 필요한 각종 파일, 소스코드, 메타데이터 등을 담고있는 디렉토리를 의미
- 즉 Dockerfile이 위치한 디렉토리
컨텍스트는 빌드 명령어의 맨 마지막에 지정된 위치에 있는 파일을 전부 포함합니다.
따라서 Dockerfile이 위치한 곳에는 이미지 빌드에 필요한 파일만 있는 것이 바람직하며, 컨텍스트는 단순 파일뿐 아니라 하위 디렉토리도 전부 포함하게 되므로 루트디렉토리와 같은 곳에서 이미지를 빌드하지 않도록 주의해야 합니다.
빌드에 불필요한 파일이 포함된다면 빌드 속도가 느려질 뿐더러 호스트의 메모리를 지나치게 점유할 수 있습니다.
이를 방지하기 위해 .dockerignore 라는 파일을 작성하면 이 파일에 명시된 이름의 파일을 컨텍스트에서 제외합니다.
.dockerignore 파일은 컨텍스트의 최상위 경로, 즉 빌드 명령어에서 맨 마지막에 오는 경로인 Dockerfile이 위치한 경로와 같은 곳에 위치해야 합니다.
[root@localhost dockerfile]# pwd
/root/dockerfile
[root@localhost dockerfile]# vi .dockerignore
test2.html
*.html
*/*.html
test.htm?
!test*.html
컨텍스트에서 제외할 파일의 경로는 Dockerfile이 존재하는 경로를 기준으로 합니다.
test2.html 의 의미 - /root/dockerfile/test2.html 제외
*.html 의 의미 - /root/dockerfile/*.html 제외
*/*.html 의 의미 - /root/dockerfile/*/*.html 제외
test.htm? 의 의미 - test.htma, test.htmb, test.htmc ... 제외
!test*.html 의 의미 - test로 시작하는 html 파일은 컨텍스트에서 제외하지 않음
빌드 명령어는 Dockerfile에 기록된 대로 컨테이너를 실행한 뒤 완성된 이미지를 만들어 냅니다.
Removing intermediate container dd483e64b0a5
---> 79ec6762b04d
Step 6/10 : ADD test.html /var/www/html
---> dfd767b18a84
Step 7/10 : WORKDIR /var/www/html
---> Running in 93ddcfa47ed8
Removing intermediate container 93ddcfa47ed8
---> 7b69751b2837
Step 8/10 : RUN ["/bin/bash", "-c", "echo hello >> test2.html"]
---> Running in 49a41123b197
Removing intermediate container 49a41123b197
---> 9474ecc9e23b
Step 9/10 : EXPOSE 80
---> Running in ddba9d8ea343
Removing intermediate container ddba9d8ea343
---> 231d8f097aec
Step 10/10 : CMD apachectl -DFOREGROUND
---> Running in 9311e9d1dbfd
Removing intermediate container 9311e9d1dbfd
---> e2c408c0f2d8
각 Step은 Dockerfile에 기록된 명령어에 해당합니다.
ADD, RUN 등의 명령어가 실행될 때마다 새로운 컨테이너가 하나씩 생성되며 이를 이미지로 커밋(commit)합니다.
즉, Dockerfile에서 명령어 한 줄이 실행될 때마다 이전 Step에서 생성된 이미지에 의해 새로운 컨테이너가 생성되며 Dockerfile에 적힌 명령어를 수행하고 다시 새로운 이미지 레이어로 저장됩니다.
따라서 이미지의 빌드가 완료되면 Dockerfile의 명령어 줄 수만큼의 레이어가 존재하게 되며, 중간에 컨테이너도 같은 수만큼 생성되고 삭제됩니다.
한번 이미지 빌드를 마치고 난 뒤 다시 같은 빌드를 진행하면 이전의 이미지 빌드에서 사용했던 캐시를 사용합니다.
[root@localhost dockerfile]# vi Dockerfile2
FROM ubuntu:18.04
MAINTAINER sotest
LABEL "purpose"="practice"
RUN apt update
[root@localhost dockerfile]# docker build -f Dockerfile2 -t mycache:0.0 ./
Sending build context to Docker daemon 4.096kB
Step 1/4 : FROM ubuntu:18.04
---> 81bcf752ac3d
Step 2/4 : MAINTAINER sotest
---> Using cache
---> 4fbae4297a30
Step 3/4 : LABEL "purpose"="practice"
---> Using cache
---> 2b7981731d8d
Step 4/4 : RUN apt update
---> Using cache
---> 6f2c78d58a2f
Successfully built 6f2c78d58a2f
Successfully tagged mycache:0.0
앞에서 먼저 사용한 Dockerfile의 내용에서 일부 삭제한 후 Dockerfile2라는 파일을 만들었습니다.
-f(--file) 옵션으로 Dockerfile의 이름을 지정하여 새로운 이미지를 빌드해 보았습니다.
Using cache라는 출력 내용과 함께 별도의 긴 빌드 과정이 진행되지 않고 바로 이미지가 생성됐습니다.
이전에 빌드했던 Dockerfile에 같은 내용이 있다면 빌드명령어는 이를 새로 빌드하지 않고 같은 명령어 줄까지 이전에 사용한 이미지 레이어를 활용해 이미지를 생성합니다.
이미지 빌드 중 오류발생 시 빌드명령어가 중지되며 이미지 레이어 생성을 위해 마지막으로 생성된 임시 컨테이너가 삭제되지 않은 채로 남게 됩니다. 또한 이미지의 빌드가 완전하지 않기 때문에 설정한 이미지의 이름이 아닌 none으로 이미지가 생성됩니다. 이러한 이미지를 삭제하기 위해서는 rmi 명령어에 docker images 출력 결과에서 확인할 수 있는 이미지의 ID를 입력합니다.
[root@localhost dockerfile]# docker rmi 이미지ID
캐시를 사용하지 않으려면 빌드명령어에 --no-cache 옵션을 추가합니다.
또한 --cache-from 으로 캐시로 사용할 이미지를 직접 지정할 수도 있습니다.
4. 멀티스테이지를 이용한 Dockerfile 빌드
일반적으로 애플리케이션을 빌드할 때는 많은 의존성 패키지와 라이브러리를 필요로 합니다.
예를들어, Go로 작성된 소스코드를 빌드하기 위해서는 Go와 관련된 빌드 툴과 라이브러리가 미리 설치돼야 합니다.
#main.go
package main
import "fmt"
func main() {
fmt.Println("Hello")
}
#go_Dockerfile
FROM golang
ADD main.go /root/dockerfile
WORKDIR /root
RUN go build -o /root/mainApp /root/main.go
CMD ["./mainApp"]
Hello를 출력하는 간단한 Go 소스코드를 작성한 뒤 Dockerfile을 생성하였습니다.
[root@localhost dockerfile]# docker build . -t go_hello -f go_Dockerfile
[root@localhost dockerfile]# docker build -t go_hello -f go_Dockerfile ./
Sending build context to Docker daemon 6.144kB
Step 1/5 : FROM golang
...
...
...
Successfully built 62b9f1448528
Successfully tagged go_hello:latest
[root@localhost dockerfile]#
dockerfile을 빌드하였습니다.
[root@localhost dockerfile]# docker images | grep hello
go_hello latest 62b9f1448528 53 seconds ago 864MB
단순히 hello를 출력하는 프로그램을 실행하는 이미지임에도 불구하고 이미지의 크기가 864MB 입니다.
실제 실행파일의 크기는 매우 작지만 소스코드 빌드에 사용된 각종 패키지 및 라이브러리가 불필요하게 이미지의 크기를 차지하고 있는 것입니다.
이미지의 크기가 불필요하게 커지는 것을 줄이기 위해 멀티 스테이지(Multi-stage) 빌드 방법을 사용할 수 있습니다.
멀티 스테이지 빌드는 하나의 Dockerfile에 여러 개의 FROM 이미지를 정의함으로써 빌드 완료 시 최종적으로 생성될 이미지의 크기를 줄이는 역할을 합니다.
[root@localhost dockerfile]# vi multi_stage
FROM golang
ADD main.go /root
WORKDIR /root
RUN go build -o /root/mainApp /root/main.go
FROM alpine:latest
WORKDIR /root
COPY --from=0 /root/mainApp
CMD ["./mainApp"]
일반적인 Dockerfile과는 다르게, 2개의 FROM을 통해 2개의 이미지가 명시되었습니다.
첫 번째 문단은 앞과 동일하게 main.go 파일을 /root/mainApp으로 빌드하였습니다.
두 번째 문단의 COPY명령어는 첫 번째 FROM에서 사용된 이미지의 최종 상태에 존재하는 /root/mainApp파일을 두 번째 이미지인 alpine:latest에 복사합니다.(--from=0 은 첫 번째 FROM에서 빌드된 이미지의 최종상태를 의미)
즉, 첫 번째 FROM 이미지에서 빌드한 /root/mainApp 파일을 두 번째 FROM에 명시된 이미지인 alpine:latest에 복사하는 것 입니다.
[ alpine이나 busybox와 같은 이미지는 우분투나 CentOS에 비해 이미지 크기가 매우 작지만 기본적인 프로그램 실행에 필요한 필수적인 런타임 요소가 포함되어 있는 리눅스 배포판 이미지 입니다. 이러한 이미지를 활용하면 경량화된 애플리케이션 이미지를 간단히 생성할 수 있다는 장점이 있습니다. ]
[root@localhost dockerfile]# docker build . -t go_hello:multi-stage -f multi_stage
Sending build context to Docker daemon 7.168kB
Step 1/8 : FROM golang
...
...
...
Successfully built 96ec11e8ef36
Successfully tagged go_hello:multi-stage
[root@localhost dockerfile]# docker images | grep hello
go_hello multi-stage 96ec11e8ef36 30 seconds ago 7.55MB
go_hello latest 62b9f1448528 13 minutes ago 864MB
앞과 동일한 역할을 하는 이미지임에도 불구하고, 이미지의 최종 크기가 크게 줄은 것을 확인할 수 있습니다.
이와 같이 멀티스테이지 빌드는 반드시 필요한 실행파일만 최종 이미지 결과물에 포함시켜 이미지 크기를 줄일 때 유용하게 사용할 수 있습니다.
FROM golang as test
ADD main.go /root
WORKDIR /root
RUN go build -o /root/mainApp /root/main.go
FROM alpine:latest
WORKDIR /root
COPY --from=test /root/mainApp
CMD ["./mainApp"]
또한 위처럼 as로 별도의 이름을 정의해 사용할 수 있습니다.
'docker' 카테고리의 다른 글
도커 컨테이너 다루기(9) - Dockerfile로 빌드할 때 주의할 점 (0) | 2021.06.04 |
---|---|
도커 컨테이너 다루기(8) - 도커파일(Dockerfile)[2] (1) | 2021.06.03 |
도커 컨테이너 다루기(7) - 도커 이미지(Image) (0) | 2021.05.31 |
도커 컨테이너 다루기(6) - 컨테이너 자원 할당 제한 (1) | 2021.05.29 |
도커 컨테이너 다루기(5) - 컨테이너 로깅 (0) | 2021.05.28 |