테마 전환

Dockerfile 입문 가이드: 첫 Docker 이미지 처음부터 만들기(예제 포함)

Easton editorial illustration: image-layer stack

터미널에 오류 메시지가 계속 올라옵니다. COPY ../config.json: no such file or directory. 오늘 밤에만 벌써 여덟 번째 빌드 실패입니다. 로컬에서는 분명히 잘 실행되는데 Docker 이미지로 패키징하기만 하면 온갖 오류가 발생합니다. Stack Overflow를 샅샅이 뒤져 top answer대로 한참 수정했더니 이미지 용량은 200MB에서 2GB로 불어났습니다.

프로젝트의 Dockerfile을 바라보면 FROM, RUN, COPY, CMD가 화면을 가득 채웁니다. 단어 하나하나는 아는데 한데 모아 놓으면 이해하기 어렵습니다. 인터넷에서 찾은 튜토리얼을 그대로 따라 해도 십중팔구 실행되지 않습니다. 오류 메시지도 늘 모호합니다. 경로가 잘못된 걸까요, 아니면 명령어를 잘못 사용한 걸까요?

180배
용량 차이
Alpine vs 전체 이미지
10배
용량 절감
RUN 명령어 통합
30배
빌드 속도 향상
5분→10초
Source: 실측 데이터

Dockerfile은 그렇게 난해하지 않습니다. 각 명령어의 역할과 흔한 문제를 하나씩 이해하고 나면 핵심은 몇 가지뿐입니다. 이 글에서는 가장 이해하기 쉬운 방식으로 첫 Docker 이미지를 처음부터 만들어 봅니다. 모든 명령어에 실제 코드를 곁들이고 초보자가 가장 자주 빠지는 세 가지 함정도 알려 드립니다. 끝까지 읽으면 자신의 Node.js 또는 Python 프로젝트에서 실제로 실행되는 Dockerfile을 작성할 수 있습니다.

Dockerfile이란 무엇인가요?

간단히 말해 Dockerfile은 Docker 이미지를 빌드하는 모든 단계를 적어 둔 텍스트 파일입니다. 집을 인테리어할 때 쓰는 시공 도면이라고 생각하면 됩니다. 도면에 바닥을 먼저 깔고 벽을 칠한 다음 마지막으로 조명을 설치한다고 적혀 있듯이, Docker 엔진은 이 도면을 따라 애플리케이션 환경을 단계별로 구성하고 최종 이미지를 패키징합니다.

이 이미지는 바로 실행할 수 있는 ‘환경 스냅샷’입니다. 예를 들어 Node.js 애플리케이션에 Node 18, 특정 npm 패키지, 소스 코드가 필요하다면 Dockerfile이 이 모든 것을 이미지 안에 설치합니다. 다른 사람은 이 이미지를 받은 뒤 docker run만 실행하면 환경을 별도로 설정하지 않고도 애플리케이션을 실행할 수 있습니다.

결국 Dockerfile이 하는 일은 세 단계입니다.

  1. 기본 환경을 선택합니다. 예를 들면 Node.js 18입니다.
  2. 코드와 의존성을 설치합니다.
  3. 컨테이너가 시작될 때 실행할 명령을 지정합니다.

Dockerfile을 작성한 뒤 docker build 명령 하나를 실행하면 이미지가 만들어집니다. 간단해 보이죠? 실제로 복잡하지 않지만 세부 사항에서 문제가 생기곤 합니다. 이제 몇 가지 핵심 명령어를 살펴보겠습니다.

6가지 핵심 명령어 자세히 알아보기

Docker 이미지 처음부터 빌드하는 전체 과정

기본 이미지 선택부터 첫 Docker 이미지의 빌드와 실행까지 Dockerfile 작성 과정을 단계별로 설명합니다

