Dockerfile 잘 쓰는 법

KYEONGMIN CHO
cloudtype
Published in
8 min readMay 22, 2023

2023년 현재, Dockerfile은 많은 개발자 분들에게 더 이상 생소한 개념이 아닙니다. 우리가 사용하고 있는 대부분의 서비스는 컨테이너 이미지를 통해 실행되고 있는데, 그 이미지를 생성하기 위한 파일이 바로 Dockerfile이기 때문이죠. 지난 번 포스트에서는 컨테이너 이미지의 개념과 도커의 기능, 그리고 레지스트리 등에 대하여 살펴 보았다면 이번에는 Dockerfile을 잘 쓸 수 있는 올바른 작성법에 대해 다뤄보도록 하겠습니다.

1. 적합한 베이스 이미지 선택

Docker Hub — Python

Dockerfile을 작성하는 첫 번째 단계는 바로 FROM 구문에 들어갈 베이스 이미지를 정하는 것 입니다. 동일한 언어 혹은 플랫폼이라 하더라도 그 베이스가 되는 OS의 종류는 굉장히 다양한데요, 여러 이미지 중 빈번하게 사용되는 Python을 기준으로 몇 가지의 OS 이미지를 살펴보겠습니다.

  • alpine: 리눅스 배포판 이미지 중 가장 작은 용량을 차지하며, Small. Simple. Secure. 이라는 슬로건에 걸맞게 가볍고 보안 측면에서 뛰어난 특징을 가지고 있습니다. 다만, Redhat 진영의 yum이나 Debian 진영의 apt와 같이 자체 패키지 매니저인 apk가 충분히 성숙하지 않았다는 점, gcc와 같은 C언어 의존성에서 에러가 발생할 수 있다는 점이 단점으로 꼽힙니다.
  • -buster, -slim-buster: buster는 Debian 배포판의 코드 네임이며 버전 10을 가리킵니다. 흔히 사용되는 Ubuntu 배포판이 바로 Debian에서 파생되었으며, apt를 패키지 매니저로 사용합니다. -slim이 붙은 이미지는 해당 이미지에서 구동하고자 하는 언어나 플랫폼에서 필요로 하는 최소한의 패키지만 설치되어 있는 것을 이릅니다.
  • -bullseye, -slim-bullseye: bullseye는 buster와 마찬가지로 Debian 리눅스의 코드 네임이고 버전 11을 가리킵니다.
  • windowsservercore: Windows Server에서 구동해야 하는 프로그램을 이미지로 빌드할 때 사용하는 베이스 이미지이며, 리눅스 베이스에 비해 사용 빈도가 적습니다.

컨테이너의 안정적인 런타임도 중요하지만 빌드되는 이미지를 최적화 하는 것도 못지 않게 중요합니다. 빌드된 이미지는 레지스트리에 저장되고 누적되는 이미지 용량은 결국 비용으로 귀결되기 때문입니다.

2. 이미지 레이어를 고려한 RUN 구문 작성

Dockerfile을 작성할 때 가장 빈번하게 볼 수 있는 것이 바로 RUN 구문입니다. 이는 쉘에서 각종 명령어를 입력하는 것에 해당하며, 일반적으로 이미지 내 OS 사용자 권한을 설정하거나 필요한 패키지를 설치하는 데에 활용됩니다. 간단한 예시는 다음과 같습니다.

# python 패키지 설치
RUN pip3 install -r requirements.txt

# Linux 사용자 추가
RUN adduser --system --uid 1000 --ingroup worker --disabled-password worker

리눅스에 익숙한 분이라면 어렵지 않게 RUN 구문을 활용하실 수 있습니다. 하지만 이미지 최적화를 위해서 주의할 점을 살펴보도록 하겠습니다. 다음 코드 예시를 주목해주세요.

# 1. 여러 명령어를 && 연산자로 연결하고 하나의 RUN 구문에 작성
RUN groupadd -g "${GID}" python \
&& useradd --create-home --no-log-init -u "${UID}" -g "${GID}" python

# 2. 명령어를 각각 RUN 구문에 작성
RUN groupadd -g "${GID}" python
RUN useradd --create-home --no-log-init -u "${UID}" -g "${GID}" python

1번과 2번 중 어떤 방식이 이미지 최적화에 적합한 작성법일까요? 정답은 1번입니다. 컨테이너 이미지는 Dockerfile의 명령 단위로 분할할 수 있으며, 하나의 분할된 단위를 레이어라고 합니다. 순차적으로 명령이 실행되어도 어플리케이션을 구성하는 논리에 이상이 없다면 이 명령을 && 연산자로 묶어 하나의 RUN 구문에 작성해 불필요한 레이어가 만들어지지 않도록 하는 것이 바람직합니다.

3. non-root 사용자로 컨테이너 실행

