커스텀 Nginx·Apache·Tomcat 이미지
Docker Hub의 공식 이미지는 범용성을 위해 최소한의 설정만 포함합니다. 실제 서비스에서는 자체 설정 파일, 애플리케이션 코드, 보안 강화 옵션을 포함한 커스텀 이미지 를 만들어야 합니다. Dockerfile로 이미지를 코드화하면 환경 재현이 보장되고, CI/CD 파이프라인과 자연스럽게 통합됩니다.
Dockerfile 기본 원칙
커스텀 이미지 작성 전에 핵심 원칙을 이해합니다.
| 원칙 | 설명 |
|---|---|
| 베이스 이미지 고정 | nginx:alpine 대신 nginx:1.25-alpine처럼 구체적 버전 지정 |
| alpine 기반 선호 | 일반 이미지 대비 크기 60~80% 절감, 공격 표면 최소화 |
| 레이어 최소화 | RUN 명령을 &&로 체이닝하여 레이어 수 감소 |
| 비루트 사용자 | USER 지시어로 root 외 사용자로 실행 |
.dockerignore | 빌드 컨텍스트에서 불필요한 파일 제외 |
.dockerignore 설정
빌드 컨텍스트에 포함할 파일을 제한하여 빌드 속도와 보안을 개선합니다.
# .dockerignore
# 버전 관리
.git/
.gitignore
# 빌드 산출물 (Java/Maven)
target/
*.class
# 빌드 산출물 (Node.js)
node_modules/
dist/
build/
# 환경 변수 및 비밀 (절대 이미지에 포함 금지)
.env
.env.*
*.key
*.pem
# 로그 파일
*.log
logs/
# IDE 설정
.idea/
.vscode/
*.iml
# OS 파일
.DS_Store
Thumbs.db
# 문서
*.md
docs/
Nginx 커스텀 이미지
Dockerfile
# nginx/Dockerfile
# 베이스: nginx:alpine (경량, 약 7MB)
FROM nginx:1.25-alpine
# 메타데이터
LABEL maintainer="devteam@example.com"
LABEL version="1.0"
LABEL description="Custom Nginx reverse proxy image"
# 기본 설정 파일 제거 후 커스텀 설정 복사
RUN rm /etc/nginx/conf.d/default.conf
# 커스텀 nginx.conf 복사
COPY nginx.conf /etc/nginx/nginx.conf
# 가상 호스트 설정 복사
COPY conf.d/ /etc/nginx/conf.d/
# 정적 파일 복사 (빌드된 프론트엔드 포함 시)
COPY --chown=nginx:nginx static/ /usr/share/nginx/html/static/
# SSL 인증서 디렉터리 생성 (볼륨 마운트 위치)
RUN mkdir -p /etc/nginx/ssl
# 로그 디렉터리 권한 설정
RUN chown -R nginx:nginx /var/log/nginx \
&& chmod 755 /var/log/nginx
# nginx 설정 검증
RUN nginx -t
# 비루트 사용자로 실행 (프로세스 포트 1024 이상만 사용 가능)
# nginx는 master(root) + worker(nginx) 구조로 운영됨
USER nginx
EXPOSE 80 443
# 헬스체크
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget -qO- http://localhost/health || exit 1
CMD ["nginx", "-g", "daemon off;"]
nginx.conf 예시 (COPY 대상)
# nginx.conf
user nginx;
worker_processes auto; # CPU 코어 수에 맞게 자동 설정
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
use epoll; # Linux 고성능 I/O
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# 로그 포맷 (JSON 형식으로 변경하면 로그 수집 용이)
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
gzip on;
gzip_types text/plain text/css application/json application/javascript;
include /etc/nginx/conf.d/*.conf;
}
Tomcat 커스텀 이미지
Dockerfile (WAR 배포 방식)
# tomcat/Dockerfile
FROM tomcat:10.1-jdk17-temurin-alpine
LABEL maintainer="devteam@example.com"
# 기본 웹앱 제거 (ROOT, examples, docs 등)
RUN rm -rf /usr/local/tomcat/webapps/*
# server.xml 커스텀 (커넥터 설정, 포트 변경 등)
COPY server.xml /usr/local/tomcat/conf/server.xml
# context.xml 복사 (DB 커넥션 풀 JNDI 설정 등)
COPY context.xml /usr/local/tomcat/conf/context.xml
# WAR 파일 복사 (빌드된 결과물)
COPY target/myapp.war /usr/local/tomcat/webapps/ROOT.war
# JVM 메모리 설정 및 GC 튜닝
ENV JAVA_OPTS="-server \
-Xms512m \
-Xmx1024m \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:+UseContainerSupport \
-Djava.security.egd=file:/dev/./urandom \
-Dfile.encoding=UTF-8 \
-Duser.timezone=Asia/Seoul"
# catalina.sh 환경 변수 설정
ENV CATALINA_OPTS="-Dspring.profiles.active=prod"
# 로그 디렉터리
VOLUME /usr/local/tomcat/logs
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=10s --retries=3 --start-period=60s \
CMD curl -f http://localhost:8080/ || exit 1
CMD ["catalina.sh", "run"]
Spring Boot 멀티 스테이지 빌드
멀티 스테이지 빌드는 빌드 환경(JDK + Maven/Gradle)과 실행 환경(JRE)을 분리하여 최종 이미지 크기를 획기적으로 줄입니다.
Maven 기반 멀티 스테이지 빌드
# Dockerfile
# ══════════════════════════════════════════════════════
# 스테이지 1: 빌드 (Maven + JDK)
# ══════════════════════════════════════════════════════
FROM maven:3.9-eclipse-temurin-17-alpine AS builder
WORKDIR /build
# 의존성 캐싱 최적화: pom.xml 먼저 복사하여 소스 변경 시 재다운로드 방지
COPY pom.xml .
COPY .mvn/ .mvn/
RUN mvn dependency:go-offline -B # 의존성 미리 다운로드
# 소스 코드 복사 및 빌드
COPY src/ src/
RUN mvn package -DskipTests -B \
&& mv target/*.jar target/app.jar
# ══════════════════════════════════════════════════════
# 스테이지 2: JRE 레이어 분리 (jlink 활용)
# ══════════════════════════════════════════════════════
FROM eclipse-temurin:17-jdk-alpine AS runtime-builder
COPY --from=builder /build/target/app.jar app.jar
# Spring Boot 레이어드 JAR 분리 (레이어 캐싱 최적화)
RUN java -Djarmode=layertools -jar app.jar extract
# ══════════════════════════════════════════════════════
# 스테이지 3: 실행 이미지 (JRE만 포함)
# ══════════════════════════════════════════════════════
FROM eclipse-temurin:17-jre-alpine
# 보안: 비루트 사용자 생성
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
# Spring Boot 레이어드 JAR 복사 (변경이 드문 레이어 먼저)
COPY --from=runtime-builder --chown=appuser:appgroup dependencies/ ./
COPY --from=runtime-builder --chown=appuser:appgroup spring-boot-loader/ ./
COPY --from=runtime-builder --chown=appuser:appgroup snapshot-dependencies/ ./
COPY --from=runtime-builder --chown=appuser:appgroup application/ ./
# 로그 디렉터리 생성
RUN mkdir -p /app/logs && chown -R appuser:appgroup /app/logs
# 비루트 사용자로 실행
USER appuser
# JVM 옵션
ENV JAVA_OPTS="-XX:+UseContainerSupport \
-XX:MaxRAMPercentage=75.0 \
-XX:+UseG1GC \
-Dfile.encoding=UTF-8 \
-Duser.timezone=Asia/Seoul"
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=10s --retries=3 --start-period=90s \
CMD wget -qO- http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS org.springframework.boot.loader.launch.JarLauncher"]
Gradle 기반 멀티 스테이지 빌드
# Dockerfile.gradle
# 스테이지 1: 빌드
FROM gradle:8.5-jdk17-alpine AS builder
WORKDIR /build
# 의존성 캐싱 (Gradle wrapper + build 파일 먼저)
COPY gradlew .
COPY gradle/ gradle/
COPY build.gradle settings.gradle ./
RUN ./gradlew dependencies --no-daemon # 의존성 미리 해결
# 소스 복사 및 빌드
COPY src/ src/
RUN ./gradlew bootJar --no-daemon -x test \
&& mv build/libs/*.jar build/libs/app.jar
# 스테이지 2: 실행
FROM eclipse-temurin:17-jre-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --from=builder --chown=appuser:appgroup /build/build/libs/app.jar app.jar
USER appuser
ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
이미지 크기 비교
| 방식 | 이미지 크기 |
|---|---|
| JDK + 전체 JAR | ~600MB |
| JRE + 전체 JAR | ~250MB |
| JRE + 레이어드 JAR | ~250MB (캐싱 효율 ↑) |
| alpine JRE + 레이어드 JAR | ~180MB |
Apache 커스텀 이미지
# apache/Dockerfile
FROM httpd:2.4-alpine
LABEL maintainer="devteam@example.com"
# 커스텀 httpd.conf 복사
COPY httpd.conf /usr/local/apache2/conf/httpd.conf
# 추가 설정 파일 복사 (mod_proxy, SSL 등)
COPY conf.d/ /usr/local/apache2/conf/extra/
# 정적 파일 복사
COPY --chown=daemon:daemon htdocs/ /usr/local/apache2/htdocs/
# 로그 디렉터리 권한
RUN chown -R daemon:daemon /usr/local/apache2/logs
# 설정 검증
RUN httpd -t
EXPOSE 80 443
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget -qO- http://localhost/ || exit 1
CMD ["httpd-foreground"]
httpd.conf 주요 모듈 활성화
# httpd.conf - mod_proxy 기반 Tomcat 연동 설정
# 필수 모듈 활성화
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so
LoadModule proxy_ajp_module modules/mod_proxy_ajp.so
LoadModule headers_module modules/mod_headers.so
LoadModule rewrite_module modules/mod_rewrite.so
ServerName example.com
# 프록시 설정
<VirtualHost *:80>
ServerName example.com
# Tomcat으로 리버스 프록시
ProxyPreserveHost On
ProxyPass / http://app:8080/
ProxyPassReverse / http://app:8080/
# 프록시 헤더 전달
RequestHeader set X-Forwarded-Proto "http"
RequestHeader set X-Forwarded-Port "80"
ErrorLog /usr/local/apache2/logs/error.log
CustomLog /usr/local/apache2/logs/access.log combined
</VirtualHost>
이미지 빌드 및 태깅
기본 빌드
# 현재 디렉터리 Dockerfile로 이미지 빌드
docker build -t myapp:1.0 .
# 특정 Dockerfile 지정
docker build -f Dockerfile.prod -t myapp:1.0-prod .
# 빌드 인수 전달
docker build --build-arg BUILD_VERSION=1.0.0 -t myapp:1.0 .
# 멀티 스테이지에서 특정 스테이지까지만 빌드 (디버깅용)
docker build --target builder -t myapp:builder .
# 캐시 없이 강제 재빌드
docker build --no-cache -t myapp:1.0 .
이미지 태깅 전략
# Semantic Versioning 태깅
docker build -t myapp:1.2.3 .
docker tag myapp:1.2.3 myapp:1.2
docker tag myapp:1.2.3 myapp:1
docker tag myapp:1.2.3 myapp:latest
# Git 커밋 해시 기반 태깅 (CI/CD 권장)
GIT_HASH=$(git rev-parse --short HEAD)
docker build -t myapp:${GIT_HASH} .
docker tag myapp:${GIT_HASH} myapp:latest
Docker Hub / 프라이빗 레지스트리 푸시
Docker Hub
# Docker Hub 로그인
docker login
# 이미지 태깅 (Hub 형식: 계정명/이미지명:태그)
docker tag myapp:1.0 username/myapp:1.0
docker tag myapp:1.0 username/myapp:latest
# 이미지 푸시
docker push username/myapp:1.0
docker push username/myapp:latest
# 이미지 풀
docker pull username/myapp:1.0
AWS ECR (Elastic Container Registry)
# AWS CLI로 ECR 로그인
aws ecr get-login-password --region ap-northeast-2 | \
docker login --username AWS --password-stdin \
123456789012.dkr.ecr.ap-northeast-2.amazonaws.com
# ECR 형식으로 태깅
ECR_URI=123456789012.dkr.ecr.ap-northeast-2.amazonaws.com
docker tag myapp:1.0 ${ECR_URI}/myapp:1.0
# 푸시
docker push ${ECR_URI}/myapp:1.0
자체 프라이빗 레지스트리 (Harbor / Registry)
# Docker Registry 컨테이너 실행 (로컬 테스트용)
docker run -d \
-p 5000:5000 \
--name registry \
-v registry-data:/var/lib/registry \
registry:2
# 로컬 레지스트리에 태깅 및 푸시
docker tag myapp:1.0 localhost:5000/myapp:1.0
docker push localhost:5000/myapp:1.0
# 프라이빗 레지스트리에서 풀
docker pull localhost:5000/myapp:1.0
docker-compose.yml에서 프라이빗 레지스트리 이미지 사용
services:
app:
image: 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/myapp:1.0
# build 섹션 없이 레지스트리 이미지 직접 사용
이미지 최적화 실전 요약
# 이미지 크기 확인
docker images myapp
# 레이어별 크기 분석
docker history myapp:1.0
# 이미지 내부 파일 시스템 검사 (dive 도구)
# docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock \
# wagoodman/dive:latest myapp:1.0
# 불필요한 이미지 정리
docker image prune -f
# 빌드 캐시 정리
docker builder prune -f
고수 팁
1. COPY vs ADD
ADD는 URL에서 파일을 다운로드하거나 tar 파일을 자동 압축 해제하는 기능이 있지만, 예측 불가능한 동작을 방지하기 위해 단순 파일 복사는 항상 COPY를 사용합니다.
2. --chown 플래그로 소유권 한 번에 설정
# ❌ 두 개 레이어 생성
COPY app.jar /app/
RUN chown appuser:appgroup /app/app.jar
# ✅ 한 개 레이어로 해결
COPY --chown=appuser:appgroup app.jar /app/
3. XX:+UseContainerSupport JVM 옵션
JDK 8u191, JDK 10 이후부터 컨테이너의 메모리 제한을 자동 인식합니다. -Xmx를 하드코딩하는 대신 -XX:MaxRAMPercentage=75.0을 사용하면 컨테이너 메모리 변경 시 자동 적응합니다.
4. 시크릿 빌드 인수 주의
# ❌ ARG로 전달된 비밀은 docker history에 노출됨
ARG DB_PASSWORD
ENV DB_PASS=${DB_PASSWORD}
# ✅ 런타임에 환경 변수로 주입 (docker-compose.yml의 environment 사용)
빌드 시점에 비밀 정보를 이미지에 넣으면 docker history로 노출됩니다. 모든 민감 정보는 런타임에 환경 변수 또는 Docker Secrets로 주입합니다.