Estimated time: PT20M

  1. 1

    Step 1: 1단계: 기본 이미지 선택(FROM)

    FROM은 반드시 첫 번째 명령어여야 하며 적절한 기본 이미지를 선택합니다.
  2. 2

    Step 2: 2단계: 작업 디렉터리 설정(WORKDIR)

    WORKDIR /app으로 작업 디렉터리를 설정합니다.
  3. 3

    Step 3: 3단계: 의존성 파일 복사 및 설치(COPY+RUN)

    먼저 package*.json 파일을 복사한 뒤 npm install로 의존성을 설치합니다.
  4. 4

    Step 4: 4단계: 애플리케이션 코드 복사(COPY)

    소스 코드를 컨테이너로 복사합니다.
  5. 5

    Step 5: 5단계: 포트 노출(EXPOSE)

    EXPOSE 3000으로 컨테이너가 사용하는 포트를 선언합니다.
  6. 6

    Step 6: 6단계: 시작 명령 설정(CMD)

    CMD [“npm”, “start”]로 컨테이너 시작 시 실행할 기본 명령을 설정합니다.
  7. 7

    Step 7: 7단계: 빌드 및 실행

    이미지 빌드:

FROM - 기본 이미지 선택

FROM은 주석과 ARG를 제외하면 Dockerfile의 첫 번째 명령어여야 합니다. 애플리케이션 환경의 ‘기초’를 결정합니다.

집을 지을 때 먼저 기초를 고르듯이 Dockerfile을 작성할 때도 기본 이미지를 먼저 선택합니다. Node.js로 작성한 애플리케이션이라면 node:18-alpine을 사용합니다. Python 프로젝트라면 python:3.11-slim이 좋은 선택입니다. Nginx를 리버스 프록시로 쓴다면 nginx:alpine을 선택하면 됩니다.

# Node.js 18의 Alpine Linux 버전을 기본 이미지로 선택
FROM node:18-alpine

여기서 alpine과 slim의 차이를 알아보겠습니다. alpine은 Alpine Linux 기반의 초경량 이미지로, 용량이 약 5MB에 불과해 프로덕션 환경에 적합합니다. 전체 node 이미지는 900MB로 무려 180배나 큽니다. 하지만 alpine에는 작은 함정이 있습니다. glibc 대신 musl libc를 사용하기 때문에 일부 네이티브 의존성에서 오류가 발생할 수 있습니다. 예상하지 못한 컴파일 오류가 생기면 node:18-slim으로 바꿔 보세요.

초보자가 흔히 하는 실수: 아무 node 이미지나 골랐다가 프로젝트가 요구하는 버전과 맞지 않아 의존성 설치 과정에서 여러 오류가 발생합니다. package.json에 적힌 Node 버전에 맞춰 FROM에서도 같은 버전을 사용하세요.

RUN - 명령을 실행해 이미지 빌드하기

RUN은 이미지 빌드 중에 명령을 실행합니다. 소프트웨어 설치, 디렉터리 생성, 설정 파일 수정 등에 사용합니다. 핵심은 각 RUN 명령어가 새로운 이미지 레이어를 만든다는 점입니다.

잘못된 예를 살펴보겠습니다.

# ❌ 좋지 않은 작성법: 이미지 레이어 3개 생성
RUN apt-get update
RUN apt-get install -y python3
RUN apt-get clean

RUN을 실행할 때마다 양파 껍질처럼 이미지 바깥쪽에 레이어가 하나씩 추가됩니다. Docker의 레이어 저장 방식에서는 뒤에서 파일을 삭제해도 앞선 레이어의 데이터가 그대로 남으므로 이미지 용량은 여전히 큽니다. 저도 이런 방식으로 여러 도구를 설치하는 RUN을 일곱 줄이나 작성했다가 최종 이미지가 2GB가 되었고, 서버에 전송하는 데 30분이나 걸린 적이 있습니다.

올바른 방법은 &&로 명령을 연결하는 것입니다.

# ✅ 권장 작성법: 이미지 레이어 1개만 생성
RUN apt-get update && \
    apt-get install -y python3 && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

