[BASIC #4] 나만의 이미지 만들기: Dockerfile 완전 정복

2025. 5. 31. 22:34·Docker

0. 들어가며

지금까지 우리는 docker pull 명령어로 기존에 만들어진 이미지(nginx, ubuntu, mysql 등)를 다운로드받아 사용했다. 하지만 실제 개발에서는 나만의 애플리케이션을 이미지로 만들어야 할 때가 많다. Spring Boot 애플리케이션을 개발했다면, 이 앱을 도커 이미지로 만들어서 어디서든 실행할 수 있게 하고 싶을 것이다. 이번 포스팅에서는 Dockerfile을 작성하여 나만의 커스텀 이미지를 만드는 방법을 알아보겠다.


 

1. Dockerfile이란?

1.1. Dockerfile의 정의

Dockerfile은 도커 이미지를 생성하기 위한 설계도와 같은 텍스트 파일이다.

  • 파일 이름은 관례적으로 Dockerfile을 사용한다. (확장자 없음)
  • 파일 안에는 베이스 이미지부터 시작해서, 복사할 파일, 실행할 명령어 등이 순서대로 작성된다.
  • 이 파일을 docker build 명령어로 빌드하면 실제 도커 이미지가 생성된다.

1.2. 실생활 비유: 레시피 카드

Dockerfile은 요리 레시피 카드에 비유할 수 있다.

요리  레시피  Dockerfile
"재료: 된장, 두부, 호박..." FROM ubuntu:20.04
"두부를 깍둑썰기 하세요" RUN apt-get update
"냄비에 물을 붓고 끓이세요" CMD ["java", "-jar", "app.jar"]

레시피 카드대로 요리하면 된장찌개가 완성되듯, Dockerfile대로 빌드하면 도커 이미지가 완성된다.


2. Dockerfile 주요 명령어

2.1. FROM: 베이스 이미지 지정 (⭐⭐)

모든 Dockerfile의 첫 줄에 위치해야 하는 명령어로, "이 이미지는 무엇으로부터 시작할 것인가" 를 정의한다. FROM 뒤에는 해당 애플리케이션을 실행하기 위해 반드시 필요한 런타임(Runtime)이 포함된 이미지를 적는다.

실무 관점 해설:

예를 들어 Spring Boot(Java)를 사용한다면, Java 실행 환경인 JRE나 개발 환경인 JDK가 포함된 이미지를 선택해야 한다.

상황 가정:

  • AWS EC2 t3.medium 인스턴스
  • Ubuntu 22.04 LTS
  • Spring Boot 애플리케이션 배포 예정

FROM이 하는 일:

[내 Docker 이미지] = [FROM에서 지정한 베이스 이미지] + [내가 추가한 파일/설정]

예를 들어 FROM openjdk:17-jdk-slim이라고 하면:

  • 이 이미지는 OpenJDK 17이 이미 설치된 Ubuntu를 기반으로 시작한다.
  • 즉, EC2에 Java를 따로 설치할 필요 없이, 이 이미지 안에 Java 17이 포함되어 있다.
  • slim은 "가벼운 버전"이라는 뜻으로, 불필요한 패키지를 제거해 이미지 크기를 줄인다.
# Ubuntu 20.04를 기반으로 시작
FROM ubuntu:20.04

# OpenJDK 17이 설치된 이미지를 기반으로 시작
FROM openjdk:17-jdk-slim

# AWS EC2에 최적화된 Amazon Corretto
FROM amazoncorretto:17

# Eclipse Temurin (예전 AdoptOpenJDK)
FROM eclipse-temurin:17-jdk-jammy

# 이미 존재하는 나만의 이미지를 기반으로 시작
FROM myapp:base

다양한 베이스 이미지 선택 기준:

이미지  특징  사용 예시
openjdk:17-jdk-slim Debian 기반, 범용적 일반적인 개발/배포
amazoncorretto:17 AWS 최적화, Amazon Linux 기반 AWS 환경에 배포할 때
eclipse-temurin:17-jdk-jammy Ubuntu 22.04 기반 Ubuntu 환경과 완전 일치
openjdk:17-jre-slim JRE만 포함 (더 가벼움) 실행만 필요할 때 (컴파일 불필요)

 

2.2. WORKDIR: 작업 디렉토리 설정 (⭐⭐)

WORKDIR은 "이 다음부터 실행될 명령어들은 어느 디렉토리에서 실행할 것인가" 를 정의한다. 해당 디렉토리가 없으면 자동으로 생성된다.

실무 관점 해설:

상황 가정:

  • EC2 Ubuntu 22.04의 기본 디렉토리 구조:
    • /home/ubuntu/ - 일반 사용자 홈
    • /var/www/ - 웹 애플리케이션
    • /opt/ - 추가 애플리케이션

WORKDIR의 실제 의미:

WORKDIR /app

이 한 줄이 의미하는 바:

  1. 컨테이너 내부에 /app 디렉토리를 생성한다.
  2. 이후 모든 명령어(COPY, RUN, CMD, ENTRYPOINT)는 이 /app 디렉토리에서 실행된다.
  3. 마치 터미널에서 cd /app을 입력한 것과 같은 상태가 된다.

파일 시스템 관점:

컨테이너 내부 구조:
/
├── bin/
├── etc/
├── home/
├── var/
└── app/          ← WORKDIR /app 으로 생성된 디렉토리
    ├── app.jar   ← 여기에 파일들이 복사됨
    └── config/   ← 여기에 설정 파일들이 위치

잘못된 예시 vs 올바른 예시:

# ❌ 잘못된 예시 (디렉토리 정리 안 됨)
COPY app.jar /
RUN java -jar /app.jar  # 매번 전체 경로 써야 함

# ✅ 올바른 예시 (WORKDIR 사용)
WORKDIR /app
COPY app.jar .          # /app/app.jar
RUN java -jar app.jar   # 현재 디렉토리 기준으로 실행

실무에서의 WORKDIR 패턴:

# Spring Boot 애플리케이션 표준 패턴
FROM eclipse-temurin:17-jre-jammy
WORKDIR /opt/spring-boot  # /opt 아래에 애플리케이션 배치
COPY app.jar .
ENTRYPOINT ["java", "-jar", "app.jar"]

# Node.js 애플리케이션 표준 패턴
FROM node:18-alpine
WORKDIR /usr/src/app      # 전통적인 소스 코드 위치
COPY package*.json .
RUN npm install
COPY . .
CMD ["npm", "start"]

 

2.3. COPY: 파일 복사 (⭐⭐)

COPY는 "호스트 컴퓨터(지금 Dockerfile이 있는 곳)의 파일을 이미지 안으로 복사" 하는 명령어다.

실무 관점 해설:

상황 가정:

  • EC2에서 빌드한다면, 호스트는 EC2 인스턴스 자체다.
  • 로컬에서 빌드한다면, 호스트는 개발자의 맥북/윈도우 PC다.

COPY의 실제 동작:

COPY target/myapp.jar app.jar

이 한 줄이 하는 일:

  1. Dockerfile이 있는 디렉토리(빌드 컨텍스트)의 target/myapp.jar 파일을 찾는다.
  2. 이미지 내부의 현재 WORKDIR(/app)에 app.jar라는 이름으로 복사한다.

파일 시스템 관점:

[호스트 (EC2 또는 개발자 PC)]
/home/ubuntu/myapp/
├── Dockerfile
├── target/
│   └── myapp.jar    ← 여기 있는 파일을
└── src/

[이미지 내부]
/app/
    └── app.jar      ← 여기로 복사

다양한 COPY 패턴:

# 1. 단일 파일 복사 (이름 변경)
COPY --chown=1000:1000 target/myapp.jar application.jar

# 2. 여러 파일 한 번에 (와일드카드)
COPY *.properties config/
# config/application.properties
# config/logback.properties

# 3. 디렉토리 전체 복사
COPY src/ /app/src/
# /app/src/main/java/...

# 4. 권한 유지하며 복사 (node 사용자 소유로)
COPY --chown=node:node package*.json ./

.dockerignore의 중요성:

# .dockerignore
.git
.gitignore
README.md
target/
*.log
.env
Dockerfile
.dockerignore
node_modules

왜 필요한가?

  • COPY . . 을 하면 현재 디렉토리의 모든 파일이 이미지에 복사된다.
  • .git 디렉토리(수백 MB)나 node_modules(수 GB)가 복사되면 이미지가 비대해진다.
  • .dockerignore에 명시한 파일들은 복사에서 제외된다.

2.4. ENV: 환경 변수 설정 (⭐⭐)

ENV는 "컨테이너가 실행되는 동안 계속 유지되는 환경 변수" 를 설정한다.

실무 관점 해설:

상황 가정:

  • Spring Boot 애플리케이션은 application.yml에서 환경 변수를 참조한다.
  • 개발 환경(dev), 테스트 환경(staging), 운영 환경(prod)에 따라 DB 연결 정보가 다르다.

ENV의 실제 활용:

# application.yml
spring:
  datasource:
    url: jdbc:mysql://${DB_HOST}:3306/${DB_NAME}
    username: ${DB_USER}
    password: ${DB_PASSWORD}
ENV DB_HOST=localhost \\
    DB_NAME=mydb \\
    DB_USER=admin \\
    DB_PASSWORD=secret

컨테이너 실행 시점:

# 개발 환경
docker run -e DB_HOST=dev-db.example.com myapp

# 운영 환경
docker run -e DB_HOST=prod-db.example.com -e DB_PASSWORD=prodpass myapp

환경 변수 우선순위:

  1. docker run -e 로 전달한 값 (최우선)
  2. Dockerfile의 ENV로 설정한 값
  3. 베이스 이미지에서 설정한 값

실무 패턴:

# Spring Boot 프로파일 설정
ENV SPRING_PROFILES_ACTIVE=prod

# 로깅 레벨
ENV LOGGING_LEVEL_COM_EXAMPLE=DEBUG

# JVM 옵션 (나중에 오버라이드 가능)
ENV JAVA_OPTS="-Xmx512m -Xms256m"

ENTRYPOINT java ${JAVA_OPTS} -jar app.jar

2.5. RUN: 이미지 빌드 중 명령어 실행 (⭐⭐)

RUN은 "이미지를 빌드하는 과정에서 실행되는 명령어" 다.

실무 관점 해설:

상황 가정:

  • Ubuntu 이미지에는 기본적으로 필요한 패키지가 없다.
  • curl, vim, git 등을 설치해야 한다.
  • 설치 후 불필요한 캐시를 지워 이미지 크기를 줄여야 한다.

RUN의 실제 동작:

RUN apt-get update && \\
    apt-get install -y curl vim && \\
    apt-get clean && \\
    rm -rf /var/lib/apt/lists/*

이 한 줄이 하는 일:

  1. 패키지 목록을 업데이트한다.
  2. curl과 vim을 설치한다. (y는 확인 없이 설치)
  3. 설치 캐시를 지운다.
  4. 이미지 레이어로 저장된다.

RUN의 레이어 특성:

# ❌ 나쁜 예시 (레이어 3개)
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get clean

# ✅ 좋은 예시 (레이어 1개)
RUN apt-get update && \\
    apt-get install -y curl && \\
    apt-get clean

각 RUN은 하나의 레이어가 된다. 레이어가 많을수록 이미지 크기가 커진다.

언어별 RUN 패턴:

# Python
RUN pip install --no-cache-dir -r requirements.txt

# Node.js
RUN npm ci --only=production && npm cache clean --force

# Maven
RUN mvn dependency:go-offline  # 의존성 미리 다운로드

2.6. ENTRYPOINT: 고정 실행 명령어 (⭐⭐)

ENTRYPOINT는 "컨테이너가 시작될 때 반드시 실행되는 명령어" 다.

실무 관점 해설:

상황 가정:

  • Spring Boot 애플리케이션은 java -jar app.jar로 실행해야 한다.
  • 이 명령어는 절대 바뀌면 안 된다.
  • 다만 JVM 옵션(Xmx)은 상황에 따라 바꿀 수 있어야 한다.

ENTRYPOINT의 실제 활용:

# 쉘 형태 (실제로는 권장하지 않음)
ENTRYPOINT java -jar app.jar

# exec 형태 (권장)
ENTRYPOINT ["java", "-jar", "app.jar"]

실행 예시:

# 기본 실행
docker run myapp
# 실제 실행: java -jar app.jar

# 추가 인자 전달
docker run myapp --server.port=9090
# 실제 실행: java -jar app.jar --server.port=9090

ENTRYPOINT vs CMD 조합:

# 패턴 1: ENTRYPOINT + CMD (권장)
ENTRYPOINT ["java", "-jar"]
CMD ["app.jar"]
# 실행: java -jar app.jar
# 오버라이드: docker run myapp other.jar → java -jar other.jar

# 패턴 2: ENTRYPOINT만
ENTRYPOINT ["java", "-jar", "app.jar"]
# 실행: java -jar app.jar
# 오버라이드 불가 (--entrypoint로만 가능)

실무에서의 ENTRYPOINT 활용:

# Spring Boot with JVM options
ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar app.jar"]

# Python with module
ENTRYPOINT ["python", "-m", "gunicorn"]
CMD ["app:app", "-b", "0.0.0.0:8000"]

# 데이터베이스 초기화 스크립트
ENTRYPOINT ["/docker-entrypoint.sh"]

2.7. CMD: 기본 실행 명령어 (⭐)

컨테이너 시작 시 실행할 기본 명령어를 지정한다. ENTRYPOINT가 있다면 그 인자로 사용된다.

실무 관점 해설:

CMD는 "기본값" 을 제공하는 역할을 한다. 사용자가 따로 명령어를 지정하지 않으면 이 명령어가 실행된다.

# CMD 단독 사용
CMD ["python", "app.py"]

# ENTRYPOINT의 기본 인자로 사용
ENTRYPOINT ["java", "-jar"]
CMD ["app.jar"]

ENTRYPOINT vs CMD 차이점:

구분  ENTRYPOINT  CMD
역할 고정 실행 명령어 기본 인자 또는 기본 명령어
오버라이드 --entrypoint 옵션으로만 변경 가능 docker run 뒤에 명령어를 주면 무시됨
사용처 실행 파일 지정 인자 또는 기본 명령어 지정

2.8. ARG: 빌드 시 변수 (⭐)

ARG는 "이미지를 빌드할 때만 사용하는 변수" 다. 컨테이너가 실행된 후에는 이 변수는 사라진다.

실무 관점 해설:

상황 가정:

  • 개발자가 로컬에서 빌드할 때와 CI/CD 서버(GitHub Actions)에서 빌드할 때 JAR 파일 위치가 다르다.
  • 개발 환경과 운영 환경에서 다른 설정으로 빌드해야 한다.

ARG의 실제 활용:

ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar

이 코드의 의미:

  1. 기본적으로 build/libs/*.jar 파일을 찾아서 복사한다.
  2. 하지만 빌드할 때 -build-arg로 다른 값을 주면 그 값으로 대체된다.

실제 빌드 시나리오:

# Case 1: 개발자가 로컬에서 빌드
./gradlew build
docker build -t myapp:latest .  # 기본값 build/libs/*.jar 사용

# Case 2: CI/CD 서버에서 빌드 (GitHub Actions)
docker build \\
  --build-arg JAR_FILE=target/*.jar \\
  -t myapp:latest .

# Case 3: 버전 정보 전달
docker build --build-arg VERSION=2.0.0 -t myapp:2.0.0 .

ARG vs ENV 차이:

구분  ARG  ENV
존재 시간 빌드 중에만 존재 컨테이너 실행 내내 존재
사용 목적 빌드 시점의 설정 실행 시점의 설정
변경 가능성 빌드 명령어로만 변경 컨테이너 실행 시에도 변경 가능
예시 JAR 파일 경로, 빌드 버전 DB 연결 정보, 프로파일

2.9. USER: 실행 사용자 지정 (⭐)

컨테이너 내부에서 명령어를 실행할 사용자를 지정한다. (기본값: root)

실무 관점 해설:

기본적으로 컨테이너는 root 권한으로 실행된다. 하지만 보안상 root가 아닌 일반 사용자로 실행하는 것이 좋다. 특히 프로덕션 환경에서는 반드시 일반 사용자로 실행해야 한다.

dockerfile

# node 사용자 생성 및 지정 (Node.js 이미지에는 미리 있음)
USER node

# 또는 직접 생성
RUN useradd -m -u 1001 appuser
USER appuser

# 특정 UID/GID 지정
USER 1000:1000

2.10. EXPOSE: 포트 노출 (⭐)

컨테이너가 실행될 때 사용할 포트를 명시한다.

실무 관점 해설:

EXPOSE는 문서화 목적이 강하다. "이 이미지는 8080 포트를 사용합니다"라고 알려주는 것일 뿐, 실제 포트 매핑은 docker run -p로 해야 한다.

# Spring Boot 기본 포트
EXPOSE 8080

# 여러 포트 노출
EXPOSE 80 443

2.11. VOLUME: 볼륨 마운트 포인트 지정

컨테이너 내부에서 볼륨으로 사용할 경로를 지정한다.

실무 관점 해설:

데이터베이스처럼 데이터가 영구 저장되어야 하는 경우 사용한다. docker run -v로 마운트하지 않아도 anonymous volume이 생성된다.

VOLUME /data
VOLUME ["/var/lib/mysql", "/var/log"]

2.12. LABEL: 메타데이터 추가

이미지에 작성자, 버전 등의 정보를 추가한다.

실무 관점 해설:

CI/CD 파이프라인에서 이미지 필터링에 사용하거나, 유지보수 정보를 남길 때 유용하다.

LABEL maintainer="your-email@example.com"
LABEL version="1.0"
LABEL description="My Spring Boot Application"

2.13. ADD: COPY의 확장판 (사용 자제)

COPY와 유사하지만 몇 가지 추가 기능이 있다.

실무 관점 해설:

ADD는 tar.gz 자동 압축 해제, URL에서 파일 다운로드 등의 기능이 있다. 하지만 이런 기능이 오히려 예측 불가능한 동작을 만들 수 있어서, 대부분의 경우 COPY 사용을 권장한다.

# tar 파일을 자동 압축 해제하여 복사
ADD app.tar.gz /app/

# URL에서 파일 다운로드하여 복사 (잘 사용하지 않음)
ADD <https://example.com/file.txt> /tmp/

권장 방식:

# COPY로 명시적으로 처리
COPY app.tar.gz /tmp/
RUN tar -xzf /tmp/app.tar.gz -C /app && rm /tmp/app.tar.gz

3. Spring Boot 프로젝트 최종 Dockerfile 해설

3.1. Best Practice 템플릿

# 1. FROM: Eclipse Temurin 17 (안정적인 JDK)
FROM eclipse-temurin:17-jdk-jammy

# 2. WORKDIR: /app 디렉토리에서 작업
WORKDIR /app

# 3. ARG: 빌드 시 JAR 파일 경로 (기본값: build/libs/*.jar)
ARG JAR_FILE=build/libs/*.jar

# 4. COPY: JAR 파일을 app.jar로 복사
COPY ${JAR_FILE} app.jar

# 5. ENV: Spring Boot 프로파일 설정 (기본값 prod)
ENV SPRING_PROFILES_ACTIVE=prod

# 6. EXPOSE: 8080 포트 사용 (문서화)
EXPOSE 8080

# 7. ENTRYPOINT: java -jar app.jar 실행
ENTRYPOINT ["java", "-jar", "app.jar"]

3.2. 이 Dockerfile이 EC2에서 실행될 때

  1. EC2에 Docker가 설치되어 있다.
  2. docker build -t myapp . 명령어로 이미지를 빌드한다.
  3. docker run -d -p 8080:8080 myapp 실행
  4. 이미지 내부의 /app/app.jar 실행
  5. Spring Boot가 8080 포트로 실행됨
  6. EC2의 8080 포트로 접속하면 애플리케이션에 연결됨

4. 명령어 실행 순서와 레이어 최적화

4.1. 레이어 캐싱의 원리

Dockerfile의 각 명령어는 하나의 레이어가 된다. 이미지 빌드 시, 변경되지 않은 레이어는 캐시를 재사용한다.

# 1. 베이스 이미지 (캐시 사용)
FROM ubuntu:20.04

# 2. 패키지 설치 (자주 변경되지 않음)
RUN apt-get update && apt-get install -y python3

# 3. 소스 코드 복사 (자주 변경됨)
COPY . /app

# 4. 실행 명령어
CMD ["python3", "/app/app.py"]

4.2. 최적화 팁: 자주 변경되는 것은 나중에

# ❌ 비효율적인 순서
FROM node:18
COPY . /app          # 코드가 변경될 때마다
RUN npm install      # npm install도 다시 실행됨
CMD ["npm", "start"]

# ✅ 최적화된 순서
FROM node:18
WORKDIR /app
COPY package*.json ./   # package.json만 먼저 복사
RUN npm install         # 의존성 설치 (코드 변경 영향 없음)
COPY . .                # 나머지 코드 복사
CMD ["npm", "start"]

package.json이 변경되지 않으면 npm install 레이어는 캐시를 사용하므로 빌드 속도가 매우 빨라진다.


5. 실전 예제 1: Spring Boot 애플리케이션 Dockerize

5.1. 프로젝트 구조

my-spring-app/
├── Dockerfile
├── build.gradle
├── src/
│   └── main/
│       └── java/
│           └── com/
│               └── example/
│                   └── Application.java
└── build/
    └── libs/
        └── app-1.0.0.jar

5.2. Dockerfile 작성

# 베이스 이미지 (slim 버전으로 크기 최소화)
FROM openjdk:17-jdk-slim

# 작성자 정보
LABEL maintainer="developer@example.com"

# 환경 변수 설정
ENV APP_HOME=/app
ENV JAVA_OPTS="-Xmx512m"

# 작업 디렉토리 생성 및 이동
WORKDIR $APP_HOME

# JAR 파일 복사 (ARG로 파일명 유연하게 처리)
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar

# 시간대 설정 (한국)
RUN ln -sf /usr/share/zoneinfo/Asia/Seoul /etc/localtime && \\
    echo "Asia/Seoul" > /etc/timezone

# 포트 노출
EXPOSE 8080

# 실행 명령어 (JAVA_OPTS로 메모리 옵션 추가 가능)
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

5.3. 이미지 빌드 및 실행

# 이미지 빌드
docker build -t spring-app:1.0 .

# 태그 추가
docker tag spring-app:1.0 spring-app:latest

# 빌드 확인
docker images | grep spring-app

# 컨테이너 실행
docker run -d \\
  --name my-spring-app \\
  -p 8080:8080 \\
  -e JAVA_OPTS="-Xmx256m" \\
  spring-app:1.0

# 로그 확인
docker logs my-spring-app

6. 실전 예제 2: Node.js 애플리케이션 Dockerize

6.1. 프로젝트 구조

my-node-app/
├── Dockerfile
├── package.json
├── package-lock.json
└── index.js

6.2. Dockerfile 작성

# 베이스 이미지 (알파인 버전으로 크기 최소화)
FROM node:18-alpine

# 앱 디렉토리 생성
WORKDIR /usr/src/app

# 의존성 설치 (package.json만 복사하여 레이어 캐싱 활용)
COPY package*.json ./
RUN npm ci --only=production

# 소스 코드 복사
COPY . .

# 일반 사용자로 실행 (보안)
USER node

# 포트 노출
EXPOSE 3000

# 실행 명령어
CMD ["node", "index.js"]

6.3. .dockerignore 파일 작성

.dockerignore 파일을 만들어 불필요한 파일이 이미지에 포함되지 않게 한다.

# .dockerignore
node_modules
npm-debug.log
.git
.env
Dockerfile
.dockerignore
README.md

6.4. 빌드 및 실행

# 이미지 빌드
docker build -t node-app:1.0 .

# 실행
docker run -d \\
  --name my-node-app \\
  -p 3000:3000 \\
  -e NODE_ENV=production \\
  node-app:1.0

7. 실전 예제 3: Python Flask 애플리케이션 Dockerize

7.1. 프로젝트 구조

my-flask-app/
├── Dockerfile
├── requirements.txt
├── app.py
└── templates/
    └── index.html

7.2. Dockerfile 작성

# 베이스 이미지
FROM python:3.9-slim

# 작업 디렉토리
WORKDIR /app

# 시스템 패키지 설치 (필요한 경우)
RUN apt-get update && apt-get install -y \\
    gcc \\
    && rm -rf /var/lib/apt/lists/*

# Python 의존성 설치
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

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

# 일반 사용자 생성 및 전환
RUN useradd -m myuser
USER myuser

# 포트 노출
EXPOSE 5000

# 실행 명령어
CMD ["python", "app.py"]

7.3. requirements.txt 예시

flask==2.2.3
gunicorn==20.1.0

7.4. 빌드 및 실행

# 빌드
docker build -t flask-app:1.0 .

# 실행 (gunicorn 사용)
docker run -d \\
  --name my-flask-app \\
  -p 5000:5000 \\
  flask-app:1.0 \\
  gunicorn --bind 0.0.0.0:5000 app:app

8. 멀티 스테이지 빌드 (Multi-stage Build)

8.1. 멀티 스테이지 빌드의 필요성

Spring Boot 애플리케이션을 예로 들어보자.

  • 빌드 단계: JDK, Gradle/Maven, 소스 코드 등이 필요하다. (크기: 수 GB)
  • 실행 단계: JRE와 빌드된 JAR 파일만 있으면 된다. (크기: 수백 MB)

멀티 스테이지 빌드를 사용하면 빌드 환경과 실행 환경을 분리하여 최종 이미지 크기를 최소화할 수 있다.

8.2. Spring Boot 멀티 스테이지 예제

# ---- 빌드 스테이지 ----
FROM maven:3.8-openjdk-17 AS builder

WORKDIR /build

# 의존성 먼저 복사 (레이어 캐싱 활용)
COPY pom.xml .
RUN mvn dependency:go-offline

# 소스 코드 복사 및 빌드
COPY src ./src
RUN mvn package -DskipTests

# ---- 실행 스테이지 ----
FROM openjdk:17-jre-slim

WORKDIR /app

# 빌드 스테이지에서 생성된 JAR 파일만 복사
COPY --from=builder /build/target/*.jar app.jar

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app.jar"]

8.3. Node.js 멀티 스테이지 예제

# ---- 빌드 스테이지 ----
FROM node:18 AS builder

WORKDIR /build

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

# ---- 실행 스테이지 ----
FROM nginx:alpine

# 빌드된 정적 파일만 복사
COPY --from=builder /build/dist /usr/share/nginx/html

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

8.4. 이미지 크기 비교

# 일반 빌드 (약 1.2GB)
docker build -f Dockerfile.single -t myapp:single .

# 멀티 스테이지 빌드 (약 300MB)
docker build -f Dockerfile.multistage -t myapp:multi .

# 크기 확인
docker images | grep myapp

9. 크로스 플랫폼 빌드 (buildx)

9.1. 문제 상황

# Apple Silicon(M1/M2) 맥북에서 이미지 빌드
docker build -t myapp:latest .

# 이 이미지를 Intel Linux 서버에서 실행하면?
docker run myapp:latest
# -> Exec format error!

도커는 기본적으로 이미지를 빌드한 시스템의 CPU 아키텍처에 맞춰 이미지를 생성한다. 따라서 Apple Silicon(ARM64)에서 만든 이미지는 Intel(AMD64) 서버에서 실행되지 않는다.

9.2. buildx로 멀티 아키텍처 이미지 빌드

# buildx 설정 확인
docker buildx ls

# 새로운 빌더 인스턴스 생성
docker buildx create --name mybuilder --use

# 부트스트랩
docker buildx inspect --bootstrap

# 멀티 아키텍처 이미지 빌드 및 푸시
docker buildx build \\
  --platform linux/amd64,linux/arm64 \\
  -t myusername/myapp:latest \\
  --push .

9.3. Dockerfile에서 플랫폼 고려

# 플랫폼에 따라 다른 베이스 이미지 사용
FROM --platform=$BUILDPLATFORM node:18 AS builder

# 빌드 타겟 플랫폼 정보
ARG TARGETPLATFORM
ARG BUILDPLATFORM

RUN echo "Building on $BUILDPLATFORM for $TARGETPLATFORM"

10. Dockerfile 작성 모범 사례

10.1. .dockerignore 활용

# .dockerignore
.git
node_modules
*.log
.env
Dockerfile
.dockerignore
README.md

10.2. 레이어 수 최소화

# ❌ 비효율적 (여러 레이어)
RUN apt-get update
RUN apt-get install -y package1
RUN apt-get install -y package2
RUN rm -rf /var/lib/apt/lists/*

# ✅ 효율적 (하나의 레이어)
RUN apt-get update && \\
    apt-get install -y package1 package2 && \\
    rm -rf /var/lib/apt/lists/*

10.3. 캐시 무효화 주의

# ❌ 매번 캐시 무효화 (항상 git clone 실행)
RUN git clone <https://github.com/myapp.git>

# ✅ 가능하면 COPY 사용
COPY . .

10.4. 특정 사용자로 실행

# ❌ root로 실행 (보안 위험)
FROM node:18
COPY . .
CMD ["node", "app.js"]

# ✅ 일반 사용자로 실행
FROM node:18
RUN useradd -m myuser
USER myuser
COPY --chown=myuser:myuser . .
CMD ["node", "app.js"]

10.5. 불필요한 패키지 제거

FROM python:3.9-slim  # slim 버전 사용

# 빌드 도구는 임시로 설치하고 제거
RUN apt-get update && \\
    apt-get install -y --no-install-recommends build-essential && \\
    pip install --no-cache-dir -r requirements.txt && \\
    apt-get purge -y build-essential && \\
    apt-get autoremove -y && \\
    rm -rf /var/lib/apt/lists/*

11. 실습: 간단한 웹앱 Dockerfile 작성해보기

11.1. 준비

간단한 "Hello World" 웹 서버를 만들어보자.

index.html

<!DOCTYPE html>
<html>
<head>
    <title>Docker Demo</title>
</head>
<body>
    <h1>Hello from Docker!</h1>
    <p>This is a custom image built with Dockerfile</p>
</body>
</html>

nginx.conf

server {
    listen 80;
    server_name localhost;

    location / {
        root /usr/share/nginx/html;
        index index.html;
    }
}

11.2. Dockerfile 작성

FROM nginx:alpine

# 작성자 정보
LABEL maintainer="demo@example.com"

# 설정 파일 복사
COPY nginx.conf /etc/nginx/conf.d/default.conf

# HTML 파일 복사
COPY index.html /usr/share/nginx/html/index.html

# 포트 노출
EXPOSE 80

# nginx 실행
CMD ["nginx", "-g", "daemon off;"]

11.3. 빌드 및 실행

# 이미지 빌드
docker build -t my-nginx:1.0 .

# 실행
docker run -d -p 8080:80 --name my-web my-nginx:1.0

# 확인
curl localhost:8080

브라우저에서 http://localhost:8080에 접속하면 작성한 HTML이 보인다.


12. Dockerfile 명령어 요약

명령어 설명  예시
FROM 베이스 이미지 지정 FROM ubuntu:20.04
LABEL 메타데이터 추가 LABEL version="1.0"
ENV 환경 변수 설정 ENV APP_HOME=/app
WORKDIR 작업 디렉토리 설정 WORKDIR /app
COPY 파일/디렉토리 복사 COPY . .
ADD 고급 복사 (압축 해제 등) ADD app.tar.gz /app
RUN 빌드 중 명령 실행 RUN apt-get update
EXPOSE 포트 문서화 EXPOSE 8080
ENTRYPOINT 고정 실행 명령어 ENTRYPOINT ["java", "-jar"]
CMD 기본 실행 명령어 CMD ["app.jar"]
USER 실행 사용자 변경 USER node
VOLUME 볼륨 마운트 지점 VOLUME /data
ARG 빌드 타임 변수 ARG VERSION=latest

13. 마치며

이번 포스팅에서는 Dockerfile을 작성하여 나만의 커스텀 이미지를 만드는 방법을 알아보았다.

  • Dockerfile은 이미지 생성을 위한 설계도다.
  • 주요 명령어: FROM, COPY, RUN, ENTRYPOINT, CMD 등
  • 레이어 캐싱을 고려한 최적화가 중요하다.
  • 멀티 스테이지 빌드로 이미지 크기 최소화가 가능하다.
  • buildx로 멀티 아키텍처 이미지를 빌드할 수 있다.

이제 여러분은 어떤 애플리케이션이든 Dockerfile로 이미지화할 수 있다. 다음 포스팅에서는 이렇게 만든 이미지를 Docker Hub에 배포하고 공유하는 방법을 알아보겠다.

'Docker' 카테고리의 다른 글

[BASIC #6] 컨테이너 간 통신: Docker Network 이해하기  (0) 2025.06.01
[BASIC #5] 데이터를 영구적으로 저장하는 법: Docker Volume  (0) 2025.05.31
[BASIC #3] 밀키트 비유로 완벽 이해하는 도커 이미지와 컨테이너  (0) 2025.05.31
[BASIC #2] 도커 설치부터 첫 컨테이너 실행까지 (Hello World 실습)  (0) 2025.05.31
[BASIC #1] VM과 비교하며 이해하는 도커(Docker)의 개념과 등장 배경  (0) 2025.05.31
'Docker' 카테고리의 다른 글
  • [BASIC #6] 컨테이너 간 통신: Docker Network 이해하기
  • [BASIC #5] 데이터를 영구적으로 저장하는 법: Docker Volume
  • [BASIC #3] 밀키트 비유로 완벽 이해하는 도커 이미지와 컨테이너
  • [BASIC #2] 도커 설치부터 첫 컨테이너 실행까지 (Hello World 실습)
h6bro
h6bro
백엔드 개발자의 기술 블로그
  • h6bro
    Jun's Tech Blog
    h6bro
  • 전체
    오늘
    어제
    • 분류 전체보기 (250) N
      • Java (18)
        • Core (9)
        • Design Pattern (9)
      • Spring (80)
        • Core (24)
        • MVC (6)
        • DB (10)
        • JPA (26)
        • Monitoring (3)
        • Security (11)
        • WebSocket (0)
      • Database (33)
        • Redis (15)
        • MySQL (18)
      • MSA (25) N
        • MSA 기본 (11)
        • MSA 아키텍처 (14) N
      • Kafka (30)
        • Core (18)
        • Connect (12)
      • ElasticSearch (11)
        • Search (11)
        • Logging (0)
      • Test (4)
        • k6 (4)
      • Docker (9)
      • CI&CD (10)
        • GitHub Actions (6)
        • ArgoCD (4)
      • Kubernetes (18)
        • Core (12)
        • Ops (6)
      • Cloud Engineering (4)
        • AWS Infrastructure (3)
        • AWS EKS (1)
        • Terraform (0)
      • Project (8)
        • LinkFolio (1)
        • Secondhand Market (7)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

    • Cloud Engineering 포스팅 정리
  • 인기 글

  • 태그

    ㅈ
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
h6bro
[BASIC #4] 나만의 이미지 만들기: Dockerfile 완전 정복
상단으로

티스토리툴바