본문으로 건너뛰기

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