역슬래시 \는 줄바꿈을 의미하며 명령이 지나치게 빽빽해 보이지 않게 해 줍니다. 마지막 rm은 매우 중요합니다. 설치 패키지 캐시를 정리해 수십 MB를 절약할 수 있습니다.

또 하나 주의할 점은 RUN apt-get update를 절대 따로 작성하지 않는 것입니다. Docker는 각 레이어를 캐시하기 때문에 update를 별도 레이어로 두면 나중에 소프트웨어를 설치할 때 오래된 캐시를 사용해 새 버전을 설치하지 못할 수 있습니다. update와 install은 항상 함께 작성하세요.

COPY vs ADD - 파일 복사하기

두 명령어 모두 이미지 안으로 파일을 복사하지만 COPY는 단순하고 명확한 반면 ADD는 기능이 많아 예상치 못한 동작을 일으키기 쉽습니다. Docker 공식 권장 사항은 COPY로 할 수 있다면 ADD를 사용하지 않는 것입니다.

먼저 COPY의 기본 사용법을 살펴보겠습니다.

# 파일 하나 복사
COPY package.json /app/

# 디렉터리 전체 복사
COPY ./src /app/src

# 현재 디렉터리의 모든 내용을 컨테이너의 /app으로 복사
COPY . /app

간단해 보이지만 치명적인 오해가 하나 있습니다. 경로는 Dockerfile이 아니라 빌드 컨텍스트를 기준으로 합니다.

빌드 컨텍스트란 무엇일까요? docker build 명령을 실행할 때 마지막의 점(.)으로 지정한 디렉터리입니다. 예를 들어 프로젝트 루트에서 docker build .을 실행하면 프로젝트 루트가 빌드 컨텍스트가 되며 COPY는 이 디렉터리와 그 하위 디렉터리에 있는 파일만 접근할 수 있습니다.

글 앞부분에서 언급한 오류도 이 때문에 발생합니다.

# ❌ 잘못된 작성법: 빌드 컨텍스트를 벗어남
COPY ../config.json /app/
COPY /opt/myfile.txt /app/

첫 번째 명령은 상위 디렉터리에 접근하려 하고 두 번째 명령은 절대 경로에 접근하려 하므로 둘 다 실패합니다. Docker는 보안과 재현 가능한 빌드를 위해 이렇게 설계되었습니다. 호스트 시스템의 임의 파일에 마음대로 접근할 수 없게 한 것입니다.

해결 방법은 다음과 같습니다.

  • config.json을 프로젝트 디렉터리 안으로 옮깁니다.
  • 또는 상위 디렉터리에서 docker build -f myproject/Dockerfile .을 실행합니다.

ADD도 살펴보겠습니다. ADD는 파일을 복사할 뿐 아니라 tar 압축 파일을 자동으로 풀고 URL에서 파일을 내려받을 수도 있습니다.

# ADD는 자동으로 압축을 풉니다
ADD myarchive.tar.gz /app/

# ADD는 URL에서 파일을 내려받을 수 있습니다(권장하지 않음)
ADD https://example.com/file.txt /app/

편리해 보이지만 동작이 명확하지 않다는 문제가 있습니다. 다른 사람이 ADD를 보면 파일의 압축이 풀리는지 바로 알기 어려워 실수하기 쉽습니다. 압축을 풀어야 한다면 RUN tar -xzf를 명시하고, 파일을 내려받아야 한다면 RUN curl을 사용하는 편이 훨씬 분명합니다.

WORKDIR - 작업 디렉터리 설정하기

WORKDIR은 Linux의 cd 명령과 같습니다. 이후 명령어가 실행될 작업 디렉터리를 설정하며, 해당 디렉터리가 없으면 Docker가 자동으로 만듭니다.

WORKDIR /app
COPY . .  # 이제 /app 디렉터리로 복사
RUN npm install  # /app 디렉터리에서 실행

상대 경로보다는 절대 경로를 권장합니다. 상대 경로는 앞선 WORKDIR을 기준으로 해 혼동하기 쉽습니다.

