# 우분투 22.04 버전 이미지 사용
FROM ubuntu:22.04
# jdk 설치
RUN apt-get update && apt-get install -y openjdk-17-jdk
# jar 파일을 컨테이너로 이동
COPY vanilla-0.0.1-SNAPSHOT.jar /
# 실행된 컨테이너에서 jar파일 동작
CMD java -jar vanilla-0.0.1-SNAPSHOT.jar
앞선 포스팅에서 작성했던 Dockerfile은 하나의 FROM 지침만을 이용했다.
Multi-Stage 빌드에서는 한 Dockerfile에 여러개의 FROM 지침을 이용한다.
Multi-Stage 빌드를 이용하면 한 Dockerfile 내에서 단계를 나누고 앞선 단계의 결과를 다음 단계에서 이용할 수 있다.
Multi-Stage를 이용하면 실제 생성되는 이미지의 크기를 줄이는데 도움이 된다.
중간 Stage에서 필요한 부분만 가져오기 때문에 해당 Stage에서 불필요한 부분만큼의 크기가 절약된다.
# syntax=docker/dockerfile:1
FROM golang:1.23
WORKDIR /src
COPY <<EOF ./main.go
package main
import "fmt"
func main() {
fmt.Println("hello, world")
}
EOF
RUN go build -o /bin/hello ./main.go
FROM scratch
COPY --from=0 /bin/hello /bin/hello
CMD ["/bin/hello"]
이해를 위해 Dockerfile 예시를 살펴보자. 위 Dockerfile은 총 2개의 단계로 나뉘어진다.
처음 golang:1.23을 이용한 단계에서는 main.go일 빌드하여 /bin/hello에 저장한다.
이후, scratch 이미지를 사용하는 단계에서는 --from=0 을 통해 앞선 단계에서 생성한 /bin/hello를 현재 단계의 /bin/hello로 복사한다.
최종적으로는 CMD 지침을 통해 내용을 출력한다. 실제 빌드이후, 생성되는 컨테이너를 확인해보자.

빌드 명령의 동작을 확인해보면 [stage-0 ...], [stage-1 ...] 와 같이 단계를 구분해서 빌드되는 것을 확인할 수 있다.

