본문으로 건너뛰기

커스텀 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로 주입합니다.