WORKDIR을 사용하면 각 RUN 명령마다 cd /app &&를 반복할 필요가 없어 코드가 훨씬 깔끔해집니다.

CMD vs ENTRYPOINT - 컨테이너 시작 명령

이 두 명령어는 가장 혼동하기 쉽습니다. 간단히 기억하면 CMD는 덮어쓸 수 있지만 ENTRYPOINT는 덮어쓸 수 없습니다.

먼저 CMD는 컨테이너가 시작될 때 실행할 기본 명령을 설정합니다.

CMD ["node", "server.js"]

이렇게 작성하면 docker run my-appnode server.js를 실행합니다. 하지만 docker run my-app npm test를 실행하면 CMD가 덮어써져 실제로는 npm test가 실행됩니다.

ENTRYPOINT는 컨테이너의 주 프로세스를 정의하며 덮어쓸 수 없습니다.

ENTRYPOINT ["node"]
CMD ["server.js"]

이렇게 조합하면 docker run my-appnode server.js를 실행하고, docker run my-app script.jsnode script.js를 실행합니다. 차이가 보이나요? ENTRYPOINT는 고정되고 CMD 또는 docker run의 인수는 ENTRYPOINT 뒤에 추가됩니다.

언제 무엇을 써야 할까요?

  • CMD만 사용: 프로덕션에서는 npm start, 테스트에서는 npm test처럼 시작 방식이 달라질 수 있는 애플리케이션 서비스
  • ENTRYPOINT + CMD 조합: 주 명령은 고정하고 인수만 바꾸는 도구 이미지. 예를 들어 Python 스크립트에서는 주 명령이 python이고 스크립트 이름이 인수입니다.
  • ENTRYPOINT만 사용: 컨테이너가 오직 한 가지 일만 하는 특별히 고정된 경우

비교해 보겠습니다.

# 사례 1: Web 애플리케이션(CMD 사용)
FROM node:18-alpine
WORKDIR /app
COPY . .
CMD ["npm", "start"]
# docker run my-app → npm start
# docker run my-app npm test → npm test(CMD가 덮어써짐)

# 사례 2: Python 도구(ENTRYPOINT + CMD 사용)
FROM python:3.11-slim
ENTRYPOINT ["python"]
CMD ["main.py"]
# docker run my-tool → python main.py
# docker run my-tool script.py → python script.py

처음에는 저도 이 차이를 이해하지 못했습니다. 나중에는 ENTRYPOINT는 ‘무엇을 할지’, CMD는 ‘어떻게 할지’라는 한 문장으로 기억하게 되었습니다.

ENV - 환경 변수

ENV는 컨테이너 실행 시 사용할 환경 변수를 설정합니다. 이후 RUN, CMD 등의 명령어에서도 이 변수를 사용할 수 있습니다.

ENV NODE_ENV=production
ENV PORT=3000

# RUN에서 사용
RUN echo "Environment: $NODE_ENV"

# 애플리케이션 코드에서도 이 환경 변수를 읽을 수 있음
CMD ["node", "server.js"]

일반적인 사용 사례는 다음과 같습니다.

  • NODE_ENV=production을 설정해 Node.js에 프로덕션 환경임을 알립니다.
  • PATH를 설정해 사용자 정의 명령 경로를 추가합니다.
  • 포트 번호나 데이터베이스 주소 같은 애플리케이션 인수를 설정합니다.

ENV로 설정한 변수는 최종 이미지에 남습니다. 비밀번호 같은 민감한 정보가 있다면 ENV를 사용하지 마세요. 컨테이너 실행 시 docker run -e로 전달하거나 Docker Secrets를 사용해야 합니다.

실습 - 첫 이미지 빌드하기

이론을 많이 살펴봤지만 직접 해 봐야 제대로 이해할 수 있습니다. 간단한 Node.js 애플리케이션을 예로 들어 첫 Docker 이미지를 단계별로 빌드해 보겠습니다.

1단계: 프로젝트 준비

아주 간단한 Node.js 애플리케이션을 만듭니다.

