[Docker] Dockerfile이 뭔데? 도커 이미지를 만들기 위한 설계도

이전 포스팅들에서는 Docker Hub에 올라온 이미지를 받아서, 실행해 보는 방식으로 Docker를 다뤘어요. Ubuntu, Nginx, MySQL 등 남이 만들어둔 환경 위에서 내 코드를 올려보는 실습을 했죠.

 

이제부터는 남이 만든 이미지가 아니라, 내가 만든 코드를 위한 나만의 이미지를 만들 차례예요. 이때 필요한 게 바로 Dockerfile입니다.


왜 Dockerfile이 필요한가?

Dockerfile은 도커 이미지를 만드는 설계도 파일이에요. 어떤 OS에서, 어떤 패키지를 설치하고, 어떤 파일을 복사하고, 어떤 명령어로 실행할지 등을 단계별로 이 한 파일에 모두 정의할 수 있죠. Docker는 이 Dockerfile을 읽고 한 단계씩 따라가면서 이미지를 빌드해 줘요.

실제 개발·배포 환경에서는 거의 대부분 Dockerfile을 직접 작성하게 돼요. CI/CD 파이프라인에서도 이 Dockerfile을 기준으로 빌드·배포가 자동화되죠.

Dockerfile 도입 목적과 장점

  • 환경 일관성 보장: 개발 환경, 운영 환경 모두 동일하게 구성 가능
  • 자동화: CI/CD 파이프라인과 연동하여 배포 자동화가 가능
  • 최적화 가능: Layer 캐싱 및 빌드 최적화로 속도 향상
  • 버전 관리: Git으로 관리 가능하여 변경 추적 및 롤백이 수월

 

Dockerfile 기본 구조

FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
EXPOSE 3000
CMD ["node", "index.js"]

 

Dockerfile 명령어 정리

1. FROM

  • 어떤 이미지를 베이스로 사용할지 정해요.
  • 대부분 ubuntunodepython 같은 공식 이미지를 가져와요
  • 반드시 가장 첫 줄 작성되어야 해요.
FROM ubuntu:22.04

FROM scratch

💡scratch는 아무것도 없는 빈 이미지로, 최적화된 최소 이미지 제작에 유용해요. (Golang, Rust 등 정적 바이너리에서 사용)

 

2. LABEL

  • 이미지에 메타데이터(정보) 추가
LABEL maintainer="MINTP" \
      description="MINTP" \
      version="1.0"

 

3. WORKDIR

  • 명령어가 실행될 기본 디렉터리를 지정해요.
WORKDIR /app

💡지정한 경로가 없으면 자동으로 만들어줘요.

 

4. COPY & ADD

  • 로컬 파일을 이미지 안으로 복사해요.
COPY . .
  • ADDCOPY와 거의 비슷하지만, URL 다운로드나 압축 해제 기능이 있어요.
ADD https://example.com/app.tar.gz /app/
⚠️ ADD 지양하기
ADD는 압축 해제나 원격 URL 다운로드 같은 기능도 포함돼 있어서, 상황에 따라 동작이 예측하기 어렵거나 의도치 않게 보안 취약점을 만들 수 있어요. 그래서 특별한 목적이 있는 경우가 아니라면 COPY를 사용하는 게 더 안전해요. 특별한 경우 아니면 COPY를 써요.

 

💡ADD 없이 URL 다운로드 & 압축 해제하기
1. URL 다운로드는 curl 또는 wget 사용
RUN apt-get update && apt-get install -y curl \
  && curl -L https://intpdev.com/app.tar.gz -o /tmp/app.tar.gz​

2. 압축 해제는 tar 사용

RUN tar -xzf /tmp/app.tar.gz -C /app && rm /tmp/app.tar.gz​

👉 ADD 없이도 같은 기능을 명확하게 제어하면서 구현할 수 있어요.
⚠️ curl로 다운로드하는 결과물이 매번 다르거나 변경될 가능성이 있는 경우, Docker의 빌드 캐시가 무효화될 수 있어요.

 

5. RUN

  • 이미지를 만들 때 실행되는 셸 명령어예요.
