[Docker] Dockerfile의 구조와 문법 + Spring Boot 프로젝트 Docker로 실행
Dockerfile이란?
Dockerfile이란?
Dockerfile
Docker 이미지는 Dockerhub을 통해 다운받아서 사용할 수 있다. 이 Docker 이미지들도 누군가 만들어서 Dockerhub에 올려놓은 것이다.
Docker 이미지를 만드려면?
Dockerfile이라는 것을 활용해서 Docker 이미지를 만들 수 있다.
Dockerhub에 올려놓은 Docker 이미지가 아닌, 나만의 Docker 이미지를 만들고 싶을 수 있다. 이럴 때 Dockerfile을 활용하면 나만의 Docker 이미지를 만들 수 있다.
정리하자면, Dockerfile이란 Docker 이미지를 만들게 해주는 파일이다.
Dockerfile 문법
[FROM]
- 생성할 image의 베이스가 되는 image를 설정한다.
- Dockerfile 작성시 필수로 설정해야 한다.
- 멀티 스테이지(Multi Stage) 빌드시 여러 개를 사용할 수 있다. (아래 추가 내용 기술)
Example)
FROM mirror.gcr.io/library/alpine:3.16
[LABEL]
- 생성할 image에 key=value 형식으로 metadata를 추가한다.
- 다수의 metadata를 추가할 수 있다.
Example)
LABEL maintainer="hackerpark.tistory.com"
LABEL appversion="1.3.1"
[ENV]
- 생성할 image에 추가할 환경변수를 설정한다.
- ENV에 설정된 환경변수는 image를 통해 생성된 container 내부에서도 사용이 가능하다.
- ENV가 설정된 image로 container를 생성할때 옵션을 사용하여 Dockerfile에 설정한 ENV를 오버라이드(override) 할 수 있다. (Default ENV를 설정한다고 생각하는게 좋다.)
Example)
ENV CUSTOM_ENV value
ENV MY_ENV="custom_value"
[ARG]
- Dockerfile 내부에서 사용할 변수를 key=value 또는 key 형태로 설정한다.
- ARG를 key만 입력하면 build시 해당 key 값의 argument가 입력될 것이라는 명시를 나타낸다.
- ARG를 key=value까지 입력한 경우 build시 argument가 입력되지 않아도 value로 적용된다.
- ARG를 key=value로 입력한 상태로 build시 argument를 입력할 경우 오버라이드(override)된다.
Example)
ARG ALPINEVER=3.16
FROM mirror.gcr.io/library/alpine:${ALPINEVER}
[RUN]
- image를 만드는 과정(layer)에서 FROM으로 설정된 베이스 image에 추가로 실행할 명령어를 입력한다.
Example)
FROM mirror.gcr.io/library/alpine:3.16
RUN apk --no-cache add libc6-compat curl && \
rm -rf /var/cache/apk/*
[USER]
- image를 만드는 과정(layer)에서 사용할 사용자 계정을 설정한다.
- USER 를 사용하기 위해서는 해당 USER가 기존 layer에 생성되어 있어야 한다.
- USER 설정 이후 아래 Dockerfile(layer)에 모두 적용된다.
- USER를 설정하지 않으면 슈퍼 유저로 진행된다.
Example)
RUN adduser hackerpark --disabled-password --gecos ""
USER hackerpark
[WORKDIR]
- image를 만드는 과정(layer)에서 기본으로 작업할 디렉토리를 설정한다.
- 쉘 스크립트의 cd와 유사하다.
- WORKDIR 설정 이후 아래 Dockerfile(layer)에 모두 적용된다.
Example)
# create /tmp/test.txt
WORKDIR /tmp
RUN touch test.txt
[COPY]
- image를 만드는 과정(layer)에서 추가할 파일과 추가될 경로를 입력한다.
- 추가할 파일은 Dockerfile이 있는 경로를 기반으로 입력한다. (context folder)
- 추가할 파일의 경우 context folder의 상위 디렉토리에 접근할 수 없다. (context folder를 변경해야 한다.)
Example)
COPY my_file /tmp/test_my_file
[ADD]
- image를 만드는 과정(layer)에서 추가할 파일과 추가될 경로를 입력한다.
- 추가할 파일은 Dockerfile이 있는 경로를 기반으로 입력한다. (context folder)
- 추가할 파일의 경우 context folder의 상위 디렉토리에 접근할 수 없다. (context folder를 변경해야한다.)
- ADD를 사용하여 tar 파일을 추가할 경우 tar 파일을 자동으로 해제해 준다.
- ADD를 사용하여 추가할 파일을 URL로 입력할 수 있다.
- 추가할 파일과 추가될 파일의 이름을 바꿀 경우 rename 된다.
Example)
ADD package.tar /tmp/
[EXPOSE]
- image를 통해 생성되는 container에서 노출할 port를 명시한다.
- 실제로 bind 하기 위해선 container 생성 시 옵션을 추가해야 한다.
Example)
EXPOSE 8080
[CMD]
- image를 통해 생성되는 container에서 실행할 command를 입력한다.
- Dockerfile 내부에서 한 번만 사용 가능하다. (생략 가능)
- container 실행 시 cmd를 입력하는 경우 CMD 내용은 오버라이드(override)된다.
- ENTRYPOINT 설정 시 CMD의 내용이 ENTRYPOINT의 파라미터로 변경된다.
Example)
CMD ["/bin/sh", "-c", "ls /"]
[ENTRYPOINT]
- image를 통해 생성되는 container에서 실행할 command를 입력한다.
- Dockerfile 내부에서 한 번만 사용 가능하다. (생략 가능)
- container 실행 시 entrypoint를 입력하는 경우 ENTRYPOINT 내용은 오버라이드(override)된다.
- CMD가 설정될 경우 ENTRYPOINT의 파라미터로 사용된다.
Example)
ENTRYPOINT ["/bin/sh", "-c", "ls /"]
[RUN] vs [ENTRYPOINT]
RUN vs ENTRYPOINT
RUN은 이미지 생성 과정에서 필요한 명령어를 실행시킬 때 사용
ENTRYPOINT는 생성된 이미지를 기반으로 컨테이너를 생성한 직후에 명령어를 실행시킬 때 사용
예시
미니 컴퓨터 환경을 ubuntu로 구성, git을 설치
이미지 생성 과정에서 git 설치, 컨테이너 생성 직후 500초 동안 일시정지(디버깅을 위함)
1. Dockerfile 작성
FROM ubuntu
RUN apt update && apt install -y git
ENTRYPOINT ["/bin/bash", "-c", "sleep 500"]
2. 이미지 빌드 및 컨테이너 실행
docker build -t my-server .
docker run -d my-server
docker exec -it [Container ID] bash
git -v # 컨테이너 내에 git이 잘 설치됐는 지 확인
[ONBUILD]
- Dockerfile을 통해 생성된 image가 다른 Dockerfile에서 FROM을 통해 베이스 image로 사용되어 build 될 때 실행할 명령어를 입력한다.
Example)
FROM mirror.gcr.io/library/alpine:3.16
LABEL imagename="hackerpark"
LABEL version="1.0"
RUN echo "First Image Build"
ONBUILD RUN echo "First Base Image" >> /first.txt
ENTRYPOINT ["/bin/sh", "-c", "ls /"]
image build (hackerpark:1.0)
hackerpark:1.0 image로 실행한 container에서는 first.txt가 생성되지 않았다.
FROM hackerpark:1.0
LABEL imagename="hackerpark"
LABEL version="1.1"
RUN echo "Second Image Build"
RUN cat /first.txt
Step 3/3 에서 First Base Image가 출력되는것을 확인할 수 있다.
hackerpark:1.1 image로 실행한 container에서는 first.txt가 생성되어 있다.
[STOPSIGNAL]
- image를 통해 생성되는 container 종료 시 사용될 SIGNAL을 설정한다.
- 설정하지 않는 경우 기본 SIGTERM이 사용된다.
Example)
STOPSIGNAL SIGKILL
[HEALTHCHECK]
- image를 통해 생성되는 container에서 실행되는 프로세스의 상태를 확인하기 위해 설정한다.
- image에 curl이 포함되어야 한다.
Example)
FROM nginx:1.21.3-alpine
RUN apk --no-cache add curl && \
rm -rf /var/cache/apk/*
HEALCHECK --interval=10s --timeout=5s --retries=3 CMD curl -f http://localhost || exit 1
- --interval 옵션을 통해 healthcheck의 interval을 설정한다. (ex: --interval=10s, --interval=1m)
- --timeout 옵션을 통해 healthcheck CMD의 timeout을 설정한다.
- --retries 옵션을 통해 healthcheck CMD의 timeout 제한 개수를 설정한다.
[SHELL]
- Dockerfile에서 사용할 기본 shell을 설정한다.
- Linux의 경우 "/bin/sh -c"를 기본값으로 사용한다.
Example)
SHELL ["/bin/bash", "-c"]
Dockerfile Multi Stage Build (멀티 스테이지)
Dockerfile을 통해 image를 생성하는 과정에서 필요한 라이브러리와 패키지들을 모두 포함하기 때문에 최종 결과물로 나온 image의 크기가 GB가 넘어가는 일도 빈번하게 발생한다.
따라서 image를 만드는 과정과 image를 통해 container로 실행하기 위해 필요한 영역을 구분 짓는다면 최종 결과물인 image는 경량화가 될 수 있다.
이를 위해 Dockerfile에서 멀티 스테이지(Multi Stage) 기능을 지원하고 있다.
멀티 스테이지(Multi Stage)를 사용하기 위해서는 image를 만드는 과정에서 필요한 내용과 최종적으로 실행할 환경을 분리하는 작업이 필요한데, 이를 FROM을 사용하여 구분할 수 있다.
ARG GOVERSION=1.19
FROM golang:${GOVERSION} as builder
RUN apt-get update && apt-get install -y \
btrfs-progs \
crun \
git \
golang-go \
go-md2man \
iptables \
libassuan-dev \
libbtrfs-dev \
libc6-dev \
libdevmapper-dev \
libglib2.0-dev \
libgpgme-dev \
libgpg-error-dev \
libprotobuf-dev \
libprotobuf-c-dev \
libseccomp-dev \
libselinux1-dev \
libsystemd-dev \
pkg-config \
uidmap \
unzip \
&& rm -rf /var/lib/apt/lists/*
ADD . /project
WORKDIR /project
RUN go mod download
ADD . /project
RUN go build -o my_go_app
#################################################
# FROM을 사용하여 Stage 분리
#################################################
FROM mirror.gcr.io/library/alpine:3.16
LABEL maintainer="hackerpark" \
version="1.0"
RUN apk --no-cache add libc6-compat curl&& \
rm -rf /var/cache/apk/*
COPY --from=builder /project/my_go_app /
EXPOSE 8080
ENTRYPOINT ["/my_go_app"]
위의 예시 Dockerfile을 보면 FROM이 두 번 사용된 것을 확인할 수 있다. (두 번 이상도 사용이 가능하다.)
go application을 build 하기 위해 필요한 모든 의존성 패키지를 설치를 진행하는데 이 파일들이 모두 최종 image에 포함된다면 DISK의 압박이 상당할 것이다.
따라서 첫 FROM golang image에 as builder라는 이름을 붙여서 아래의 FROM alpine image에서 활용하는 것을 볼 수 있다.
Go Application 빌드에 필요한 모든 과정은 위쪽 FROM의 stage에서 진행하고, 최종에는 아래의 FROM인 alpine 환경에 빌드되어 나온 go binary만 가져와서 image를 만드는 것이다.
이런 식으로 Dockerfile의 Stage를 분리하여 image의 크기를 상당히 줄일 수 있다.
멀티 스테이지(Multi Stage) 환경에서 ARG 사용
FROM과 ARG의 위치에 따라 ARG를 사용하는 방법이 다르다.
위에서 사용한 예시의 경우 FROM 보다 위에 ARG가 선언(정의)되어 있는데 이런 경우 ARG의 아래쪽 FROM에서 모두 사용이 가능하다.
만약 FROM 아래에 ARG가 선언(정의)되어 있다면 그 ARG를 포함하는 FROM에서만 사용이 가능하고 다른 FROM에서는 ARG를 다시 정의해서 사용해야 한다.
Dockerfile 작성 시 알아두면 좋은 점 (Cache)
Dockerfile은 application을 container화 하기 위한 과정을 기록(layer)하는 것이고 Dockerfile의 명령어(문법)가 한 개의 layer를 구성한다라고 이해하면 편하다.
또한 layer는 이전 과정에서의 변경되는 것을 기록하기 때문에 Dockerfile을 사용하여 image를 여러 번 build 하게 될 경우 layer의 변경점이 없다면 Cache 되어 이미 존재하는 layer를 재사용한다.
이 점을 활용하여 Dockerfile을 효율적으로 사용하려면 엔간하면 변하지 않는 명령어(layer)를 Dockerfile의 상단에 배치하여 cache 된 layer를 재사용하는 것이 유리하다.
여러 open source의 Dockerfile을 참고해 보면 FROM 절 거의 바로 아래에 RUN apt-get과 같은 명령어로 패키지들을 먼저 설치하고 그 이후에 필요한 작업을 하는 것을 볼 수 있는데, Dockerfile의 Cache를 활용하고 있는 것이다.
또한 layer 생성과정의 cache를 줄이고 step을 단축시키기 위해서 변경이 크지 않은 명령어(layer) 여러 개를 하나의 명령어(layer)로 압축하는 것이 좋다.
[예시 Dockerfile]
FROM mirror.gcr.io/library/alpine:3.16
LABEL maintainer="hackerpark" \
email="abc" \
version="1.0"
RUN apk --no-cache add libc6-compat curl &&\
rm -rf /var/cache/apk/*
추가로 image 크기를 줄이기 위해 패키지 설치 후 container 내부의 cache 파일을 지우면 크기를 더 줄일 수 있다. (apt, yum 모두 가능)
백엔드 프로젝트(Spring Boot) 프로젝트를 Docker로 실행시키기
1. 간단한 코드 작성
@RestController
public class AppController {
@GetMapping("/")
public String home() {
return "Hello, World!";
}
}
2. Dockerfile 작성하기
FROM openjdk:17-jdk
COPY build/libs/*SNAPSHOT.jar app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
3. Spring Boot 프로젝트 빌드하기
./gradlew clean build
4. Dockerfile을 바탕으로 이미지 빌드하기
docker build -t hello-server .
5. 이미지가 잘 생성됐는 지 확인하기
docker image ls
6. 생성한 이미지를 컨테이너로 실행시켜보기
docker run -d -p 8080:8080 hello-server
7. 컨테이너 잘 실행되고 있는 지 확인하기
docker ps
8. localhost:8080으로 들어가보기