mkdir my-node-app
cd my-node-app

package.json을 만듭니다.

{
  "name": "my-node-app",
  "version": "1.0.0",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^4.18.2"
  }
}

server.js를 만듭니다.

const express = require('express');
const app = express();
const PORT = 3000;

app.get('/', (req, res) => {
  res.send('Hello from Docker!');
});

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

2단계: Dockerfile 작성

프로젝트 루트에 확장자가 없는 Dockerfile을 만듭니다.

# 1. 기본 이미지 선택
FROM node:18-alpine

# 2. 작업 디렉터리 설정
WORKDIR /app

# 3. 의존성 파일 복사(캐시 활용)
COPY package*.json ./

# 4. 의존성 설치
RUN npm install --production

# 5. 애플리케이션 코드 복사
COPY . .

# 6. 포트 노출
EXPOSE 3000

# 7. 애플리케이션 시작
CMD ["npm", "start"]

package.json과 소스 코드를 왜 따로 복사할까요? 핵심은 Docker의 캐시 방식입니다.

Docker는 명령 순서대로 빌드하며 각 명령어가 하나의 레이어를 만듭니다. 어떤 레이어가 바뀌면 그 뒤의 모든 레이어를 다시 빌드합니다. package.json은 거의 바뀌지 않지만 소스 코드는 자주 수정됩니다. 모든 파일을 먼저 복사한 뒤 의존성을 설치하면 코드를 수정할 때마다 의존성을 다시 설치해야 하므로 매우 느립니다.

지금과 같은 순서에서는 package.json이 바뀌지 않는 한 Docker가 캐시된 의존성 레이어를 사용하고 코드 복사 단계로 바로 넘어갑니다. 빌드 속도가 훨씬 빨라집니다.

3단계: 이미지 빌드

프로젝트 루트에서 다음 명령을 실행합니다.

docker build -t my-node-app:1.0 .

인수의 의미는 다음과 같습니다.

  • -t my-node-app:1.0: 이미지에 이름:버전 형식으로 태그를 지정합니다.
  • .: 현재 디렉터리를 빌드 컨텍스트로 지정합니다.

터미널에 많은 내용이 출력되며 각 단계는 Dockerfile의 명령어 하나에 해당합니다. 모든 과정이 순조롭게 끝나면 마지막에 Successfully built xxx가 표시됩니다.

4단계: 컨테이너 실행

docker run -p 3000:3000 my-node-app:1.0

인수의 의미는 다음과 같습니다.

  • -p 3000:3000: 호스트 포트:컨테이너 포트 형식으로 포트를 매핑합니다.
  • my-node-app:1.0: 실행할 이미지입니다.

터미널에 Server running on port 3000이 출력됩니다.

5단계: 결과 확인

브라우저에서 http://localhost:3000에 접속해 Hello from Docker!가 보이면 성공입니다.

Ctrl+C를 눌러 컨테이너를 중지합니다.

요약

전체 과정은 다음과 같습니다.

  1. 코드를 작성합니다(package.json + server.js).
  2. Dockerfile을 작성해 Docker에 패키징 방법을 알려 줍니다.
  3. 이미지를 빌드합니다(docker build).
  4. 컨테이너를 실행합니다(docker run).

생각보다 복잡하지 않죠? 핵심은 각 명령어의 역할, 빌드 컨텍스트, 캐시 방식이라는 두 가지 개념을 이해하는 것입니다.

초보자를 위한 문제 해결 가이드

지금까지 올바른 작성법을 살펴봤으니 이제 초보자가 가장 자주 겪는 문제를 알아보겠습니다. 저도 모두 직접 겪었던 문제들이며, 이 내용을 알면 많은 시간을 아낄 수 있습니다.

문제 1: 잘못된 빌드 컨텍스트 경로

현상: 파일이 분명히 있는데 COPY 명령어에서 no such file or directory 오류가 발생합니다.

원인: COPY 경로는 Dockerfile이 아니라 빌드 컨텍스트를 기준으로 합니다.

