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
이 한 줄이 의미하는 바:
- 컨테이너 내부에 /app 디렉토리를 생성한다.
- 이후 모든 명령어(COPY, RUN, CMD, ENTRYPOINT)는 이 /app 디렉토리에서 실행된다.
- 마치 터미널에서 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
이 한 줄이 하는 일:
- Dockerfile이 있는 디렉토리(빌드 컨텍스트)의 target/myapp.jar 파일을 찾는다.
- 이미지 내부의 현재 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
환경 변수 우선순위:
- docker run -e 로 전달한 값 (최우선)
- Dockerfile의 ENV로 설정한 값
- 베이스 이미지에서 설정한 값
실무 패턴:
# 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/*
이 한 줄이 하는 일:
- 패키지 목록을 업데이트한다.
- curl과 vim을 설치한다. (y는 확인 없이 설치)
- 설치 캐시를 지운다.
- 이미지 레이어로 저장된다.
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
이 코드의 의미:
- 기본적으로 build/libs/*.jar 파일을 찾아서 복사한다.
- 하지만 빌드할 때 -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에서 실행될 때
- EC2에 Docker가 설치되어 있다.
- docker build -t myapp . 명령어로 이미지를 빌드한다.
- docker run -d -p 8080:8080 myapp 실행
- 이미지 내부의 /app/app.jar 실행
- Spring Boot가 8080 포트로 실행됨
- 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 |