빌드를 통해 생성된 이미지를 이용하여 컨테이너를 생성해보면, 위 처럼 실행되는 것을 확인할 수 있다.
위 예시에선
--from=0의 형태로 단계의 순서를 이용해 내용을 참조했다.
FROM지침에서AS를 이용하면 각 단계의 별칭을 지정할 수 있다.# syntax=docker/dockerfile:1 FROM golang:1.23 AS build # build 라는 이름 지정 WORKDIR /src COPY <<EOF /src/main.go package main import "fmt" func main() { fmt.Println("hello, world") } EOF RUN go build -o /bin/hello ./main.go FROM scratch COPY --from=build /bin/hello /bin/hello # 앞서 지정한 이름인 build 사용 CMD ["/bin/hello"]
여러개의 단계가 존재하는 Dockerfile 에서, 특정 단계까지만 빌드를 진행하고 싶다면 아래와 같이 빌드명령을 작성하면 된다.
docker build --target <stage name> -t <tag name> .
이렇게 특정 단계까지만 빌드가 진행되게 함으로써, 빌드 과정에 대한 디버깅 등의 상황에 사용할 수 있다.
BuildKit에서는 타겟 Stage가 의존하고있는 Stage들만 빌드된다.
# syntax=docker/dockerfile:1 FROM ubuntu AS base RUN echo "base" FROM base AS stage1 RUN echo "stage1" FROM base AS stage2 RUN echo "stage2"예를들어, 위 Dockerfile에서
stage2를 타겟으로 지정한다면stage1은 빌드되지 않는다.
COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf
COPY 지침과 --from 태그를 이용하면 외부의 이미지를 Stage로 활용할 수 있다.
로컬이미지나 로컬 혹은 DockerHub 같은 레지스트리에 있는 태그명과 태그ID를 활용하여 이미지를 복사해올 수 있다.
# syntax=docker/dockerfile:1
FROM alpine:latest AS builder
RUN apk --no-cache add build-base
FROM builder AS build1
COPY source1.cpp source.cpp
RUN g++ -o /binary source.cpp
FROM builder AS build2
COPY source2.cpp source.cpp
RUN g++ -o /binary source.cpp
위 예시에서 builder Stage를 이후의 Stage에서 이미지로 활용하는 것처럼,
앞선 Stage를 새로운 Stage에서 활용할 수 있다.
빌드 프로세스에 정보를 전달하기 위한 수단으로 ARG, ENV를 활용할 수 있다.
두 값은 모두 Dockerfile에 선언되어 있으며, docker build 명령에서 플래그를 통해 설정할 수 있다.
주의사항으로
ARG와ENV는 최종적으로 생성되는 이미지에 값이 노출된다.
따라서, 민감한 정보를 작성해서는 안된다.
ARG은 Dockerfile 자체의 변수를 의미한다.
설치할 종속성의 버전을 지정하는 등의 용도로 사용되는데, 이는 명령어에서 사용되지 않는한 빌드에 영향을 끼치지 않는다.
또한, 특정한 설정을 통해 전달되지 않는한 인스턴스화 된 컨테이너에 접근할 수 없거나 존재하지 않는다.
# syntax=docker/dockerfile:1
ARG NODE_VERSION="20"
ARG ALPINE_VERSION="3.20"
FROM node:${NODE_VERSION}-alpine${ALPINE_VERSION} AS base
WORKDIR /src
FROM base AS build
COPY package*.json ./
RUN npm ci
RUN npm run build
FROM base AS production
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=build /src/dist/ .
CMD ["node", "app.js"]
이런식으로 Dockerfile 내에서 ${NODE_VERSION} 이나 ${ALPINE_VERSION}과 같이 특정 버전을 빌드명령에서 입력받아야 하는 경우 사용될 수 있다.
docker build --build-arg NODE_VERSION=current .
이 예시에선 Dockerfile 내부에 기본값이 존재하지만, 빌드 명령에서 특정 값을 입력하면 재정의가 가능하다.
Stage 밖, 즉 글로벌 범위에 설정된
ARG값은 빌드단계에 영향을 주지 못한다.# syntax=docker/dockerfile:1 # The following build argument is declared in the global scope: ARG NAME="joe" FROM alpine # The following instruction doesn't have access to the $NAME build argument # because the argument was defined in the global scope, not for this stage. RUN echo "hello ${NAME}!"즉, 위 Dockerfile 을 빌드한 이미지를 실행하면
hello !가 출력된다.
글로벌에 선언된ARG값을 빌드단계에서 사용하고 싶다면, 아래처럼 Stage내에 한번 더 작성해야한다.# syntax=docker/dockerfile:1 # Declare the build argument in the global scope ARG NAME="joe" FROM alpine # Consume the build argument in the build stage ARG NAME RUN echo $NAME
ENV는 인스턴스화된 컨테이너에서도 유지되는 값이다.
주로, 빌드를 위한 실행 환경 구성이나 컨테이너를 위한 기본 환경변수 값을 세팅하는데 사용된다.
ENV 값은 빌드 명령에서 재정의하는 것이 불가능하며, Dockerfile 내에서 작성되어야한다.
# syntax=docker/dockerfile:1
FROM node:20
WORKDIR /app
COPY package*.json ./
ENV NODE_ENV=production
RUN npm ci && npm cache clean --force
COPY . .
CMD ["node", "app.js"]
이런식으로 Dockerfile 내에 ENV 값을 세팅하면, 이미지를 거쳐 컨테이너로 인스턴스화 된 이후에도 유지된다.
만약,
ENV값을 빌드명령에서 재정의하고 싶다면ARG와 함께 사용하는 방법이 있다.# syntax=docker/dockerfile:1 FROM node:20 ARG NODE_ENV=production ENV NODE_ENV=$NODE_ENV WORKDIR /app COPY package*.json ./ RUN npm ci && npm cache clean --force COPY . . CMD ["node", "app.js"]
ARG와 동일하게 글로벌 범위에 설정된ENV도 빌드단계에 영향을 주지 못한다.즉, 로컬에 설정된
ENV는 인스턴스화 된 컨테이너까지 유지되지만, 글로벌에 설정된ENV는 유지되지 못한다.
docker build --build-arg HTTP_PROXY=https://my-proxy.example.com .
프록시 빌드 인수를 사용하기 위해서는 빌드 명령에 --build-arg로 해당되는 인수를 작성하기만 하면 된다.( Dockerfile 내부에는 작성이 필요하지 않다 )
관련된 인수로는 HTTP_PROXY, HTTPS_PROXY, FTP_PROXY, NO_PROXY, ALL_PROXY가 있다.
# syntax=docker/dockerfile:1
# Pre-defined build arguments are available in the global scope
FROM --platform=$BUILDPLATFORM golang
# To inherit them to a stage, declare them with ARG
ARG TARGETOS
RUN GOOS=$TARGETOS go build -o ./exe .
멀티 플랫폼 빌드는 하나의 Dockerfile로 여러 OS나 아키텍처에서 실행가능한 이미지를 생성하는 기능을 뜻한다.
( 멀티 플랫폼과 관련해서는 다음 포스팅에서 자세히 알아볼 예정이다 )
빌드 플랫폼, 즉, 빌드 프로세스가 진행되는 환경에 대한 인수로는
BUILDPLATFORM, BUILDOS, BUILDARCH, BUILDVARIANT 가 있다.
타겟 플랫폼, 즉, 빌드된 이미지가 실행될 환경에 대한 인수로는
TARGETPLATFORM, TARGETOS, TARGETARCH,TARGETVARIANT 가 있다.
빌드 플랫폼 : Docker를 활용해 빌드명령을 수행하는 환경
타겟 플랫폼 : Docker를 활용해 이미지를 컨테이너로 인스턴스화 하려는 환경
BuildX와 BuildKit의 동작과 관련된 값들은 공식문서를 참고하자