Dockerfile과 Docker Image
🐳 Docker 시리즈 살펴보기 🐳
1. Docker와 Container
➡️ 2. Dockerfile과 Docker Image
3. Docker Volume
4. Docker Network
5. Docker Compose
🐳 Dockerfile
- Docker Image를 만들기 위한 설정파일이다.
- 여러가지 명령어를 토대로 Docker File을 작성하면 설정대로 Docker Image를 만들 수 있다.
- 이때 파일 이름을 'Dockerfile'로 해야한다. 이름을 변경하고 싶다면 빌드시에 옵션(-f/--file)을 주어야 한다.
✅ Dockerfile 항목
Dockerfile 예시는 다음과 같다.
FROM node:18-alpine # nodejs alpine 버전 이미지를 가져온다.
WORKDIR /usr/src/app # 작업 디렉토리를 지정환다.
COPY package.json . # 빌드 컨텍스트의 package.json 을 작업 디렉토리로 지정한 /usr/src/app 으로 복사한다.
RUN npm install # 실행한다.
EXPOSE 3000 # 3000번 포트를 열어준다.
CMD [ "npm", "start" ] # 실행한다.
명령어 | 설명 |
FROM | 베이스 이미지를 지정한다. (필수) |
RUN | 새로운 레이어에서 명령어를 실행하고, 새로운 이미지 생성 - RUN <command> - RUN ["executable", "param1", "param2"] |
CMD | 컨테이너를 생성 및 실행할 명령어. 컨테이너를 생성할때만 실행됨 (docker run) - CMD ["executable", "param1", "param2"] - CMD ["param1", "param2"] - CMD command param1 param2 |
ENTRYPOINT | 컨테이너를 생성 및 실행할 명령어. 컨테이너를 시작할때마다 실행됨 (docker start) - ENTRYPOINT ["executable", "param1", "param2"] - ENTRYPOINT command param1 param2 |
LABEL | 이미지에 메타데이터를 추가 (key-value 형태) - LABEL <key>=<value> <key>=<value> <key>=<value> <key>=<value> ... - LABEL <key>=<value> \ <key>=<value> ,,, |
MAINTAINER | 이미지를 생성한 개발자의 정보 (1.13.0 이후 deprecated) |
EXPOSE | Dockerfile의 빌드로 생성된 이미지에서 열어줄 포트를 의미 - docker run / create시 -p로 설정할 경우 EXPOSE 설정이 오버라이드 된다. - 기본적으로 EXPOSE는 tcp로 설정된다. udp로 지정하려면 EXPOSE 80/udp 처럼 명시해줘야 한다. |
ENV | 이미지에서 사용할 환경 변수 값을 지정 - ENV <key>=<value> <key>=<value> .. - ENV <key>=<value> |
ADD | build 명령 중간에 호스트의 파일 또는 폴더를 컨테이너의 이미지 안으로 복사하는 것 |
COPY | build 명령 중간에 호스트의 파일 또는 폴더를 컨테이너의 이미지 안으로 복사하는 것 - RUN 처럼 체인 형태로 사용할 수는 없으나, 다수의 파일을 하나의 target으로 복사하는것은 가능하다. - COPY file1 file2 /target |
VOLUME | 마운트 할 볼륨을 지정한다. |
USER | 이미지를 어떤 계정에서 실행하는지 지정 / 기본은 root |
WORKDIR | 작업 디렉토리 지정. 없으면 생성 |
ARG | 빌드 시 전달할 수 있는 변수를 정의 |
ONBUILD | 이미지가 다른 빌드의 기반으로 사용될 때 나중에 실행될 트리거 명령을 기재한다. |
STOPSIGNAL | 종료하기 위해 컨테이너로 전송될 시스템 호출 신호를 설정. - SIGTERM / SIGKILL 설정 가능 - 기본값은 SIGTERM이다. - docker run / create 에서 --stop-signal 옵션으로도 설정 가능하다. |
HEALTHCHECK | 컨테이너가 잘 작동하고 있는지 확인하기 위해 컨테이너를 테스트 하는 방법이다. interval, timeout, start period, retries 를 옵션으로 설정할 수 있다. status는 다음과 같다. - 0: success - 컨테이너가 healthy 하고 사용할 준비가 되었음 - 1: unhealthy - 현재 컨테이너가 잘 작동하고 있지 않음 - 2: reserved - do not use this exit code |
SHELL | 기본 셸 지정 |
RUN/CMD/ENTRYPOINT 차이
- RUN : 도커 파일로부터 도커 이미지를 빌드하는 순간 실행되는 명렁어로 라이브러리 설치 부분에서 주로 활용
- CMD : 이미지로부터 컨테이너를 생성하여 최초로 실행할 때(docker run) 실행되는 명령어이지만 변경할 수 있을때
- 컨테이너 실행 시에 파라미터를 추가로 입력하면 파라미터가 우선순위를 갖게 된다. Dockerfile 내에서 딱 한번만 사용 가능하다.
- ENTRYPOINT 의 default 파라미터를 설정하거나 추가 파라미터를 설정하는 용도로도 쓴다.
- ENTRYPONT : docker start 할때 매번 실행 / CMD와 동일하나 최초 실행 시 '꼭' 실행되어야 하는 명령어가 있을 때 사용 / 파라미터 값과 관련 없이 항상 실행된다.
- CMD나 ENTRYPOINT 에서 다수의 명령을 수행하려면 CMD ["/bin/bash", "-c", "echo FIRST COMMAND;echo SECOND COMMAND"] 로 가능하지만
- .sh 파일 같은 스크립트 파일을 작성해서 실행시키는것을 권장
ADD/COPY 차이
- ADD : 로컬 파일 디렉토리, url(원격파일) 복사, tar파일 자동 압축 해제 및 추출 기능이 있다.
- COPY : 단순 로컬 파일 또는 디렉토리를 복사할때 사용한다. (파일의 출처 추적 가능)
ENV/ARG 차이
- ENV : container 시작 시에 사용하는 변수. 해당 이미지를 실행한 컨테이너에서 사용할 수 있다.
- ARG : Dockerfile build 시에 사용하는 변수 (Dockerfile 내에서 사용하거나, Dockerfile에 선언하고 docker build~ 할때 값을 넘겨주어 변수로 사용)
Shell From vs Exec From
- RUN, CMD, ENTRYPOINT 에서 두가지 방식을 선택할 수 있다.
- Shell From
- [ ] 기호 없이 컨테이너의 shell 을 실행시키는 방식이다.
- 를 넘겨주면 리눅스라면 /bin/sh -c , 윈도우라면 cmd /S /C 와 같은 형태로 실행된다.
- Exec From
- [ ] 기호로 문자열 배열로 나타낸다.
- shell 을 통하는 것이 아니기 때문에 실행 가능한 명령과 그 인자만 넘겨줄 수 있다.
- 예를들어 RUN ["echo", "$HOME"] 은 shell processing이 일어나지 않기 때문에 $HOME 라는 변수를 알 수 없다. exec form 으로 의도한 구문을 실행하려면 RUN ["sh", "-c", "echo $HOME"] 로 사용해야 한다.
- 기본적으로 shell은 sh로 실행되기 때문에, bash 로 바꾸기 위해서는 exec form 을 통해 /bin/bash 로 지정해야 한다.
- ex) ENTRYPOINT ["/bin/bash", "-c", "echo hello!"]
🐳 Dockerfile 사용법
$ docker build [PATH] # build context로 지정한다. 현재 디렉토리로 지정하려면 '.' 로 작성한다. -f 옵션을 주지 않으면 'Dockerfile'을 찾아 빌드
$ docker build [URL] # 원격에 있는 Dockerfile 을 이용할 수도 있다.
- docker build 요청이 시작되면 Docker daemon에게 build context의 하위 모든 파일에 대한 정보를 전송한다.
- build context 내에 build되지 않았으면 하는 항목이 있다면 '.dockerignore' 파일에 기재하면 된다.
- '.' 는 현재 경로이며, build context 를 커맨드를 실행하는 현재 경로로 설정하려면 'docker build .' 라고 입력하면 된다.
- Git 레포지토리 임을 특정해서 받으려면 'git://' 이나 'git@' 을 쓰면 된다.
옵션
옵션 | 설명 |
--tag, -t | name:tag 형식으로 name을 짓거나 선택적으로 tag를 설정할 때 사용 |
--rm | default로는 true 이며, 이미지가 성공적으로 빌드되었을때 임시 컨테이너를 즉시 삭제하는 옵션 |
--file, -f | PATH/Dockerfile 이외의 다른 경로나, 다른 파일 이름에 대해 빌드할 때 사용 |
--no-cache | 이미지를 빌드할 때 캐시를 사용하지 않는 옵션 |
🐳 Docker Image
- 컨테이너 실행에 필요한 모든 파일과 설정값을 포함하고 있는 것으로, 더 이상의 의존성 파일을 컴파일하거나 설치할 필요 없는 상태의 파일이다.
- 이미지는 Immutable하며, 하나의 이미지는 여러개의 컨테이너를 생성할 수 있다.
- 컨테이너가 삭제되어도 이미지는 변하지 않는다.
- Read-only이다.
✅ Image Layer
- 하나의 이미지는 여러개의 레이어가 쌓여 만들어진다.
- Dockerfile의 instruction(명령어) 와 이미지 레이어는 1:1 관계를 갖는다.
- 이미지 레이어는 도커 엔진의 캐시에 물리적으로 저장된 파일이다.
- 이미지 레이어는 여러 이미지와 컨테이너에서 공유된다.
- 위 이미지를 보면, ubuntu 이미지는 레이어 A,B,C 를 기반으로 만들어졌다. 그다음 nginx 이미지를 빌드할 때는 레이어 A, B, C를 새롭게 생성하지 않고 이미 있는 것을 캐시에서 사용한다.
- 그래서, 이미지는 공유되므로 이미지를 함부로 수정하면 레이어들을 공유하는 다른 이미지에도 영향을 미치게 된다. 따라서 이미지는 Read-only(읽기 전용) 이다.
- Dockerfile 명령문 하나가 레이어 하나로 구성되는데, 레이어를 생성하기 전에 해당 instrunction 해시값을 조회하고, 캐시에 없다면 생성한다.
- ADD, COPY 명령어는 명령어 String + 해당 파일의 변경 유무를 캐싱 / 그 이외의 명령어는 명령어 String을 캐싱
- 한번 명령문이 실행되면 그 하위 명령문들은 캐시를 확인하지 않고 실행한다.
✅ 예시
예를들어, 아래와 같이 서로 겹치는 레이어가 있는 네개의 이미지가 있다.
다음 명령어로 이미지 목록을 조회한다.
$ docker images
네 개의 이미지의 크기를 모두 합하면 총 12.24GB 임을 확인할 수 있다.
아래 명령어로 실제 디스크를 차지하는 용량을 확인해보자.
$ docker system df
이미지가 실제로 차지하는 용량은 3.684GB이다.
우리는 이로써, 도커는 이미지를 빌드할 때, 캐시에 있는 레이어를 공유하여 사용한다는 것을 알 수 있다
🐳 Dockerfile/Image 최적화
Docker image가 최적화 되면, :
- 이미지 빌드와 배포가 빨라진다.
- 애플리케이션의 보안이 지켜진다. (필요 없는 기능을 넣지 말아야 한다. 예를들어, 어플리케이션에 curl이 필요 없는데 설치했을 경우, 보안 취약점의 이유가 될 수 있다.
1. 꼭 필요한 파일만 이미지에 포함시킨다.
- Dockerfile 스크립트의 명령문 하나하나 마다 이미지 레이어가 하나씩 생긴다.
- 복사한 파일 중 일부를 삭제한다고 해도, 실제로 이미지에서 지워지지 않는다.
- 다음 레이어에서 불필요한 파일을 제거해봐야 소용 없으므로(오히려 용량을 증가시킬 수 있다.), 각 레이어마다 최적화를 해야 한다.
- '.dockerignore' 파일을 이용해 포함시키지 않아야 하는 디렉터리나 파일을 명시해 주는것도 좋다.
- '.gitignore' 형식과 동일하다.
2. 좋은 기반 이미지를 골라야 한다.
- 필수 기능만 있는 가벼운 기반 이미지에서 필요한 기능을 추가하는 것이 좋다.
- 리눅스 컨테이너에서는 alpine 리눅스 또는 데비안 슬림 이미지가 기반 이미지로 좋다.
3. Dockerfile 스크립트의 명령문은 자주 수정하는 순서대로 뒤에 오도록 배치해 캐시를 최대한 활용한다.
- Dockerfile을 빌드할 때, 한번이라도 캐시에 없는 명령문을 수행하려고 하면 그 뒤는 모두 실행되게 된다.
- 이 점을 이용해 Dockerfile을 최적화 할 수 있다.
4. 쉘 기능이 반드시 필요한 것이 아니라면 최대한 exec form 을 사용한다.
RUN ehco $VERSION # Shell
RUN ["echo", "$VERSION"] # Exec
- shell form은 쉘을 통해 실행되므로 불필요한 프로세스를 실행시킨다.
- 반드시 쉘 기능이 필요한 것이 아니라면 되도록 exec form 을 사용한다.
5. 캐시할 수 있는 단위를 구별해야 한다.
- 각각의 RUN 명령어들은 캐시할 수 있는 단위이다.
- 너무 많은 명령어들을 연결해 하나의 RUN으로 실행시키는 것도 캐시가 쉽게 무효화 되어 좋지 않고,
- 너무 세세하게 나누어 RUN을 실행해도 효율적인 캐싱이 되지 않는다.
- update-install 을 하나의 캐시 단위로 묶는것처럼, 잘 구별하여 사용해야 한다.
6. 멀티 스테이지 빌드를 이용해 런타임에 필요로 하는 것들만 포함시킨다.
- 특정 언어로 작성된 소스코드는 빌드하기 위해 관련 빌드 툴과 라이브러리가 필요하다.
- 하지만 이는 실행단계에 필요하지 않을 수 있다.
- 이럴 경우, 멀티스테이지 빌드를 이용해 이를 분리한다.
# 첫번째 스테이지: maven 기본 이미지 지정, jar 패키지 생성
FROM maven:3.5.0-jdk-8-alpine AS builder
# add pom.xml and source code
ADD ./pom.xml pom.xml
ADD ./src src/
# package jar
RUN mvn clean package
# 두번재 스테이지: 생성한 jar 패키지 openjdk 이미지를 이용해 실행. 최종 이미지 생성
From openjdk:8-jre-alpine
# copy jar from the first stage
COPY --from=builder target/my-app-1.0-SNAPSHOT.jar my-app-1.0-SNAPSHOT.jar
EXPOSE 8080
CMD ["java", "-jar", "my-app-1.0-SNAPSHOT.jar"]
- 이렇게 하면, 마지막 스테이지의 이미지가 최종 이미지가 된다.
- 스테이지에 별도로 이름을 부여하지 않으면 (AS ~ 로 지정할 수 있다.) 순서대로 0부터 정수값을 가진다.
- 중간 빌드 결과물을 최종 이미지에 포함시키려면 COPY --from=NAME(or integer) 을 사용하면 된다.