# ❌ 잘못됨: 상위 디렉터리에 접근하려 함
COPY ../config.json /app/

# ❌ 잘못됨: 절대 경로에 접근하려 함
COPY /opt/myfile.txt /app/

해결 방법:

  1. 파일을 프로젝트 디렉터리 안으로 옮깁니다.
  2. 또는 빌드 명령을 docker build -f subdir/Dockerfile .으로 조정합니다. -f로 Dockerfile 위치를 지정하고 마지막의 점은 계속 상위 디렉터리를 컨텍스트로 사용합니다.

숨은 문제도 하나 있습니다. 루트 디렉터리에서 docker build .을 실행하면 Docker가 node_modules와 .git 같은 큰 폴더를 포함한 전체 디렉터리를 Docker 데몬으로 전송합니다. 저도 몇 GB를 전송하느라 빌드 시작 전에 10분을 기다린 적이 있습니다.

해결하려면 .dockerignore 파일을 만들어 필요 없는 파일을 제외합니다.

node_modules
.git
.env
*.log

문제 2: 지나치게 많은 이미지 레이어로 인한 용량 증가

현상: 코드는 몇 MB밖에 안 되는데 이미지 용량은 몇 GB에 이를 정도로 큽니다.

원인: 각 RUN, COPY, ADD는 새 레이어를 만듭니다. 뒤에서 파일을 삭제해도 앞선 레이어의 데이터는 그대로 남습니다.

# ❌ 레이어 7개를 만들며 각 레이어에 데이터가 남음
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git
RUN curl -o tool.sh https://example.com/tool.sh
RUN chmod +x tool.sh
RUN ./tool.sh
RUN rm tool.sh  # 이렇게 삭제해도 소용없음! 앞선 레이어에 tool.sh가 남아 있음

해결 방법: RUN 명령어를 합쳐 설치, 사용, 정리를 같은 레이어에서 처리합니다.

# ✅ 하나의 레이어만 생성하므로 같은 레이어에서 정리가 적용됨
RUN apt-get update && \
    apt-get install -y curl git && \
    curl -o tool.sh https://example.com/tool.sh && \
    chmod +x tool.sh && \
    ./tool.sh && \
    rm tool.sh && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

저도 RUN을 일곱 줄로 나눠 썼다가 최종 이미지가 2GB가 된 적이 있습니다. 하나로 합친 뒤 200MB로 줄어 10배나 작아졌습니다.

10배
용량 절감
Source: RUN 명령어 통합: 2GB → 200MB

문제 3: 의존성 캐시 무효화

현상: 빌드할 때마다 의존성을 다시 설치해 시간이 너무 오래 걸립니다.

원인: COPY 순서가 잘못되어 코드와 의존성 파일을 함께 복사하므로 코드를 수정할 때마다 의존성을 다시 설치하게 됩니다.

# ❌ 잘못된 순서: 코드를 수정하면 의존성을 다시 설치함
COPY . .
RUN npm install

Docker 캐시는 명령 순서대로 동작합니다. 위와 같은 작성법에서는 주석 하나만 수정해도 소스 코드가 바뀌면서 COPY 레이어가 변경되고 뒤의 npm install도 다시 실행됩니다.

해결 방법: 의존성 파일을 먼저 복사해 의존성을 설치한 다음 소스 코드를 복사합니다.

# ✅ 올바른 순서: package.json이 바뀔 때만 다시 설치
COPY package*.json ./
RUN npm install
COPY . .

이렇게 하면 소스 코드를 수정해도 npm install을 다시 실행하지 않습니다. 빌드 시간이 5분에서 10초로 줄어들 정도로 훨씬 빨라집니다.

추가 팁: EXPOSE는 필수가 아닙니다

많은 튜토리얼에 EXPOSE 3000이 있어서 초보자는 이를 작성하지 않으면 포트에 접근할 수 없다고 생각하기 쉽습니다. 하지만 EXPOSE는 이 이미지가 어느 포트를 사용하는지 알리는 문서 역할만 하므로 작성하지 않아도 실행할 수 있습니다.

