Next.js 어플리케이션 컨테이너화하기
Next.js 어플리케이션 컨테이너화하기
이 가이드는 Docker로 Next.js 어플리케이션을 컨테이너화하고, 효율적이고 프로덕션 준비가 된 컨테이너를 만드는 모범 사례를 알려줘요.
출처: 문서
본문
이 가이드는 Docker를 사용해 Next.js 어플리케이션을 컨테이너화하는 방법을 보여주고, 효율적이고 프로덕션 준비가 된 컨테이너를 만드는 모범 사례를 따르도록 안내해요.
Next.js는 서버 사이드 렌더링, 정적 사이트 생성, 풀스택 기능을 지원하는 React 프레임워크예요. Docker는 개발부터 프로덕션까지 일관된 컨테이너화 환경을 제공해요.
감사의 말 (Acknowledgment)
Docker는 이 가이드를 집필하고 Next.js Docker examples를 Vercel Next.js 저장소에 기여해준 Kristiyan Velkov에게 진심으로 감사를 전해요(standalone과 export 출력 예제 포함). Docker Captain이자 경험 많은 엔지니어로서, Docker·DevOps·현대 웹 개발에 대한 그의 전문성 덕분에 이 자료가 커뮤니티에 매우 가치 있는 자료가 되고 있어요.
무엇을 배우게 될까요?
이 가이드에서 다음을 배우게 돼요:
- Docker를 사용해 Next.js 어플리케이션을 컨테이너화하고 실행하기.
- 컨테이너 안에서 Next.js용 로컬 개발 환경 설정하기.
- Docker 컨테이너 안에서 Next.js 어플리케이션 테스트 실행하기.
시작하려면 기존 Next.js 어플리케이션을 컨테이너화하는 것부터 해볼 거예요.
준비 사항 (Prerequisites)
시작하기 전에 다음에 익숙한지 확인해요:
- JavaScript 또는 TypeScript에 대한 기본 이해.
- 의존성 관리와 스크립트 실행을 위한 Node.js와 npm에 대한 기본 지식.
- React와 Next.js 기초에 대한 이해.
- 이미지, 컨테이너, Dockerfile 같은 Docker 개념에 대한 이해. Docker가 처음이라면 Docker basics 가이드부터 시작해요.
Next.js getting started 모듈을 완료하면, 이 가이드의 예제와 설명을 바탕으로 자신만의 Next.js 어플리케이션을 컨테이너화할 준비가 될 거예요.
Next.js 어플리케이션 컨테이너화하기
준비 사항 (Prerequisites)
시작하기 전에 다음 도구가 시스템에 설치·사용 가능한지 확인해요:
- 최신 버전의 Docker Desktop을 설치했어야 해요.
- git 클라이언트가 필요해요. 이 섹션의 예제는 명령줄 기반의 git 클라이언트를 사용하지만, 어떤 클라이언트를 써도 무방해요.
[!NOTE] Docker가 처음이라면 Docker basics 가이드를 시작해 이미지, 컨테이너, Dockerfile 같은 핵심 개념에 익숙해져 보세요.
개요 (Overview)
이 가이드는 Docker로 Next.js 어플리케이션을 컨테이너화하는 과정을 안내해요. 성능, 보안, 확장성, 배포 효율성을 높이는 모범 사례를 사용해 프로덕션 준비가 된 Docker 이미지를 만드는 방법을 배우게 돼요.
이 가이드를 끝내면 다음을 할 수 있게 돼요:
- Docker를 사용해 Next.js 어플리케이션을 컨테이너화하기.
- 프로덕션 빌드용 Dockerfile을 만들고 최적화하기.
- 다단계 빌드(multi-stage builds)를 사용해 이미지 크기 최소화하기.
- 효율적인 컨테이너화를 위해 Next.js standalone 또는 export 출력 활용하기.
- 안전하고 유지보수하기 쉬운 Docker 이미지를 만드는 모범 사례 따르기.
샘플 어플리케이션 가져오기
이 가이드에서 사용할 샘플 어플리케이션을 클론해요. 터미널을 열고 작업할 디렉토리로 이동한 다음, 다음 명령을 실행해 git 저장소를 클론해요:
$ git clone https://github.com/kristiyan-velkov/docker-nextjs-sample
Docker 이미지 빌드하기
Next.js에는 프로덕션 배포를 위한 특정 요구 사항이 있어요. 이 가이드는 두 가지 접근 방식을 보여줘요: standalone 출력(Node.js 서버)과 export 출력(Nginx로 정적 파일 제공).
[!TIP]
Gordon, Docker의 AI 어시스턴트가 프로젝트에 맞는 Docker 자산을 생성해줄 수 있어요. Gordon에게 어플리케이션에 맞춘 Dockerfile, Compose 파일,
.dockerignore를 만들어 달라고 요청해보세요.
1단계: Next.js 구성 및 Dockerfile 만들기
Dockerfile을 만들기 전에 베이스 이미지를 선택하세요: Node.js Official Image 또는 Hardened Image 카탈로그의 Docker Hardened Image (DHI). DHI를 선택하면 프로덕션 준비가 되고 가볍고 안전한 이미지를 얻을 수 있어요. 자세한 내용은 Docker Hardened Images를 참고해요.
[!IMPORTANT] 이 가이드는 작성 시점에 안전한 것으로 간주되는 안정적인 Node.js LTS 이미지 태그를 사용해요. 새 릴리스와 보안 패치가 정기적으로 배포되므로, 빌드·배포 전에 항상 공식 Node.js Docker 이미지를 검토하고 안전하고 최신 버전을 선택하세요.
1.1 standalone 출력을 사용하는 Next.js
standalone 출력(output: "standalone")은 어플리케이션 실행에 필요한 파일과 의존성만 포함하는 자체 완결형 출력을 Next.js가 만들게 해요. 하나의 node server.js로 앱을 서빙할 수 있으며, Docker에 이상적이고 서버 사이드 렌더링, API 라우트, 증분 정적 재생성(incremental static regeneration)을 지원해요. 자세한 내용은 Next.js output configuration documentation에서 확인하세요("standalone" 옵션 포함).
컨테이너는 3000 포트에서 Node.js로 Next.js 서버를 실행해요.
Next.js 구성 — 프로젝트 루트에서 next.config.ts를 열거나 만드세요:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
};
export default nextConfig;
Docker Hardened Image 또는 Docker Official Image 중 하나를 선택한 다음, 선택한 탭의 내용으로 Dockerfile을 만드세요.
Docker Hardened Images 사용하기
Docker Hardened Images (DHI)는 Docker Hardened Images catalog의 Node.js용으로 제공돼요. 자세한 내용은 DHI quickstart 가이드를 참고해요.
- DHI 레지스트리에 로그인해요:
$ docker login dhi.io
- Node.js DHI를 pull해요 (사용 가능한 버전은 카탈로그에서 확인하세요):
$ docker pull dhi.io/node:24-alpine3.22-dev
Dockerfile이라는 파일을 다음 내용으로 만들어요.FROM지시문은dhi.io/node:24-alpine3.22-dev를 사용해요. 최신 버전은 Docker Hardened Images catalog에서 확인하고, 보안·호환성을 위해 필요에 따라 이미지 태그를 업데이트하세요.
# ============================================
# Stage 1: Dependencies Installation Stage
# ============================================
# IMPORTANT: Docker Hardened Image (DHI) Version Maintenance
# This Dockerfile uses dhi.io/node. Regularly validate and update to the latest DHI versions in the catalog for security and compatibility.
FROM dhi.io/node:24-alpine3.22-dev AS dependencies
# Set working directory
WORKDIR /app
# Copy package-related files first to leverage Docker's caching mechanism
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./
# Install project dependencies with frozen lockfile for reproducible builds
RUN --mount=type=cache,target=/root/.npm \
--mount=type=cache,target=/usr/local/share/.cache/yarn \
--mount=type=cache,target=/root/.local/share/pnpm/store \
if [ -f package-lock.json ]; then \
npm ci --no-audit --no-fund; \
elif [ -f yarn.lock ]; then \
corepack enable yarn && yarn install --frozen-lockfile --production=false; \
elif [ -f pnpm-lock.yaml ]; then \
corepack enable pnpm && pnpm install --frozen-lockfile; \
else \
echo "No lockfile found." && exit 1; \
fi
# ============================================
# Stage 2: Build Next.js application in standalone mode
# ============================================
FROM dhi.io/node:24-alpine3.22-dev AS builder
# Set working directory
WORKDIR /app
# Copy project dependencies from dependencies stage
COPY --from=dependencies /app/node_modules ./node_modules
# Copy application source code
COPY . .
ENV NODE_ENV=production
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED=1
# Build Next.js application
# If you want to speed up Docker rebuilds, you can cache the build artifacts
# by adding: --mount=type=cache,target=/app/.next/cache
# This caches the .next/cache directory across builds, but it also prevents
# .next/cache/fetch-cache from being included in the final image, meaning
# cached fetch responses from the build won't be available at runtime.
RUN if [ -f package-lock.json ]; then \
npm run build; \
elif [ -f yarn.lock ]; then \
corepack enable yarn && yarn build; \
elif [ -f pnpm-lock.yaml ]; then \
corepack enable pnpm && pnpm build; \
else \
echo "No lockfile found." && exit 1; \
fi
# ============================================
# Stage 3: Run Next.js application
# ============================================
FROM dhi.io/node:24-alpine3.22-dev AS runner
# Set working directory
WORKDIR /app
# Set production environment variables
ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the run time.
# ENV NEXT_TELEMETRY_DISABLED=1
# Copy production assets
COPY --from=builder --chown=node:node /app/public ./public
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown node:node .next
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=node:node /app/.next/standalone ./
COPY --from=builder --chown=node:node /app/.next/static ./.next/static
# If you want to persist the fetch cache generated during the build so that
# cached responses are available immediately on startup, uncomment this line:
# COPY --from=builder --chown=node:node /app/.next/cache ./.next/cache
# Switch to non-root user for security best practices
USER node
# Expose port 3000 to allow HTTP traffic
EXPOSE 3000
# Start Next.js standalone server
CMD ["node", "server.js"]
Docker Official Image 사용하기
Dockerfile이라는 파일을 다음 내용으로 만들어요 (node 사용):
# ============================================
# Stage 1: Dependencies Installation Stage
# ============================================
ARG NODE_VERSION=24.14.0-slim
FROM node:${NODE_VERSION} AS dependencies
# Set working directory
WORKDIR /app
# Copy package-related files first to leverage Docker's caching mechanism
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./
# Install project dependencies with frozen lockfile for reproducible builds
RUN --mount=type=cache,target=/root/.npm \
--mount=type=cache,target=/usr/local/share/.cache/yarn \
--mount=type=cache,target=/root/.local/share/pnpm/store \
if [ -f package-lock.json ]; then \
npm ci --no-audit --no-fund; \
elif [ -f yarn.lock ]; then \
corepack enable yarn && yarn install --frozen-lockfile --production=false; \
elif [ -f pnpm-lock.yaml ]; then \
corepack enable pnpm && pnpm install --frozen-lockfile; \
else \
echo "No lockfile found." && exit 1; \
fi
# ============================================
# Stage 2: Build Next.js application in standalone mode
# ============================================
FROM node:${NODE_VERSION} AS builder
# Set working directory
WORKDIR /app
# Copy project dependencies from dependencies stage
COPY --from=dependencies /app/node_modules ./node_modules
# Copy application source code
COPY . .
ENV NODE_ENV=production
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED=1
# Build Next.js application
# If you want to speed up Docker rebuilds, you can cache the build artifacts
# by adding: --mount=type=cache,target=/app/.next/cache
# This caches the .next/cache directory across builds, but it also prevents
# .next/cache/fetch-cache from being included in the final image, meaning
# cached fetch responses from the build won't be available at runtime.
RUN if [ -f package-lock.json ]; then \
npm run build; \
elif [ -f yarn.lock ]; then \
corepack enable yarn && yarn build; \
elif [ -f pnpm-lock.yaml ]; then \
corepack enable pnpm && pnpm build; \
else \
echo "No lockfile found." && exit 1; \
fi
# ============================================
# Stage 3: Run Next.js application
# ============================================
FROM node:${NODE_VERSION} AS runner
# Set working directory
WORKDIR /app
# Set production environment variables
ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the run time.
# ENV NEXT_TELEMETRY_DISABLED=1
# Copy production assets
COPY --from=builder --chown=node:node /app/public ./public
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown node:node .next
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=node:node /app/.next/standalone ./
COPY --from=builder --chown=node:node /app/.next/static ./.next/static
# If you want to persist the fetch cache generated during the build so that
# cached responses are available immediately on startup, uncomment this line:
# COPY --from=builder --chown=node:node /app/.next/cache ./.next/cache
# Switch to non-root user for security best practices
USER node
# Expose port 3000 to allow HTTP traffic
EXPOSE 3000
# Start Next.js standalone server
CMD ["node", "server.js"]
[!NOTE] 이 Dockerfile은
dependencies,builder,runner세 개의 스테이지를 사용해요. 최종 이미지는node server.js를 실행하고 3000 포트에서 수신 대기해요.
1.2 export 출력을 사용하는 Next.js
출력 export(output: "export")는 빌드 시점에 완전한 정적 사이트를 만들게 해요. HTML, CSS, JavaScript를 out 디렉토리에 생성하며, 이 디렉토리는 런타임에 Node.js 서버 없이 어떤 정적 호스트나 CDN에서도 서빙될 수 있어요. 서버 사이드 렌더링이나 API 라우트가 필요 없을 때 사용하세요. 자세한 내용은 Next.js output configuration documentation에서 확인하세요.
Next.js 구성 — 프로젝트 루트에서 next.config.ts를 열고 다음 코드를 추가해요:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "export",
trailingSlash: true,
images: {
unoptimized: true,
},
};
export default nextConfig;
Docker Hardened Image 또는 Docker Official Image 중 하나를 선택한 다음, 선택한 탭의 내용으로 Dockerfile을 만드세요.
Docker Hardened Images 사용하기
Docker Hardened Images (DHI)는 Docker Hardened Images catalog의 Node.js와 Nginx용으로 제공돼요. 자세한 내용은 DHI quickstart 가이드를 참고해요.
- DHI 레지스트리에 로그인해요:
$ docker login dhi.io
- Node.js DHI를 pull해요 (사용 가능한 버전은 카탈로그에서 확인하세요):
$ docker pull dhi.io/node:24-alpine3.22-dev
- Nginx DHI를 pull해요 (사용 가능한 버전은 카탈로그에서 확인하세요):
$ docker pull dhi.io/nginx:1.28.0-alpine3.21-dev
Dockerfile이라는 파일을 다음 내용으로 만들어요.FROM지시문은 Docker Hardened Images(dhi.io/node:24-alpine3.22-dev와dhi.io/nginx:1.28.0-alpine3.21-dev)를 사용해요. 최신 버전은 Docker Hardened Images catalog에서 확인하고, 보안·호환성을 위해 필요에 따라 이미지 태그를 업데이트하세요.
# ============================================
# Stage 1: Dependencies Installation Stage
# ============================================
# IMPORTANT: Docker Hardened Image (DHI) Version Maintenance
# This Dockerfile uses dhi.io/node and dhi.io/nginx. Regularly validate and update to the latest DHI versions in the catalog for security and compatibility.
FROM dhi.io/node:24-alpine3.22-dev AS dependencies
# Set the working directory
WORKDIR /app
# Copy package-related files first to leverage Docker's caching mechanism
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./
# Install project dependencies with frozen lockfile for reproducible builds
RUN --mount=type=cache,target=/root/.npm \
--mount=type=cache,target=/usr/local/share/.cache/yarn \
--mount=type=cache,target=/root/.local/share/pnpm/store \
if [ -f package-lock.json ]; then \
npm ci --no-audit --no-fund; \
elif [ -f yarn.lock ]; then \
corepack enable yarn && yarn install --frozen-lockfile --production=false; \
elif [ -f pnpm-lock.yaml ]; then \
corepack enable pnpm && pnpm install --frozen-lockfile; \
else \
echo "No lockfile found." && exit 1; \
fi
# ============================================
# Stage 2: Build Next.js Application
# ============================================
FROM dhi.io/node:24-alpine3.22-dev AS builder
# Set the working directory
WORKDIR /app
# Copy project dependencies from dependencies stage
COPY --from=dependencies /app/node_modules ./node_modules
# Copy application source code
COPY . .
ENV NODE_ENV=production
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED=1
# Build Next.js application
RUN --mount=type=cache,target=/app/.next/cache \
if [ -f package-lock.json ]; then \
npm run build; \
elif [ -f yarn.lock ]; then \
corepack enable yarn && yarn build; \
elif [ -f pnpm-lock.yaml ]; then \
corepack enable pnpm && pnpm build; \
else \
echo "No lockfile found." && exit 1; \
fi
# =========================================
# Stage 3: Serve Static Files with Nginx
# =========================================
FROM dhi.io/nginx:1.28.0-alpine3.21-dev AS runner
# Set the working directory
WORKDIR /app
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the run time.
# ENV NEXT_TELEMETRY_DISABLED=1
# Copy custom Nginx config
COPY nginx.conf /etc/nginx/nginx.conf
# Copy the static build output from the build stage to Nginx's default HTML serving directory
COPY --chown=nginx:nginx --from=builder /app/out /usr/share/nginx/html
# Non-root user for security best practices
USER nginx
# Expose port 8080 to allow HTTP traffic
EXPOSE 8080
# Start Nginx directly with custom config
ENTRYPOINT ["nginx", "-c", "/etc/nginx/nginx.conf"]
CMD ["-g", "daemon off;"]
Docker Official Image 사용하기
Dockerfile이라는 파일을 다음 내용으로 만들어요 (node와 nginxinc/nginx-unprivileged 사용):
# ============================================
# Stage 1: Dependencies Installation Stage
# ============================================
ARG NODE_VERSION=24.14.0-slim
ARG NGINXINC_IMAGE_TAG=alpine3.22
FROM node:${NODE_VERSION} AS dependencies
# Set the working directory
WORKDIR /app
# Copy package-related files first to leverage Docker's caching mechanism
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./
# Install project dependencies with frozen lockfile for reproducible builds
RUN --mount=type=cache,target=/root/.npm \
--mount=type=cache,target=/usr/local/share/.cache/yarn \
--mount=type=cache,target=/root/.local/share/pnpm/store \
if [ -f package-lock.json ]; then \
npm ci --no-audit --no-fund; \
elif [ -f yarn.lock ]; then \
corepack enable yarn && yarn install --frozen-lockfile --production=false; \
elif [ -f pnpm-lock.yaml ]; then \
corepack enable pnpm && pnpm install --frozen-lockfile; \
else \
echo "No lockfile found." && exit 1; \
fi
# ============================================
# Stage 2: Build Next.js Application
# ============================================
FROM node:${NODE_VERSION} AS builder
# Set the working directory
WORKDIR /app
# Copy project dependencies from dependencies stage
COPY --from=dependencies /app/node_modules ./node_modules
# Copy application source code
COPY . .
ENV NODE_ENV=production
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED=1
# Build Next.js application
RUN --mount=type=cache,target=/app/.next/cache \
if [ -f package-lock.json ]; then \
npm run build; \
elif [ -f yarn.lock ]; then \
corepack enable yarn && yarn build; \
elif [ -f pnpm-lock.yaml ]; then \
corepack enable pnpm && pnpm build; \
else \
echo "No lockfile found." && exit 1; \
fi
# =========================================
# Stage 3: Serve Static Files with Nginx
# =========================================
FROM nginxinc/nginx-unprivileged:${NGINXINC_IMAGE_TAG} AS runner
# Set the working directory
WORKDIR /app
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the run time.
# ENV NEXT_TELEMETRY_DISABLED=1
# Copy custom Nginx config
COPY nginx.conf /etc/nginx/nginx.conf
# Copy the static build output from the build stage to Nginx's default HTML serving directory
COPY --from=builder /app/out /usr/share/nginx/html
# Non-root user for security best practices
USER nginx
# Expose port 8080 to allow HTTP traffic
EXPOSE 8080
# Start Nginx directly with custom config
ENTRYPOINT ["nginx", "-c", "/etc/nginx/nginx.conf"]
CMD ["-g", "daemon off;"]
[!NOTE] 이 가이드는 보안 모범 사례를 따라 비루트(non-root) 사용자로 실행하기 위해 표준 Nginx 이미지 대신 nginx-unprivileged를 사용해요.
nginx.conf만들기 (export 출력에서만 필요) — 프로젝트 루트에nginx.conf라는 파일을 만들어요:
# Minimal Nginx config for static Next.js app
worker_processes 1;
# Store PID in /tmp (always writable)
pid /tmp/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Disable logging to avoid permission issues
access_log off;
error_log /dev/stderr;
# Optimize static file serving
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
# Gzip compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
gzip_min_length 256;
server {
listen 8080;
server_name localhost;
# Serve static files
root /usr/share/nginx/html;
index index.html;
# Handle Next.js static export routing
# See: https://nextjs.org/docs/app/guides/static-exports#deploying
location / {
try_files $uri $uri.html $uri/ =404;
}
# This is necessary when `trailingSlash: false` (default).
# You can omit this when `trailingSlash: true` in next.config.
# Handles nested routes like /blog/post -> /blog/post.html
location ~ ^/(.+)/$ {
rewrite ^/(.+)/$ /$1.html break;
}
# Serve Next.js static assets
location ~ ^/_next/ {
try_files $uri =404;
expires 1y;
add_header Cache-Control "public, immutable";
}
# Optional 404 handling
error_page 404 /404.html;
location = /404.html {
internal;
}
}
}
[!NOTE] export는 8080 포트를 사용해요. 자세한 내용은 Next.js output configuration과 Nginx 문서를 참고해요.
2단계: compose.yaml 파일 만들기
compose.yaml이라는 파일을 다음 내용으로 만들어요:
services:
server:
build:
context: .
ports:
- 3000:3000
[!NOTE] export 출력(Nginx)을 사용한다면 포트 매핑을
8080:8080으로 바꾸세요.
3단계: .dockerignore 파일 만들기
.dockerignore 파일은 이미지를 빌드할 때 어떤 파일과 폴더를 제외할지 Docker에 알려줘요.
[!NOTE] 이것은 다음과 같은 도움이 돼요:
- 이미지 크기 줄이기
- 빌드 과정 가속화하기
- 민감하거나 불필요한 파일(예:
.env,.git,node_modules)이 최종 이미지에 추가되는 것을 방지하기자세히 알아보려면 .dockerignore reference를 참고해요.
dockerignore이라는 파일을 다음 내용으로 만들어요:
# Dependencies (installed inside the image, never copy from host)
node_modules/
.pnp/
.pnp.js
.pnpm-store/
# Next.js build output (generated during the image build)
.next/
out/
dist/
build/
.vercel/
# Testing (not needed in the production image)
coverage/
.nyc_output/
__tests__/
__mocks__/
jest/
cypress/
playwright-report/
test-results/
.vitest/
# Environment files (avoid leaking secrets into the build context)
.env
.env*
.env.local
.env.development.local
.env.test.local
.env.production.local
# Debug and log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
*.log
# IDE and editor files
.vscode/
.idea/
.cursor/
.cursorrules
.copilot/
*.swp
*.swo
*~
# Git
.git/
.gitignore
.gitattributes
# Docker files (reduce build context; not needed inside the image)
Dockerfile*
.dockerignore
docker-compose*.yml
compose*.yaml
# Documentation (not needed in the image)
*.md
docs/
# CI/CD (not needed in the image)
.github/
.gitlab-ci.yml
.travis.yml
.circleci/
Jenkinsfile
# TypeScript and build metadata
*.tsbuildinfo
# Cache and temporary directories
.cache/
.parcel-cache/
.eslintcache
.stylelintcache
.turbo/
.tmp/
.temp/
# Sensitive or dev-only config (optional; omit if your build needs these)
.pem
.editorconfig
.prettierrc*
.eslintrc*
.stylelintrc*
.babelrc*
*.iml
# OS-specific files
.DS_Store
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
Desktop.ini
4단계: Next.js 어플리케이션 이미지 빌드하기
커스텀 구성을 준비했으니 이제 Docker 이미지를 빌드할 준비가 됐어요. 1단계에서 만든 Dockerfile(standalone 또는 export)을 사용하세요.
이 구성에는 다음이 포함돼요:
- 최적화된 이미지 크기를 위한 다단계 빌드
- Standalone: 3000 포트의 Node.js 서버; Export: 8080 포트에서 정적 파일을 서빙하는 Nginx
- 보안 강화를 위한 비루트 사용자
- 올바른 파일 권한과 소유권
이전 단계를 완료한 후 프로젝트 디렉토리에는 최소한 다음 파일들이 있어야 해요 (export는 nginx.conf도 필요해요):
├── docker-nextjs-sample/
│ ├── Dockerfile
│ ├── .dockerignore
│ ├── compose.yaml
│ └── next.config.ts
이제 Dockerfile이 구성됐으니 Next.js 어플리케이션용 Docker 이미지를 빌드할 수 있어요.
[!NOTE]
docker build명령은 Dockerfile의 지시사항을 사용해 어플리케이션을 이미지로 패키징해요. 현재 디렉토리(이걸 build context라고 해요)에서 필요한 모든 파일을 포함해요.
프로젝트 루트에서 다음 명령을 실행해요:
$ docker build --tag nextjs-sample .
이 명령이 하는 일:
- 현재 디렉토리(.)의 Dockerfile을 사용해요
- 어플리케이션과 그 의존성을 Docker 이미지로 패키징해요
- 이미지를 nextjs-sample로 태그해서 나중에 참조할 수 있게 해요
5단계: 로컬 이미지 확인하기
Docker 이미지를 빌드한 후 Docker CLI나 Docker Desktop으로 로컬 머신에서 사용 가능한 이미지를 확인할 수 있어요. 이미 터미널에서 작업 중이므로 Docker CLI를 사용해보죠.
로컬에서 사용 가능한 모든 Docker 이미지를 나열하려면 다음 명령을 실행해요:
$ docker images
예시 출력:
REPOSITORY TAG IMAGE ID CREATED SIZE
nextjs-sample latest 8c5fc80f098e 14 seconds ago 130MB
이 출력은 이미지에 대한 주요 세부 정보를 제공해요:
- Repository – 이미지에 부여된 이름.
- Tag – 다른 빌드를 식별하는 데 도움을 주는 버전 라벨 (예: latest).
- Image ID – 이미지의 고유 식별자.
- Created – 이미지가 빌드된 시점을 나타내는 타임스탬프.
- Size – 이미지가 사용하는 전체 디스크 공간.
빌드에 성공했다면 nextjs-sample 이미지가 나열된 것을 볼 수 있어요.
컨테이너화한 어플리케이션 실행하기
이전 단계에서 Next.js 어플리케이션용 Dockerfile을 만들고 docker build 명령으로 Docker 이미지를 빌드했어요. 이제 그 이미지를 컨테이너에서 실행하고 어플리케이션이 예상대로 동작하는지 확인할 차례예요.
터미널에서 다음 명령을 실행해요. 구성에 맞는 포트를 사용하세요: standalone은 3000 포트, export는 8080 포트를 사용해요.
$ docker run -p 3000:3000 nextjs-sample
export 출력이라면 대신 8080 포트를 사용해요:
$ docker run -p 8080:8080 nextjs-sample
브라우저를 열고 어플리케이션을 확인하세요: standalone은 http://localhost:3000, export는 http://localhost:8080. Next.js 웹 어플리케이션을 볼 수 있어요.
터미널에서 ctrl+c를 눌러 어플리케이션을 중지해요.
어플리케이션을 백그라운드로 실행하기
-d 옵션을 추가하면 터미널에서 분리된 상태로 어플리케이션을 실행할 수 있고, --name으로 컨테이너에 이름을 주면 나중에 중지할 수 있어요:
$ docker run -d -p 3000:3000 --name nextjs-app nextjs-sample
export 출력이라면 8080 포트를 사용해요:
$ docker run -d -p 8080:8080 --name nextjs-app nextjs-sample
브라우저를 열고 어플리케이션을 확인하세요: standalone은 http://localhost:3000, export는 http://localhost:8080. 웹 어플리케이션을 볼 수 있어요.
컨테이너가 실행 중인지 확인하려면 docker ps 명령을 사용해요:
$ docker ps
이 명령은 포트, 이름, 상태와 함께 모든 활성 컨테이너를 나열해요. 3000(standalone) 또는 8080(export) 포트를 노출하는 컨테이너를 찾아보세요.
예시 출력:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f49b74736a9d nextjs-sample "node server.js" About a minute ago Up About a minute 0.0.0.0:3000->3000/tcp nextjs-app
어플리케이션을 중지하려면 다음을 실행해요:
$ docker stop nextjs-app
[!NOTE] 컨테이너 실행에 대한 자세한 내용은
docker runCLI reference와docker stopCLI reference를 참고해요.
Next.js 개발에 컨테이너 사용하기
준비 사항 (Prerequisites)
Next.js 어플리케이션 컨테이너화를 완료해요.
개요 (Overview)
이 섹션에서는 Docker Compose를 사용해 컨테이너화한 Next.js 어플리케이션을 위한 프로덕션 환경과 개발 환경을 모두 구성하는 방법을 배워요. 이 구성으로 standalone 서버를 사용해 프로덕션 빌드를 실행하고, Compose Watch와 함께 Next.js의 내장 핫 리로딩을 사용해 컨테이너 안에서 효율적으로 개발할 수 있어요.
다음을 배우게 돼요:
- 프로덕션용과 개발용으로 별도의 컨테이너 구성하기
- 개발 중 Compose Watch를 사용해 자동 파일 동기화 활성화하기
- 수동 리빌드 없이 실시간으로 변경 사항을 디버그하고 라이브 미리보기하기
서비스 자동 업데이트 (개발 모드)
Compose Watch를 사용하면 소스 파일 변경 사항을 컨테이너화한 개발 환경으로 자동 동기화할 수 있어요. 컨테이너를 수동으로 재시작하거나 리빌드할 필요 없이 파일 변경 사항을 자동 동기화해요.
1단계: 개발용 Dockerfile 만들기
프로젝트 루트에 Dockerfile.dev라는 파일을 다음 내용으로 만들어요 (샘플 프로젝트와 일치):
# ============================================
# Development Dockerfile for Next.js
# ============================================
ARG NODE_VERSION=24.14.0-slim
FROM node:${NODE_VERSION} AS dev
WORKDIR /app
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./
RUN --mount=type=cache,target=/root/.npm \
--mount=type=cache,target=/usr/local/share/.cache/yarn \
--mount=type=cache,target=/root/.local/share/pnpm/store \
if [ -f package-lock.json ]; then \
npm ci --no-audit --no-fund; \
elif [ -f yarn.lock ]; then \
corepack enable yarn && yarn install --frozen-lockfile --production=false; \
elif [ -f pnpm-lock.yaml ]; then \
corepack enable pnpm && pnpm install --frozen-lockfile; \
else \
echo "No lockfile found." && exit 1; \
fi
COPY . .
ENV WATCHPACK_POLLING=true
ENV HOSTNAME="0.0.0.0"
RUN chown -R node:node /app
USER node
EXPOSE 3000
CMD ["sh", "-c", "if [ -f package-lock.json ]; then npm run dev; elif [ -f yarn.lock ]; then yarn dev; elif [ -f pnpm-lock.yaml ]; then pnpm dev; else npm run dev; fi"]
이 파일은 핫 모듈 교체와 함께 Next.js 앱을 위한 개발 환경을 구성하고 npm, yarn, pnpm을 지원해요.
2단계: compose.yaml 파일 업데이트하기
compose.yaml 파일을 열고 프로덕션용(nextjs-prod-standalone)과 개발용(nextjs-dev) 두 개의 서비스를 정의해요. 이것은 샘플 프로젝트 구조와 일치해요.
Next.js 어플리케이션의 구성 예시는 다음과 같아요:
services:
nextjs-prod-standalone:
build:
context: .
dockerfile: Dockerfile
image: nextjs-sample:prod
container_name: nextjs-sample-prod
ports:
- "3000:3000"
nextjs-dev:
build:
context: .
dockerfile: Dockerfile.dev
image: nextjs-sample:dev
container_name: nextjs-sample-dev
ports:
- "3000:3000"
environment:
- WATCHPACK_POLLING=true
develop:
watch:
- action: sync
path: .
target: /app
ignore:
- node_modules/
- .next/
- action: rebuild
path: package.json
nextjs-prod-standalone서비스는 standalone 출력을 사용해 프로덕션 Next.js 앱을 빌드하고 실행해요.nextjs-dev서비스는 핫 모듈 교체를 갖춘 Next.js 개발 서버를 실행해요.watch는 Compose Watch로 파일 동기화를 트리거해요.WATCHPACK_POLLING=true는 Docker 안에서 파일 변경이 올바르게 감지되도록 보장해요.package.json에 대한rebuild액션은 파일이 변경될 때 의존성이 다시 설치되도록 보장해요.
[!NOTE] 자세한 내용은 공식 가이드인 Use Compose Watch를 참고해요.
3단계: Docker 개발용 Next.js 구성하기
Next.js는 Docker 컨테이너 안에서 기본적으로 잘 동작하지만, 개발 경험을 개선할 수 있는 몇 가지 구성이 있어요.
컨테이너화할 때 만든 next.config.ts 파일에는 이미 프로덕션용 output: "standalone" 옵션이 포함돼 있어요. 개발에서는 Next.js가 자동으로 핫 리로딩이 활성화된 내장 개발 서버를 사용해요.
[!NOTE] Next.js 개발 서버가 자동으로:
- 즉각적인 업데이트를 위해 Hot Module Replacement (HMR) 활성화하기
- 파일 변경을 감시하고 자동으로 재컴파일하기
- 브라우저에서 상세한 오류 메시지 제공하기
compose 파일의
WATCHPACK_POLLING=true환경 변수는 Docker 컨테이너 안에서 파일 감시가 올바르게 동작하도록 보장해요.
이전 단계를 완료한 후 프로젝트 디렉토리에는 다음 파일들이 있어야 해요:
├── docker-nextjs-sample/
│ ├── Dockerfile
│ ├── Dockerfile.dev
│ ├── .dockerignore
│ ├── compose.yaml
│ └── next.config.ts
4단계: Compose Watch 시작하기
프로젝트 루트에서 다음 명령을 실행해 watch 모드로 컨테이너를 시작해요:
$ docker compose watch nextjs-dev
5단계: Next.js로 Compose Watch 테스트하기
Compose Watch가 올바르게 동작하는지 확인하려면:
-
텍스트 편집기에서
app/page.tsx파일을 열어요 (프로젝트가src디렉토리를 사용한다면src/app/page.tsx). -
메인 콘텐츠 영역을 찾아 수정할 텍스트 요소를 찾아요.
-
보이는 변경을 해보세요, 예를 들어 헤딩을 업데이트해요:
<h1>Hello from Docker Compose Watch!</h1>
-
파일을 저장해요.
-
브라우저에서 http://localhost:3000을 열어요.
컨테이너를 수동으로 리빌드할 필요 없이 업데이트된 텍스트가 즉시 나타나는 것을 볼 수 있어요. 이는 파일 감시와 자동 동기화가 예상대로 동작하고 있다는 뜻이에요.
컨테이너에서 Next.js 테스트 실행하기
준비 사항 (Prerequisites)
이 가이드의 이전 섹션을 전부 완료해요 — Next.js 어플리케이션 컨테이너화부터 시작해서요.
개요 (Overview)
테스트는 개발 과정의 중요한 부분이에요. 이 섹션에서 다음을 배우게 돼요:
- Docker 컨테이너 안에서 Vitest(또는 Jest)를 사용해 단위 테스트 실행하기.
- Docker 컨테이너 안에서 lint(예: ESLint) 실행하기.
- Docker Compose를 사용해 격리되고 재현 가능한 환경에서 테스트와 lint 실행하기.
샘플 프로젝트는 컴포넌트 테스트를 위해 Testing Library와 함께 Vitest를 사용해요. 같은 구성을 사용하거나 나중에 대안인 Jest 구성을 따라가도 돼요.
개발 중 테스트 실행하기
샘플 프로젝트에는 lint(ESLint)와 샘플 테스트(Vitest, app/page.test.tsx)가 이미 준비돼 있어요. 샘플 앱을 사용한다면 3단계: compose.yaml 업데이트하기로 건너뛰고 아래 명령으로 테스트나 lint를 실행하면 돼요. 자신만의 프로젝트를 사용한다면 패키지와 스크립트를 추가하도록 설치·구성 단계를 따라가세요.
샘플에는 다음 위치에 테스트 파일이 포함돼 있어요:
app/page.test.tsx
이 파일은 Vitest와 React Testing Library를 사용해 페이지 컴포넌트의 동작을 검증해요.
1단계: Vitest와 React Testing Library 설치하기 (커스텀 프로젝트)
커스텀 프로젝트를 사용 중이고 필요한 테스트 도구를 아직 추가하지 않았다면 다음을 실행해 설치해요:
$ npm install --save-dev vitest @vitejs/plugin-react @testing-library/react @testing-library/dom jsdom
그런 다음 package.json 파일의 scripts 섹션을 업데이트해 다음을 포함시켜요:
"scripts": {
"test": "vitest",
"test:run": "vitest run"
}
lint용으로 lint 스크립트를 추가해요(그리고 선택적으로 lint:fix). 예를 들어 ESLint를 사용한다면:
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
}
샘플 프로젝트는 Next.js용 eslint와 eslint-config-next를 사용해요. 커스텀 프로젝트에 다음으로 설치해요:
$ npm install --save-dev eslint eslint-config-next @eslint/eslintrc
프로젝트 루트에 ESLint 구성 파일(예: eslint.config.cjs)을 Next.js 규칙과 전역 무시(global ignores)로 만들어요:
const { defineConfig, globalIgnores } = require("eslint/config");
const { FlatCompat } = require("@eslint/eslintrc");
const compat = new FlatCompat({ baseDirectory: __dirname });
module.exports = defineConfig([
...compat.extends(
"eslint-config-next/core-web-vitals",
"eslint-config-next/typescript",
),
globalIgnores([
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
"node_modules/**",
"eslint.config.cjs",
]),
]);
2단계: Vitest 구성하기 (커스텀 프로젝트)
커스텀 프로젝트를 사용한다면 프로젝트 루트에 vitest.config.ts 파일을 만들어요 (샘플 프로젝트와 일치):
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
test: {
environment: "jsdom",
setupFiles: "./vitest.setup.ts",
globals: true,
},
});
프로젝트 루트에 vitest.setup.ts 파일을 만들어요:
import "@testing-library/jest-dom/vitest";
[!NOTE] Vitest는 Next.js와 잘 동작하며 빠른 실행과 ESM 지원을 제공해요. 자세한 내용은 Next.js testing documentation과 Vitest docs를 참고해요.
3단계: compose.yaml 업데이트하기
compose.yaml 파일에 nextjs-test와 nextjs-lint 서비스를 추가해요. 샘플 프로젝트에서 이 서비스들은 tools 프로필을 사용해 일반적인 docker compose up으로는 시작되지 않아요. 둘 다 Dockerfile.dev를 재사용하고 테스트나 lint 명령을 실행해요:
services:
nextjs-prod-standalone:
build:
context: .
dockerfile: Dockerfile
image: nextjs-sample:prod
container_name: nextjs-sample-prod
ports:
- "3000:3000"
nextjs-dev:
build:
context: .
dockerfile: Dockerfile.dev
image: nextjs-sample:dev
container_name: nextjs-sample-dev
ports:
- "3000:3000"
environment:
- WATCHPACK_POLLING=true
develop:
watch:
- action: sync
path: .
target: /app
ignore:
- node_modules/
- .next/
- action: rebuild
path: package.json
nextjs-test:
build:
context: .
dockerfile: Dockerfile.dev
image: nextjs-sample:dev
container_name: nextjs-sample-test
command:
[
"sh",
"-c",
"if [ -f package-lock.json ]; then npm run test:run 2>/dev/null || npm run test -- --run; elif [ -f yarn.lock ]; then yarn test:run 2>/dev/null || yarn test --run; elif [ -f pnpm-lock.yaml ]; then pnpm run test:run; else npm run test -- --run; fi",
]
profiles:
- tools
nextjs-lint:
build:
context: .
dockerfile: Dockerfile.dev
image: nextjs-sample:dev
container_name: nextjs-sample-lint
command:
[
"sh",
"-c",
"if [ -f package-lock.json ]; then npm run lint; elif [ -f yarn.lock ]; then yarn lint; elif [ -f pnpm-lock.yaml ]; then pnpm lint; else npm run lint; fi",
]
profiles:
- tools
nextjs-test와 nextjs-lint 서비스는 개발에 사용된 것과 같은 Dockerfile.dev를 재사용하고 기본 명령을 테스트나 lint 실행으로 덮어써요. profiles: [tools]는 이 서비스들이 --profile tools 옵션을 사용할 때만 실행된다는 뜻이에요.
이전 단계를 완료한 후 프로젝트 디렉토리에는 다음이 있어야 해요:
├── docker-nextjs-sample/
│ ├── Dockerfile
│ ├── Dockerfile.dev
│ ├── .dockerignore
│ ├── compose.yaml
│ ├── vitest.config.ts
│ ├── vitest.setup.ts
│ └── next.config.ts
4단계: 테스트 실행하기
컨테이너 안에서 테스트 스위트를 실행하려면 프로젝트 루트에서 실행해요:
$ docker compose --profile tools run --rm nextjs-test
이 명령은:
nextjs-test서비스를 시작해요 (--profile tools덕분에).- 개발과 같은 환경에서 테스트 스크립트(
test:run또는test -- --run)를 실행해요. - 테스트가 완료된 후 컨테이너를 제거해요 (
docker compose run --rm).
[!NOTE] Compose 명령과 프로필에 대한 자세한 내용은 Compose CLI reference를 참고해요.
5단계: 컨테이너에서 lint 실행하기
컨테이너 안에서 linter(예: ESLint)를 실행하려면 같은 tools 프로필을 가진 nextjs-lint 서비스를 사용해요:
$ docker compose --profile tools run --rm nextjs-lint
이 명령은:
nextjs-lint서비스를 시작해요 (--profile tools덕분에).- 개발과 같은 환경에서 lint 스크립트(
npm run lint,yarn lint,pnpm lint— lockfile에 따라 다름)를 실행해요. - lint가 완료된 후 컨테이너를 제거해요.
package.json에 lint 스크립트가 포함되어 있는지 확인해요. 샘플 프로젝트에는 이미 "lint": "eslint ."와 "lint:fix": "eslint . --fix"가 있고, 커스텀 프로젝트라면 같은 것을 추가하고 필요 시 eslint와 eslint-config-next를 설치해요.
요약 (Summary)
이 섹션에서는 Vitest와 Docker Compose를 사용해 Docker 컨테이너 안에서 Next.js 어플리케이션의 단위 테스트를 실행하는 방법을 배웠어요.
달성한 것:
- Next.js 컴포넌트 테스트를 위해 Vitest와 React Testing Library를 설치하고 구성했습니다.
- 테스트와 lint 실행을 격리하기 위해
compose.yaml에nextjs-test와nextjs-lint서비스(tools프로필)를 만들었습니다. - dev, test, lint 환경 간 일관성을 보장하기 위해 개발용
Dockerfile.dev를 재사용했습니다. docker compose --profile tools run --rm nextjs-test를 사용해 컨테이너 안에서 테스트를 실행했습니다.docker compose --profile tools run --rm nextjs-lint를 사용해 컨테이너 안에서 lint를 실행했습니다.- 로컬 머신 설정에 의존하지 않고 다양한 환경에서 안정적이고 반복 가능한 테스트와 lint를 보장했습니다.
관련 자료 (Related resources)
Docker 테스트 워크플로를 향상시키려면 공식 레퍼런스와 모범 사례를 살펴보세요:
- Dockerfile reference – 모든 Dockerfile 지시문과 문법을 이해하기.
- Best practices for writing Dockerfiles – 효율적이고 유지보수하기 쉬우며 안전한 Dockerfile 작성하기.
- Compose file reference –
compose.yaml에서 서비스 구성에 사용 가능한 전체 문법과 옵션 배우기. docker compose runCLI reference – 서비스 컨테이너에서 일회성 명령 실행하기.- Next.js Testing Documentation – 공식 Next.js 테스트 가이드.