대부분의 컨테이너 이미지는 리눅스의 root 계정으로 어플리케이션 혹은 서비스를 실행합니다. 개발 단계에서는 복잡한 권한 설정 없이 명령어만 적절하게 입력하면 이미지를 빌드하고 실행하는 데에 문제가 발생하지 않죠. 하지만 root 권한으로 컨테이너를 실행하게 되면 불필요하게 많은 권한이 부여된 상태로 노출이 되어 보안상 취약점으로 작용할 수 있습니다. 따라서 다음과 같이 1000 이상의 GID/UID를 할당한 후 그룹과 사용자를 생성한 뒤, USER 명령을 통해 root에서 non-root 계정으로 사용자를 전환해야 합니다.

FROM python:3.9-slim-buster

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED 1

ARG UID=1000
ARG GID=1000

RUN groupadd -g "${GID}" python \
&& useradd --create-home --no-log-init -u "${UID}" -g "${GID}" python
WORKDIR /home/python

COPY --chown=python:python requirements.txt requirements.txt
RUN pip3 install -r requirements.txt

USER python:python
ENV PATH="/home/${USER}/.local/bin:${PATH}"
COPY --chown=python:python . .

ARG FLASK_ENV

ENV FLASK_ENV=${FLASK_ENV}

EXPOSE 5000

CMD ["gunicorn", "-b", "0.0.0.0:5000", "app:app"]

이미지에 따라서 node 등과 같이 GID/UID 1000에 해당하는 non-root 사용자가 사전에 생성되어 있는 것이 있으며, 이러한 경우는 USER 명령으로 사용자를 전환해주기만 하면 됩니다. 사용하고 있는 베이스 이미지에 non-root 계정이 생성되었는지 여부는 해당 이미지를 빌드할 때 사용된 Dockerfile을 통해 확인할 수 있고 일반적으로 GitHub 저장소에서 열람할 수 있습니다.

4. 컨테이너 런타임 환경의 아키텍쳐 고려

컨테이너 이미지를 빌드하고 런타임하는 기본 환경은 OS입니다. 우리가 사용하는 OS의 환경은 CPU 아키텍처에 따라 AMD64, ARM64 등으로 나뉩니다. 만약 이미지를 빌드한 환경과 실행하려는 환경이 다를 경우 어떤 일이 발생할까요? 아마 아래와 같은 경고 문구가 출력되면서 컨테이너 실행이 실패할 것입니다.

 WARNING: The requested image's platform (linux/arm64/v8) 
does not match the detected host platform (linux/amd64)
and no specific platform was requested standard_init_linux.go:228:
exec user process caused: exec format error

이전에는 대부분 사용자의 컴퓨터 CPU가 AMD64 아키텍처로 구성되었기 때문에 환경에 따른 이슈가 자주 발생하지 않았지만, ARM64 기반의 애플 자체 CPU인 M1/M2 등이 등장하면서 위와 같은 에러를 많은 사람들이 접하게 되었습니다. M1 Mac에서 빌드한 이미지를 AMD64 기반의 AWS EC2에서 런타임하려는 상황이 대표적인 예시 상황이라고 할 수 있죠.

위와 같은 이슈를 해결하기 위해 빌드 시 옵션을 부여하여 AMD64 아키텍처의 이미지가 생성되도록 하는 방법이 널리 사용되고 있습니다.

docker build -t --platform linux/amd64 ...

5. .dockerignore 파일을 통해 빌드 시 불필요한 파일 제외

.gitignore 라는 파일은 아마 많은 개발자 분들께 익숙한 파일 이름일 것입니다. git의 형상관리 대상에서 특정 파일 혹은 디렉토리를 제외시키는 역할을 하고 있죠. Docker에서 사용되는 .dockerignore은 빌드 시 특정 이미지 혹은 디렉토리가 이미지에 포함되지 않도록 하는 명세 파일입니다. 그냥 프로젝트의 모든 내용을 이미지에 넣으면 좋은거 아닌가? 라고 할 수도 있지만 그렇지 않습니다.

git과 관련된 설정이 담긴 .git이나 README 등 실제 컨테이너 런타임에 영향을 주지 않는 파일에서부터 .env와 같이 민감정보가 포함된 파일까지 확인해보면 제외해야 할 파일이 적지 않습니다. Dockerfile 역시 빌드 시에만 사용되는 파일이므로 이미지에 포함될 필요가 없습니다. 불필요하게 이미지의 용량이 커지는 것을 방지하고 보안상 취약점을 제거하기 위해서는 .dockerignore 를 적절히 활용하는 법을 익혀야 합니다.

지금까지 Dockerfile 잘 쓰는 법에 대한 내용을 다뤄보았습니다. 클라우드타입에서는 Dockerfile을 통한 어플리케이션 빌드 및 배포를 지원하고 있으며, 하단의 링크를 통해 접속하여 직접 개발 중인 프로젝트를 배포할 수 있습니다. 🙌

🔗 클라우드타입에서 Dockerfile로 배포하기

--

--