실제 포트 매핑은 docker run -p 명령이 담당합니다.

# Dockerfile에 EXPOSE가 없어도 이렇게 접근할 수 있음
docker run -p 3000:3000 my-app

그래도 다른 사람이 사용하는 포트를 쉽게 이해할 수 있도록 EXPOSE를 작성하는 편이 좋습니다.

결론

지금까지 많은 내용을 다뤘지만 Dockerfile 입문에서 기억해야 할 핵심은 세 가지입니다.

  1. 핵심 명령어 이해하기: FROM은 기본 이미지를 고르고, RUN은 필요한 항목을 설치하며, COPY는 파일을 옮기고, CMD는 애플리케이션을 시작합니다. WORKDIR과 ENV는 이를 돕습니다.
  2. 빌드 컨텍스트 이해하기: COPY는 docker build 명령 마지막의 점으로 지정한 디렉터리 안에서만 파일에 접근할 수 있습니다. 상위 디렉터리나 절대 경로에 접근하려 해서는 안 됩니다.
  3. 캐시 방식 활용하기: 의존성 설치처럼 변경이 적은 작업은 앞에, 코드 복사처럼 변경이 잦은 작업은 뒤에 배치합니다. RUN 명령어를 합쳐 이미지 레이어 수도 줄입니다.

이제 자신의 작은 프로젝트로 직접 시도해 보세요. 먼저 가장 기본적인 Dockerfile을 작성해 실행하는 데 집중하고 완벽함을 추구할 필요는 없습니다. 익숙해지면 멀티 스테이지 빌드나 이미지 용량 최적화 같은 고급 기술을 배워도 됩니다.

Docker는 그렇게 어렵지 않습니다. 중요한 것은 직접 연습하는 것입니다. 저도 첫 Dockerfile을 작성할 때 밤새 오류를 만났지만 원리를 이해하고 나니 생각보다 단순했습니다. 여러분도 충분히 할 수 있습니다.

다음에 배울 내용:

  • Docker Compose(여러 컨테이너 애플리케이션 관리)
  • 멀티 스테이지 빌드(이미지 용량 추가 절감)
  • Docker 네트워크와 볼륨(컨테이너 간 통신 및 데이터 영속성)

여러분도 첫 Docker 이미지를 성공적으로 빌드해 보세요. 궁금한 점이 있다면 댓글로 남겨 주세요.

FAQ

Dockerfile의 핵심 명령어에는 무엇이 있나요?
6가지 핵심 명령어는 다음과 같습니다.
1) FROM은 기본 이미지를 선택하며 반드시 첫 번째 명령어여야 합니다.
2) RUN은 이미지를 빌드할 명령을 실행하며 &&로 합치면 레이어 수를 줄일 수 있습니다.
3) COPY는 파일을 복사하며 경로는 빌드 컨텍스트 기준입니다. ../ 또는 절대 경로는 사용할 수 없습니다.
4) WORKDIR은 작업 디렉터리를 설정하며 절대 경로를 권장합니다.
5) CMD는 컨테이너 시작 명령을 설정하며 덮어쓸 수 있습니다.
6) ENV는 환경 변수를 설정합니다.

그 밖에 주 명령을 고정하는 ENTRYPOINT와 문서 목적으로 포트를 선언하는 EXPOSE도 있습니다.
COPY ../config.json을 실행하면 왜 오류가 발생하나요?
COPY 경로는 Dockerfile이 아니라 빌드 컨텍스트를 기준으로 합니다.

빌드 컨텍스트는 docker build 명령 마지막의 점(.)으로 지정한 디렉터리입니다. COPY는 이 디렉터리와 그 하위 디렉터리에 있는 파일만 접근할 수 있으며 상위 디렉터리나 절대 경로에는 접근할 수 없습니다.

