DevFinance
테크·8 min read

Docker 입문 완전 가이드 2026 — 개발자 필수 컨테이너 기초

Docker의 핵심 개념, Dockerfile 작성, Docker Compose 멀티 서비스 구성, Next.js multi-stage 빌드, 보안 모범 사례까지 개발자가 알아야 할 Docker 기초를 완전 정리했습니다.

"내 컴퓨터에서는 되는데..." — Docker가 해결합니다

Docker는 애플리케이션과 실행 환경을 컨테이너로 패키징해 어디서나 동일하게 실행할 수 있게 해주는 플랫폼입니다. 개발 환경의 불일치, 배포 환경 설정 복잡성 문제를 근본적으로 해결합니다.


핵심 개념

이미지 (Image)

실행 환경의 스냅샷. Dockerfile로 정의하며, Docker Hub 같은 레지스트리에 저장·공유합니다.

Node.js 공식 이미지 → 내 코드를 추가 → 내 앱 이미지

컨테이너 (Container)

이미지를 실행한 인스턴스. 격리된 프로세스로 호스트 시스템과 독립적입니다.

이미지 = 레시피
컨테이너 = 레시피로 만든 요리 (실행 중인 인스턴스)

레이어 캐싱

Dockerfile의 각 명령어는 레이어를 만듭니다. 변경된 레이어부터만 재빌드하므로 순서가 중요합니다.

# 변경이 적은 것 먼저 (캐시 활용 극대화)
COPY package*.json ./     # 패키지 목록 변경 시에만 재실행
RUN npm ci                # 위가 캐시 히트면 이것도 캐시
COPY . .                  # 소스 변경마다 재실행

Dockerfile 기초

Node.js 앱 기본 Dockerfile

FROM node:20-alpine

WORKDIR /app

# 패키지 파일 먼저 복사 (캐시 활용)
COPY package*.json ./
RUN npm ci --only=production

# 소스 코드 복사
COPY . .

RUN npm run build

EXPOSE 3000
CMD ["npm", "start"]

.dockerignore — 반드시 설정

node_modules
.next
.git
*.log
.env
.env.local
.env.*.local
*.md
coverage

.dockerignore 없이 COPY . . 하면 node_modules (수백 MB)가 컨테이너에 복사됩니다.


자주 쓰는 명령어

# 이미지 빌드 (-t: 태그/이름 지정)
docker build -t my-app .
docker build -t my-app:v1.2.3 .

# 컨테이너 실행
docker run -p 3000:3000 my-app            # 포어그라운드
docker run -d -p 3000:3000 my-app         # 백그라운드 (detached)
docker run -d -p 3000:3000 --name webapp my-app  # 이름 지정

# 환경 변수 전달
docker run -d -e NODE_ENV=production -e DATABASE_URL=... my-app

# 실행 중인 컨테이너 확인
docker ps
docker ps -a  # 중지된 것 포함

# 컨테이너 조작
docker stop webapp
docker start webapp
docker restart webapp
docker rm webapp       # 컨테이너 삭제

# 로그 확인
docker logs webapp
docker logs -f webapp  # 실시간 스트리밍

# 컨테이너 내부 진입
docker exec -it webapp sh
docker exec -it webapp bash  # bash 있는 이미지의 경우

# 이미지 관리
docker images
docker rmi my-app      # 이미지 삭제
docker image prune     # 사용하지 않는 이미지 정리

Docker Compose — 멀티 컨테이너 관리

앱 서버 + 데이터베이스 + 캐시 등 여러 서비스를 하나의 파일로 정의합니다.

기본 구성 (Next.js + PostgreSQL + Redis)

# docker-compose.yml
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/mydb
      - REDIS_URL=redis://cache:6379
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: mydb
    volumes:
      - pgdata:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
      interval: 10s
      timeout: 5s
      retries: 5

  cache:
    image: redis:7-alpine
    volumes:
      - redisdata:/data
    ports:
      - "6379:6379"

volumes:
  pgdata:
  redisdata:
# 전체 시작 (백그라운드)
docker compose up -d

# 특정 서비스만 시작
docker compose up -d db cache

# 실시간 로그
docker compose logs -f app

# 전체 중지 및 컨테이너 삭제
docker compose down

# 볼륨까지 삭제 (데이터 초기화)
docker compose down -v

# 재빌드 후 시작
docker compose up -d --build

개발 환경 오버라이드

# docker-compose.dev.yml (개발 전용 설정)
services:
  app:
    volumes:
      - .:/app                # 소스 코드 바인드 마운트
      - /app/node_modules     # node_modules는 컨테이너 것 사용
    command: npm run dev      # dev 서버로 실행
    environment:
      - NODE_ENV=development
# 개발 환경 실행
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d

Next.js Multi-stage Build (최적화)

빌드 도구와 소스 코드는 제외하고 실행에 필요한 것만 최종 이미지에 포함합니다.

# 1단계: 의존성 설치
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci

# 2단계: 빌드
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

# 3단계: 프로덕션 (최소 이미지)
FROM node:20-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production

# non-root 사용자 (보안)
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000
ENV PORT=3000

HEALTHCHECK --interval=30s --timeout=3s \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/ || exit 1

CMD ["node", "server.js"]

이미지 크기 비교:

단순 빌드:  ~1.2GB
multi-stage: ~180MB (85% 감소)

next.config.tsoutput: "standalone" 추가 필요:

const nextConfig = {
  output: "standalone",
};

보안 모범 사례

1. Non-root 유저 사용

RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

2. 시크릿 관리

# 나쁜 예 — 이미지 레이어에 시크릿이 남음
RUN export API_KEY=secret && npm run setup

# 좋은 예 — 환경 변수로 런타임에 전달
ENV API_KEY=""  # 기본값만, 실제 값은 실행 시 주입
# 실행 시 시크릿 전달
docker run -e API_KEY=${API_KEY} my-app

3. Healthcheck 추가

HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1

4. 최신 보안 패치 적용

# Alpine에서 최신 보안 패치 적용
RUN apk update && apk upgrade --no-cache

실무 팁

내용
Alpine 이미지node:20node:20-alpine (900MB → 130MB)
레이어 캐싱package.json 복사 → npm ci → 소스 복사 순서 유지
Non-root 유저보안을 위해 root가 아닌 전용 유저로 실행
.dockerignorenode_modules, .next, .git 반드시 제외
Healthcheck컨테이너 상태 모니터링 및 재시작 자동화
Multi-stage빌드 이미지와 실행 이미지 분리로 크기 최소화

자주 하는 실수

# 실수: 컨테이너 안에서 소스 수정
# → 컨테이너 재시작 시 초기화됨

# 올바른 방법: 바인드 마운트 사용 (개발 시)
docker run -v $(pwd):/app my-app

# 실수: 환경 변수를 Dockerfile에 하드코딩
ENV DATABASE_URL="postgresql://prod-server..."

# 올바른 방법: 실행 시 주입
docker run -e DATABASE_URL=${DATABASE_URL} my-app

관련 글: GitHub Actions CI/CD 설정법 · Vercel 무료 배포 완전 가이드 · Supabase 사이드프로젝트 시작하기

DF

DevFinance 편집팀

현직 소프트웨어 엔지니어가 운영합니다. IT 업계 종사자에게 필요한 재테크·기술 정보를 공식 데이터와 실무 경험을 바탕으로 정리합니다.

더 알아보기