RUN apt update
RUN apt install -y git
RUN apt clean -y
RUN apt autoremove -y
RUN rm -rfv /tmp/* /var/lib/apt/lists/* /var/tmp/*

# 권장 사항
RUN apt update && \
	apt install -y git \
            curl && \
	apt clean -y && \
	apt autoremove -y && \
	rm -rfv /tmp/* /var/lib/apt/lists/* /var/tmp/*

💡여러 RUN 명령어는 &&로 묶어서 한 줄에 쓰면 이미지 레이어 수를 줄일 수 있어요.

 

6. CMD

  • 컨테이너가 실행될 때 기본으로 실행되는 명령어예요.
  • Dockerfile에서 한 번만 쓸 수 있어요. 여러 개의 CMD를 작성해도 마지막 명령만 처리됩니다.
CMD ["node", "index.js"]

💡docker run 명령에서 실행 명령을 따로 넘기면 Dockerfile에 정의된 CMD는 무시돼요.

FROM ubuntu
CMD ["echo", "hello from CMD"]
docker run my-image                        # echo hello from CMD
docker run my-image "hello again"   # echo hello again

👉 CMD는 완전히 덮어쓰기 됨

ENTRYPOINT ["/entrypoint.sh"]
CMD ["--mode=dev"]

👉 docker run my-image --mode=prod도 유연하게 가능

 

7. ENTRYPOINT

  • CMD랑 비슷하지만, "고정된 실행 파일"을 지정할 때 사용해요.
FROM ubuntu
ENTRYPOINT ["echo", "hello from ENTRYPOINT"]
docker run my-image                  # echo hello from ENTRYPOINT
docker run my-image "again"         # echo hello from ENTRYPOINT again

👉 ENTRYPOINT는 고정되고, 인자만 추가됨

ENTRYPOINT ["/entrypoint.sh"]
CMD ["--dev"]

→ 실제 실행 명령: /entrypoint.sh --dev

📌 ENTRYPOINT는 "컨테이너가 항상 실행해야 할 주 명령어"를 고정하는데 쓰고, CMD는 그 명령어에 전달할 기본 인자를 지정하는 데 사용한 형식입니다. CMD는 쉽게 덮어쓸 수 있기 때문에, 고정된 실행 파일이 있을 땐 ENTRYPOINT, 유동적인 인자가 있을 때 CMD를 함께 쓰는 방식이 유용해요 (고정 실행 파일유동 파라미터)

 

8. ENV

  • 환경 변수를 설정해요.
  • 형식은 일반적으로 ENV 키=값이지만, ENV 키 값 형태도 사용할 수 있어요.

ENV는 두 가지 목적에서 사용돼요:

  1. 애플리케이션 실행 시 필요한 환경 변수 미리 정의
  2. Dockerfile 내에서 반복되는 값을 재사용 (예: ENV APP_HOME /app) 이후 여러 RUN, WORKDIR, COPY, CMD 명령에서 $APP_HOME을 사용해 반복을 피할 수 있습니다.
ENV MYSQL_ROOT_PASSWORD=mypassword
ENV MYSQL_DATABASE mydb
ENV APP_HOME=/app
WORKDIR $APP_HOME
COPY . $APP_HOME
RUN mkdir -p $APP_HOME/logs && chmod 755 $APP_HOME/logs

 

9. EXPOSE

  • 컨테이너가 외부에 열 포트를 알려줘요 (자동으로 열리지 않아요).
  • 이미지 내에 애플리케이션이 사용하는 포트를 사전에 확인하고 호스트와 연결되도록 구성하는 경우 설정하고, docker run 사용 시 포트를 연결해 주어야(-p) 합니다.
EXPOSE 3000

 

10. ARG

  • 빌드(docker build)할 때 넘겨주는 변수예요. 전달하기 위해 --build-arg=인자를 정의하여 사용해야 해요.
  • ENV랑 다르게 컨테이너 안에는 남지 않아요.
ARG VERSION
ENV VERSION=$VERSION

빌드할 때:

docker build --build-arg VERSION=1.0 .

 

11. USER

  • 컨테이너에서 명령어를 실행할 사용자를 지정해요.
  • 컨테이너 기본 사용자는 root예요. 보안을 위해 사용자 계정을 지정하는 게 좋아요.
RUN adduser -D appuser
USER appuser

 

12. VOLUME

  • 컨테이너와 호스트가 공유하는 볼륨 디렉터리를 설정합니다. VOLUME으로 지정된 경로는 /var/lib/docker와 자동으로 연결됩니다.
VOLUME ["/data"]

컨테이너 내부의 /data와 연결됩니다.

 

 

Dockerfile 최적화하기

✅ 캐시를 잘 활용하기

Docker는 Dockerfile의 각 명령어를 실행할 때마다 중간 결과를 레이어(layer)로 저장하고, 이후 빌드에서 같은 명령이 있다면 이전 레이어를 재사용해요. 이걸 '빌드 캐시'라고 해요.

# 덜 바뀌는 패키지 설치 → 캐시 재사용
COPY package*.json ./     # 의존성 파일만 먼저 복사
RUN npm install           # 의존성 설치

# 자주 바뀌는 소스 코드는 마지막에 COPY
COPY . .                  # 전체 소스 복사

소스코드는 자주 바뀌지만 package.json은 잘 안 바뀌니까, 먼저 COPY 하면 npm install은 캐시로 재사용 가능해요.

  RUN 명령 최소화 (Layer 줄이기)

RUN 명령마다 새로운 레이어가 생겨 이미지 크기가 증가합니다. &&를 활용해 여러 명령을 하나의 RUN으로 묶어야 해요.

# 나쁜 예
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get clean

# 좋은 예
RUN apt-get update && apt-get install -y curl \
  && apt-get clean
  • 여러 RUN을 하나로 묶으면 불필요한 레이어가 줄어들고 불필요한 이미지 용량도 줄어들어요.
  • 도커 이미지는 레이어 단위로 캐시 되기 때문에, apt-get updateinstall의 중간 결과가 남고 clean은 나중에 실행되기 때문에 이전 레이어는 그대로 유지돼요.

💡 불필요한 레이어 병합

마찬가지로 COPY, ENV, RUN 등이 너무 쪼개져있으며 레이어가 많아져요. 비슷한 작업을 묶어서 처리하는 것이 좋습니다.

# 나쁜 예
ENV A=1
ENV B=2

# 좋은 예
ENV A=1 B=2

✅ 불필요한 바이너리와 캐시 제거

임시 파일이나 패키지 캐시가 남으면 이미지가 부풀려지기 때문에, 빌드 마지막에 임시 파일과 패키지 캐시를 삭제하는 것은 이미지 경령화에 효과적입니다.

RUN apt-get update \
 && apt-get install -y --no-install-recommends curl unzip openjdk-17-jdk \
 && apt-get clean autoclean \
 && apt-get autoremove -y \
 && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
  • apt-get autoremove, clean, rm -rf 명령으로 캐시와 임시 파일을 제거하면 경량화에 도움이 됩니다.
  • --no-install-recommends 옵션은 의존성으로 추천된 추가 패키지들을 설치하지 않도록 해서 이미지 크기를 줄여줍니다.

.dockerignore 사용

Git, node_modules, 테스트 파일까지 이미지에 포함될 수 있어요. 불필요한 파일은 빌드 시 제외해야 해요.

.git
node_modules
tests/
Dockerfile~
README.md

✅ Multi-stage Build

빌드 도구/의존성으로 인해 이미지가 크게 증가할 수 있어요. 때문에 빌드를 먼저 하고, 결과물에 복사하는 방식을 사용할 수 있어요.

# Step 1: 빌드 전용
FROM node:18 AS builder
WORKDIR /app
COPY . .
RUN npm install && npm run build

# Step 2: 실행용 (빌드 결과물만)
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/index.js"]

✅ scratch, Alpine Linux 활용한 이미지 경량화

scratch 이미지는 아무것도 포함되지 않은 완전 빈 베이스 이미지예요. 정적으로 컴파일된 Go, Rust앱에 적합해요.

FROM scratch
COPY mybinary /mybinary
ENTRYPOINT ["/mybinary"]

 

Alpine Linux는 매우 작고 경량화된 리눅스 배포판이에요. glibc 대신 musl 사용하므로, 일부 라이브러리 호환 이슈 주의가 필요해요.

FROM alpine