"내 컴퓨터에서는 되는데요?" — 개발자라면 한 번쯤 들어봤거나 해본 말일 겁니다. 환경 차이로 인한 버그는 매우 흔하고 해결하기도 어렵습니다. Docker는 애플리케이션과 그 실행 환경을 하나의 컨테이너로 패키징해 어디서든 동일하게 실행할 수 있게 해줍니다.

핵심 개념

  • 이미지(Image): 컨테이너를 만들기 위한 읽기 전용 템플릿. 레이어 구조로 효율적으로 관리됩니다.
  • 컨테이너(Container): 이미지를 실행한 인스턴스. 격리된 환경에서 동작합니다.
  • Dockerfile: 이미지를 만드는 설계도(명령어 모음).
  • 레지스트리(Registry): Docker Hub처럼 이미지를 저장하고 공유하는 저장소.

기본 명령어

# 이미지 다운로드
docker pull nginx

# 컨테이너 실행 (-d: 백그라운드, -p: 포트 매핑)
docker run -d -p 8080:80 --name my-nginx nginx

# 실행 중인 컨테이너 목록
docker ps

# 모든 컨테이너 (중지 포함)
docker ps -a

# 컨테이너 중지 / 시작 / 삭제
docker stop my-nginx
docker start my-nginx
docker rm my-nginx

# 로그 확인
docker logs -f my-nginx

# 컨테이너 내부 접속
docker exec -it my-nginx bash

Dockerfile 작성하기

# Node.js 앱 Dockerfile
FROM node:20-alpine          # 베이스 이미지 (경량 Alpine Linux)

WORKDIR /app                  # 작업 디렉토리 설정

# 의존성 먼저 복사 (캐시 최적화)
COPY package*.json ./
RUN npm install --production

# 소스 코드 복사
COPY . .

# 포트 노출 (문서용, 실제 바인딩은 docker run -p에서)
EXPOSE 3000

# 컨테이너 시작 명령
CMD ["node", "app.js"]
# 이미지 빌드
docker build -t my-app:1.0 .

# 실행
docker run -d -p 3000:3000 --name my-app my-app:1.0

.dockerignore

node_modules
.git
.env
*.log
README.md

docker-compose로 멀티 컨테이너 관리

# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DB_URL=mongodb://db:27017/mydb
    depends_on:
      - db
    volumes:
      - ./logs:/app/logs  # 로그 파일 호스트와 공유

  db:
    image: mongo:7
    ports:
      - "27017:27017"
    volumes:
      - mongo_data:/data/db  # 데이터 영속성

volumes:
  mongo_data:
# 모든 서비스 시작
docker-compose up -d

# 로그 확인
docker-compose logs -f app

# 특정 서비스 재시작
docker-compose restart app

# 전체 종료 및 볼륨 삭제
docker-compose down -v

멀티 스테이지 빌드 (최적화)

# 빌드 스테이지
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# 실행 스테이지 (빌드 결과물만 복사)
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
RUN npm install --production
CMD ["node", "dist/app.js"]

컨테이너 vs 가상 머신(VM) — 무엇이 다른가

Docker 컨테이너는 가상 머신(VM)과 혼동하기 쉽지만 근본적으로 다릅니다. 가상 머신은 하드웨어를 소프트웨어로 흉내 내고 그 위에 완전한 운영체제를 올립니다. 반면 Docker 컨테이너는 호스트 OS의 커널을 공유하며 프로세스 수준의 격리만 적용합니다.

이 차이 때문에 컨테이너는 부팅 시간이 수 초 이내로 매우 빠르고, 메모리와 디스크 사용량이 훨씬 적습니다. Node.js 앱 컨테이너 하나가 50MB 안팎인 반면, 동일한 앱을 올린 VM은 수 GB의 이미지를 필요로 합니다. 이런 이유로 마이크로서비스 아키텍처나 CI/CD 파이프라인에서 컨테이너가 표준이 됐습니다.

Docker 볼륨 — 데이터 영속성

컨테이너는 삭제되면 내부 데이터가 모두 사라집니다. 데이터베이스 파일, 업로드된 파일, 로그처럼 컨테이너가 재시작되어도 유지되어야 하는 데이터는 반드시 볼륨을 사용해야 합니다.

  • named volume — Docker가 관리하는 볼륨. mongo_data:/data/db 형태로 사용. 운영 환경에 적합합니다.
  • bind mount — 호스트의 특정 디렉토리를 컨테이너에 마운트. ./src:/app/src 형태로 사용. 개발 시 코드 변경을 즉시 반영할 때 편리합니다.