해결 방법은 다음과 같습니다.
• 파일을 프로젝트 디렉터리 안으로 옮깁니다.
• 또는 docker build -f subdir/Dockerfile . 명령으로 빌드 위치를 조정합니다.

.dockerignore를 만들어 node_modules, .git 같은 큰 폴더를 제외하면 빌드 속도를 높일 수 있습니다.
Docker 이미지 용량을 줄이려면 어떻게 해야 하나요?
세 가지 방법이 있습니다.

1) Alpine 이미지를 사용합니다.
• 전체 이미지 900MB에 비해 약 5MB로, 용량 차이가 180배입니다.

2) RUN 명령어를 합칩니다.
• &&로 연결해 설치, 사용, 정리를 같은 레이어에서 처리합니다.
• 이미지 용량을 2GB에서 200MB로 줄여 10배 절감할 수 있습니다.

3) .dockerignore를 만들어 필요 없는 파일을 제외합니다.

오래된 캐시를 사용하지 않도록 apt-get update와 install은 항상 함께 작성하세요.
Docker 캐시로 빌드 속도를 높이려면 어떻게 해야 하나요?
핵심은 변경이 적은 작업을 앞에, 변경이 잦은 작업을 뒤에 배치하는 것입니다.

올바른 순서는 다음과 같습니다.
• 먼저 package*.json을 복사해 의존성을 설치합니다.
• 그다음 소스 코드를 복사합니다.
• package.json이 바뀌지 않으면 의존성 레이어를 다시 빌드하지 않습니다.
• 빌드 시간이 5분에서 10초로 줄어 30배 빨라집니다.

잘못된 순서는 다음과 같습니다.
• 모든 파일을 먼저 복사한 뒤 의존성을 설치합니다.
• 코드를 수정할 때마다 의존성을 다시 설치하므로 매우 느립니다.
CMD와 ENTRYPOINT의 차이는 무엇인가요?
CMD는 docker run 인수로 덮어쓸 수 있지만 ENTRYPOINT는 덮어쓸 수 없습니다.

사용 사례는 다음과 같습니다.
1) CMD만 사용: 프로덕션의 npm start, 테스트의 npm test처럼 시작 방식이 달라질 수 있는 애플리케이션 서비스
2) ENTRYPOINT+CMD 조합: Python 스크립트처럼 주 명령은 고정하고 인수만 바꾸는 도구 이미지
3) ENTRYPOINT만 사용: 동작이 특별히 고정된 경우

ENTRYPOINT는 '무엇을 할지', CMD는 '어떻게 할지'라고 기억하면 됩니다.
EXPOSE 명령어는 꼭 필요한가요?
필수는 아닙니다. EXPOSE는 이 이미지가 어느 포트를 사용하는지 알리는 문서 역할만 하므로 작성하지 않아도 실행할 수 있습니다.

실제 포트 매핑은 docker run -p 명령이 담당합니다. Dockerfile에 EXPOSE가 없어도 docker run -p 3000:3000 my-app으로 정상적으로 접근할 수 있습니다.

다만 다른 사람이 이미지에서 사용하는 포트를 이해하기 쉽도록 EXPOSE를 작성하는 편이 좋습니다.
Alpine 이미지와 slim 이미지의 차이는 무엇인가요?
Alpine은 Alpine Linux 기반입니다.
• 용량이 약 5MB에 불과해 프로덕션 환경에 적합합니다.
• 하지만 glibc 대신 musl libc를 사용하므로 일부 네이티브 의존성에서 오류가 발생할 수 있습니다.
• 전체 이미지는 900MB로 용량 차이가 180배입니다.

예상하지 못한 컴파일 오류가 발생하면 node:18-slim 같은 slim 버전으로 바꿔 보세요.

선택 원칙은 Alpine을 우선 사용하고 호환성 문제가 생기면 slim으로 바꾸는 것입니다.

4분 읽기 · 게시일: 2025년 12월 17일 · 수정일: 2026년 9월 4일

댓글

GitHub로 로그인하여 댓글을 남기세요

Easton BlogEaston Blog