본문으로 건너뛰기
yceffort
PostsSeriesTagsAbout🧪 Research
EN

Tweaks

theme
accent palette
film grain
minimal mode
BACK TO INDEX
◆ ESSAY
--min
--year
KOoriginal

mailMail icongithubtwitter
yceffort
•
© 2026
•
https://yceffort.kr
BACK TO INDEX
◆ ESSAY

Dockerfile 작성 가이드

avatar
yceffort
2022-02-07 · 14분
14min
2022year
KOoriginal
docker

여기 에서 이어집니다.

하단 권장사항은 효율적이고 유지관리가 용이한 Dockerfile을 만드는데 도움이 되도록 제공되었다.

Table of Contents

  • FROM
  • LABEL
  • RUN
    • apt-get
    • Pipe
  • CMD
  • EXPOSE
  • ENV
  • ADD or COPY
  • ENTRYPOINT
  • VOLUME
  • USER
  • WORKDIR
  • ONBUILD

FROM

https://docs.docker.com/engine/reference/builder/#from

가능하면, 현재 제공되고 있는 공식 이미지를 사용하는 것이 좋다. 알파인 이미지는 리눅스 배포 판 중에서 크키가 매우작고 (6mb) 엄격하게 관리되고 있기 때문에 사용을 추천한다.

LABEL

https://docs.docker.com/config/labels-custom-metadata/

이미지에 레이블을 추가하여 프로젝트별 이미지 구성, 라이센스 정보 기록, 자동화 정보 등 기타 여러가지 정보를 기록할 수 있다. 각 레이블은 LABEL로 시작하고, 하나 이상의 키-값 쌍으로 추가하면 된다.

공백이 있는 문자열은 따옴표로 묶거나 공백을 이스케이프 해야 한다. ('' 도 마찬가지다.)

# Set one or more individual labels
LABEL com.example.version="0.0.1-beta"
LABEL vendor1="ACME Incorporated"
LABEL vendor2=ZENITH\ Incorporated
LABEL com.example.release-date="2015-02-12"
LABEL com.example.version.is-production=""

모든 이미지는 레이블을 하나 이상 가지고 있을 수 있다. Docker 1.10 이전 버전에서는, 추가적인 레이어가 생성되지 않도록 여러 레이블을 하나의 LABEL로 묶는 것이 권장되었다. 이제 더이상 필요하진 않지만 여전히 여러개를 결합하는 방식은 가능하다.

# Set multiple labels on one line
LABEL com.example.version="0.0.1-beta" com.example.release-date="2015-02-12"
# Set multiple labels at once, using line-continuation characters to break long lines
LABEL vendor=ACME\ Incorporated \
      com.example.is-beta= \
      com.example.is-production="" \
      com.example.version="0.0.1-beta" \
      com.example.release-date="2015-02-12"
  • https://docs.docker.com/config/labels-custom-metadata/
  • https://docs.docker.com/engine/reference/builder/#label

RUN

https://docs.docker.com/engine/reference/builder/#run

길거나 복잡한 RUN 구문은 백슬래시를 활용하여 여러줄로 분할하는 것이 Dockerfile 관리에 좋다.

apt-get

RUN에서 아마 가장 자주 사용되는 명령어는 apt-get일 것이다. RUN apt-get은 패키지를 설치하는 명령어이기 때문에 몇가지를 고려 해야 한다.

RUN apt-get update 와 apt-get install은 항상 같은 RUN구문 안에 있어야 한다.