핵심 정리
Docker는 "환경 차이" 문제를 컨테이너로 해결합니다. 컨테이너는 VM보다 훨씬 가볍고 빠릅니다. Dockerfile로 이미지를 정의하고, docker-compose로 여러 서비스를 함께 관리하세요. 데이터베이스처럼 재시작 후에도 데이터를 유지해야 하는 서비스는 반드시 볼륨을 설정하고, 멀티 스테이지 빌드로 최종 이미지 크기를 최소화하세요.

Docker 네트워킹

Docker 컨테이너들은 기본적으로 서로 통신할 수 없습니다. docker-compose를 사용하면 같은 파일의 서비스들이 자동으로 같은 네트워크에 배치됩니다. 서비스 이름으로 통신할 수 있습니다.

services:
  web:
    build: .
    environment:
      # 컨테이너 이름(db)을 호스트명으로 직접 사용
      DATABASE_URL: postgresql://user:pass@db:5432/mydb
      REDIS_URL: redis://cache:6379

  db:
    image: postgres:16
    volumes:
      - pg_data:/var/lib/postgresql/data

  cache:
    image: redis:7-alpine

volumes:
  pg_data:

도커 네트워크에서는 서비스 이름이 DNS 이름이 됩니다. web 컨테이너에서 db로 접속하면 PostgreSQL 컨테이너로 연결됩니다. 외부에서는 포트 매핑(ports: - "5432:5432")을 설정한 서비스만 접근 가능합니다.

Docker 이미지 보안 강화

프로덕션 환경에서는 최소 권한 원칙을 적용해야 합니다.

# 1. 특정 버전 태그 사용 (latest는 위험)
FROM node:20.11-alpine

# 2. 비루트 사용자로 실행
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

# 3. 불필요한 패키지 설치 금지
# alpine 이미지는 최소한의 패키지만 포함

WORKDIR /app
COPY --chown=appuser:appgroup . .
RUN npm ci --production

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

이미지 취약점 스캔을 CI/CD에 포함하세요. docker scout cves my-image(Docker Scout) 또는 trivy image my-image(Trivy)로 알려진 CVE 취약점을 자동으로 검출할 수 있습니다.

컨테이너 리소스 제한

리소스 제한 없이 컨테이너를 실행하면 하나의 컨테이너가 서버 전체 메모리와 CPU를 독점할 수 있습니다.

services:
  web:
    image: myapp
    deploy:
      resources:
        limits:
          cpus: '0.50'     # CPU 50% 이하
          memory: 512M     # 메모리 512MB 이하
        reservations:
          cpus: '0.25'
          memory: 256M
    restart: unless-stopped   # 크래시 시 자동 재시작
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

Docker를 실무에 도입하면 달라지는 개발 문화

Docker를 도입한 팀에서 가장 먼저 변화를 느끼는 것은 "내 컴퓨터에서는 됩니다" 문제의 해소입니다. 컨테이너가 애플리케이션과 의존성을 함께 패키징하기 때문에, 개발 환경과 프로덕션 환경의 차이에서 비롯된 버그가 크게 줄어듭니다. 신규 팀원 온보딩도 빨라집니다. 기존에는 개발 환경 세팅에 반나절이 걸리던 것이, Docker와 docker-compose 파일이 갖춰진 프로젝트에서는 docker compose up 명령 하나로 모든 서비스가 실행됩니다. 데이터베이스, 캐시 서버, 메시지 큐까지 로컬에서 동일하게 띄울 수 있어 외부 서비스에 의존하지 않고도 개발할 수 있습니다.

배포 자동화와의 시너지도 큽니다. Docker 이미지가 한 번 빌드되면, CI/CD 파이프라인에서 그 이미지를 스테이징과 프로덕션에 순서대로 배포할 수 있습니다. 배포 단위가 이미지 태그로 명확하게 관리되어 롤백도 이전 버전 이미지를 다시 실행하는 것으로 간단하게 처리됩니다. 다만 Docker를 처음 도입할 때는 이미지 크기 관리, 볼륨 데이터 영속성, 네트워크 설정 같은 개념을 팀 전체가 이해해야 합니다. 무작정 도입했다가 이미지가 수 기가바이트가 되거나, 컨테이너가 재시작될 때 데이터가 사라지는 문제를 겪는 팀이 많습니다. 공식 문서와 실습을 통해 기본 개념을 다진 후 단계적으로 적용하는 것을 권장합니다.