RUN apt-get update && apt-get install -y \
    package-bar \
    package-baz \
    package-foo  \
    && rm -rf /var/lib/apt/lists/*

apt-get update를 RUN구문에서 단독으로 쓰면 캐시 문제가 있을 수 있고, 이어지는 apt-get install가 실패할 가능성도 있다. 예를 들면

# syntax=docker/dockerfile:1
FROM ubuntu:18.04
RUN apt-get update
RUN apt-get install -y curl

이미지가 빌드된 이후에, 모든 레이어가 도커 캐시안에 들어가게 된다. apt-get install 뒤에 구문을 추가했다고 가정해보자.

# syntax=docker/dockerfile:1
FROM ubuntu:18.04
RUN apt-get update
RUN apt-get install -y curl nginx

도커는 이전 명령어와 수정된 명령어가 동일 할 때에만 이전단계의 캐시를 사용한다. 따라서 빌드가 캐신된 버전을 사용하기 때문에 apt-get update가 실행되지 않는다. 그러므로 이 빌드는 잠재적으로 오래된 버전의 curl과 nginx 패키지를 얻게되는 결과를 초래할 수 있다.

RUN apt-get update && apt-get install -y 를 수행하면, 도커파일이 더이상의 코딩이나 수동작업 없이 최신 패키지 버전을 설치할 수 있다. 이 기술을 cache busting이라고 한다. 패키지 버전을 지정하여 이 캐시버스팅을 수행할 수도 있다.

RUN apt-get update && apt-get install -y \
    package-bar \
    package-baz \
    package-foo=1.3.*

버전을 이런식으로 고정하면 캐시에 무엇이 있든 상관없이 특정 버전을 검색하도록 강제할 수 있다. 또한 이 기술을 사용하면 예기치 않은 필수 패키지의 버저닝으로 인한 장애를 줄일 수 있다.

아래는 모든 적절한 권장사항을 잘 수행한 예시다.

RUN apt-get update && apt-get install -y \
    aufs-tools \
    automake \
    build-essential \
    curl \
    dpkg-sig \
    libcap-dev \
    libsqlite3-dev \
    mercurial \
    reprepro \
    ruby1.9.1 \
    ruby1.9.1-dev \
    s3cmd=1.1.* \
 && rm -rf /var/lib/apt/lists/*

s3cmd는 1.1.* 버전을 사용하도록 했다. 이미지가 만약 이전 버전을 사용하고, 새로운 버전을 지정하면 apt-get update에 캐시 버스팅을 발생시키고 새로운 버전을 설치한다. 각 라인에 패키지를 나열하면 패키지가 중복되는 오류도 방지할 수 있다.

또한 /var/lib/apt/lists를 제거하여 캐시를 적절히 정리하면 캐시가 레이어에 저장되지 않기 때문에 이미지 용량을 줄일 수 있다. RUN 구문은 apt-get update와 함께 시작하므로, 패키지 캐시는 항상 apt-get install 전에 정리될 것이다.

Debian Ubuntu에서는 자동으로 apt-get clean을 수행해주므로 이럴 필요가 없다.

Pipe

몇 몇 RUN 커맨드는 |에 의존하여 동작할 수 있다. 예를 들어

RUN wget -O - https://some.site | wc -l > /number

도커는 /bin/sh -c 커맨드를 사용하여, 이러한 명령어를 실행한다. 이 명령어는 마지막 작업의 종료코드만 확인하여 성공 실패 여부를 결정한다. 위의 예제에서 살펴보면, 이 빌드 단계는 wget 명령어가 실패하더라도, wc -l 명령어가 성공하면 새로운 이미지를 만들어 낼 것이다.

파이프의 어느 단계에서든 오류로 인해 명령이 실패하도록 하려면, set -o pipefail &&를 앞에 추가하면 된다.

RUN set -o pipefail && wget -O - https://some.site | wc -l > /number

모든 쉘이 -o pipefail을 제공하는 것은 아니므로, 아래와 같이 별도로 나눠서 실행해야 할 수도 있다.

RUN ["/bin/bash", "-c", "set -o pipefail && wget -O - https://some.site | wc -l > /number"]

CMD

https://docs.docker.com/engine/reference/builder/#cmd

CMD 는 나열되어 있는 인수와 함께, 이미지에 포함되어 있는 소프트웨어를 실행하는데 사용된다. CMD는 거의 대부분 항상 ["실행 파일", "param1", "param2"...] 와 같은 형태로 사용되어야 한다.

대부분의 경우, CMD는 bash, paython, perl과 같은 대화형 셸이 필요하다. 예를들어 CMD ["perl", "-de0"], CMD ["python"], or CMD ["php", "-a"] 등이 있다. 이러한 형태를 사용하면, docker run -it python고과 같은 것을 실행하면 바로 셸로 진입할 수 있다.

EXPOSE

https://docs.docker.com/engine/reference/builder/#expose

EXPOSE는 컨테이너가 연결을 받는 포트를 나타낸다. 따라서 애플리케이션에서 공통으로 사용되는 기존 포트를 사용해야 한다. (아파치 EXPOSE 80, 몽고 디비 EXPOSE 27017과 같이)

외부에서 접근을 위해 docker run에 플래그를 사용하여 이 포트가 어떤 포트에 연결될지 지정할 수 있다.

ENV

https://docs.docker.com/engine/reference/builder/#env

ENV를 사용하여 컨테이너가 설치하는 소프트웨어의 PATH 환경변수를 업데이트 할 수 있다. 예를 들어, ENV PATH=/usr/local/nginx/bin:$PATH는 CMD ["nginx"] 명령어가 실행될 수 있도록 해준다.

또한 Postgres의 PGDATA와 같이 컨테이너에 포함하려는 서비스와 관련된 필수 환경변수를 제공하는데 유용하다.

마지막으로, ENV는 일반적으로 사용되는 버전 번호를 설정하기 위해 사용할 수도 있다.

ENV PG_MAJOR=9.3
ENV PG_VERSION=9.3.4
RUN curl -SL https://example.com/postgres-$PG_VERSION.tar.xz | tar -xJC /usr/src/postgres && …
ENV PATH=/usr/local/postgres-$PG_MAJOR/bin:$PATH

프로그램에 상수값이 있는 것과 비슷하게, ENV를 사용하면 자동적으로 컨테이너 내부의 소프트웨어 버전을 지정하는 것도 가능하다.

각 ENV 라인은 RUN과 동일하게 새로운 중간 레이어를 생성한다. 즉, 이후 레이어에서 환경변수를 설정해제하더라도, 이 레이어에서 계속 유지되며 해당 값이 덤프될 수 있다.

# syntax=docker/dockerfile:1
FROM alpine
ENV ADMIN_USER="mark"
RUN echo $ADMIN_USER > ./mark
RUN unset ADMIN_USER
$ docker run --rm test sh -c 'echo $ADMIN_USER'

mark

이러한 사태를 방지하고, 환경 변수를 실제로 해지 하기 위해서는 RUN을 사용하여 변수를 단일 레이어에서 설정, 사용, 해제를 하면 된다. 이 명령어는 ; &&을 사용하여 구분할 수 있다. 후자를 사용한다면, 명령이 실패한다면 도커 빌드도 실패한다.

# syntax=docker/dockerfile:1
FROM alpine
RUN export ADMIN_USER="mark" \
    && echo $ADMIN_USER > ./mark \
    && unset ADMIN_USER
CMD sh

ADD or COPY

  • https://docs.docker.com/engine/reference/builder/#add
  • https://docs.docker.com/engine/reference/builder/#copy

ADD COPY 두 명령어가 기능적으로 거의 유사하지만, COPY가 일반적으로 더 사용된다. 그 이유는 ADD 보다 더 순수하기 때문이다. COPY는 단순히 컨테이너에 있는 로컬 파일을 복사할 뿐이다. 반면 ADD는 몇가지 추가적이 있다. (로컬 전용 tar 파일 해제, 원격 URL 지원 등) 따라서 ADD는 ADD rootfs.tar.xz / 와 같은 상황에서 사용하는 것이 좋다.

컨텍스트에서 다른 파일을 사용하는 여러 단계가 Dockerfile 내부에 있을 경우, 한번에 하지말고 개별적으로 복사하는 것이 좋다. 이렇게 하면 각 단계의 빌드 캐시는 필요한 파일이 변경되었을 경우에만 무효화 (재실행)된다.

예를 들어

COPY requirements.txt /tmp/
RUN pip install --requirement /tmp/requirements.txt
COPY . /tmp/

COPY . /tmp/ 를 앞에 두는 경우보다 캐시 무효화가 더 줄어든다.

이미지 크기는 중요한 문제이므로, 원격 URL에서 패키지를 가져올때는 ADD를 사용하는 것은 권장되지 않는다. 대신 curl wget을 사용해야 한다. 이렇게 하면 파일 압축을 해제한 후 더이상 필요 없는 파일을 삭제할 수 있으며, 이미지에 다른 레이어를 추가할 필요가 없다. 예를 들어, 아래와 같은 경우는 피해야 한다.

ADD https://example.com/big.tar.xz /usr/src/things/
RUN tar -xJf /usr/src/things/big.tar.xz -C /usr/src/things
RUN make -C /usr/src/things all

이 대신,

RUN mkdir -p /usr/src/things \
    && curl -SL https://example.com/big.tar.xz \
    | tar -xJC /usr/src/things \
    && make -C /usr/src/things all

자동 tar 파일 압축 해제 기능 등이 필요하지 않은 다른 항목 (파일, 디렉토리) 에는 COPY를 쓰자.

ENTRYPOINT

https://docs.docker.com/engine/reference/builder/#entrypoint

ENTRYPOINT를 쓰는 가장 좋은 방법은 이미지의 메인 커맨드를 설정해두어, 해당 명령어를 기본으로 사용할 수 있게 하는 것이다.

ENTRYPOINT ["s3cmd"]
CMD ["--help"]

이렇게 하면 아래와 같이 실행했을 때 도움말을 볼 수 있다.

docker run s3cmd

혹은 파라미터를 바로 두어서 바로 커맨드를 실행할 수도 있다.

docker run s3cmd ls s3://mybucket

ENTRYPOINT 명령은 helper 스크립트와 함께 사용할 수 있으므로, 특정 tool 을 시작할때 위의 명령어와 유사한 방식으로 동작할 수도 있다.

예를 들어, Postgres 공식 이미지는 다음 스크립트를 ENTRYPOINT로 사용한다.

#!/bin/bash
set -e

if [ "$1" = 'postgres' ]; then
    chown -R postgres "$PGDATA"

    if [ -z "$(ls -A "$PGDATA")" ]; then
        gosu postgres initdb
    fi

    exec gosu postgres "$@"
fi

exec "$@"

helper 스크립트는 컨테이너에 복사되고, 컨테이너 시작시 ENTRYPOINT에 의해 실행된다.

COPY
Learn more about the "COPY" Dockerfile command.
 ./docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["postgres"]

이 스크립트는 유저가 Postgres를 다양한 방식으로 상호작용할 수 있도록 해준다.

단순히 Postgres를 실행할수도 있고

 docker run postgres

서버에 파라미터를 전달하여 실행할 수도있고

 docker run postgres postgres --help

또한 Bash와 같은 완전히 다른 툴에서도 실행할 수 있다.

 docker run --rm -it postgres bash

VOLUME

https://docs.docker.com/engine/reference/builder/#volume

VOLUME은 도커 컨테이너에서 만든 데이터 저장소 영역, 설정 저장소, 또는 파일이나 폴더를 노출하는데 사용해야 한다. 이미지의 변경 가능한 부분 및 사용자가 수정가능한 부분에는 VOLUME을 사용하는 것이 좋다.

USER

https://docs.docker.com/engine/reference/builder/#user

서비스를 실행하는데 별도로 권한이 필요 없다면, USER 를 사용하여 루트가 아닌 사용자로 변경해야 한다. RUN groupadd -r postgres && useradd --no-log-init -r -g postgres postgres와 같은 명령어로 유저나 그룹을 생성할 수 있다.

sudo는 문제를 일으킬 여지가 있으므로 사용하지 않는 것이 좋다. 그럼에도 sudo가 어쩔 수 없이 필요한 경우 gosu의 사용을 고려해보자.

마지막으로, 레이어와 복잡성을 줄이기 위해서는 너무 자주 USER를 사용하지 않는 것이 좋다.

WORKDIR

https://docs.docker.com/engine/reference/builder/#workdir

명확성, 그리고 신뢰성을 위해 WORKDIR은 항상 절대 경로를 사용해야 한다. 읽기 어렵고, 유지보수도 어려운 RUN cd … && do-something 대신 WORKDIR을 사용하자.

ONBUILD

https://docs.docker.com/engine/reference/builder/#onbuild

ONBUILD 명령은 현재 Dockerfile의 빌드가 완료된 후 실행된다. ONBUILD는 현재 이미지에서 파생된 하위 이미지에서 실행된다. ONBUILD 명령은 상위 Dockerfile이 하위 Dockerfile에 제공하는 명령이라고 보면 된다.

도커는 하위 Dockerfile의 명령에 앞서 ONBUILD를 수행한다.

ONBUILD는 지정된 이미지에서 빌드할 이미지가 필요할 때 유용하다. 예를 들어, Dockerfile내에서 해당 언어로 소프트웨어를 필요로 하는 이미지가 있다면, ONBUILD 명령어가 유용하다.

ONBUILD로 빌드된 이미지에는 별도 태그가 있어야 한다. (ruby:1.9-onbuild ruby:2.0-onbuild)

ONBUILD에 ADD COPY를 넣을 때 주의하자. 새 빌드 컨텍스트에 이렇게 추가되는 리소스가 없을 경우 하위 "onbuild" 이미지가 실패할 것이다. 위에서 권장한대로 태그를 추가해서 구별하면, Dockerfile 작성자가 이를 선택할 수 있으므로 이러한 문제를 예방할 수 있다.

관련 글

  • ◆ 프론트엔드 개발자가 알아야 할 쿠버네티스
    #kubernetes#docker#nextjs

    내 Next.js 앱은 어떻게 파드가 되는가: 컨테이너와 파드를 직접 열어본 기록

    같은 Next.js 앱인데 이미지 하나는 1.72GB, 하나는 208MB였다. 사라진 1.5GB를 레이어에서 역추적하고, 컨테이너가 격리된 프로세스라는 것을 PID와 cgroup 파일로 직접 확인한다. 프론트엔드 개발자를 위한 쿠버네티스 시리즈의 두 번째 편이다.

    2026-08-05·34분
  • #docker#devops

    더 나은 Dockerfile 작성을 위한 best practice - 2022년 버전

    갑자기 docker를 파는 이유는

    2022-02-05·13분
  • #kubernetes#docker

    K8s 공부 (3)

    무지성에서 시작하는 K8s 공부해보기 시리즈(3)

    2021-06-12·10분
  • #kubernetes#devops#docker

    K8s 공부 (1)

    무지성에서 시작하는 K8s 공부해보기 시리즈(1)

    2021-06-09·10분

새 글을 놓치고 싶지 않으시다면 RSS로 구독해 주세요.

RSS 구독 →

yceffort — 프론트엔드 엔지니어입니다. 발표·기술 자문·기고 문의는 이곳에서 받고 있습니다.

← Back to the blogIssue on GitHub →