Docker 완벽 가이드: 컨테이너 기반 개발 환경 구축

Docker 완벽 가이드: 컨테이너 기반 개발 환경 구축

이 글의 핵심

Docker는 애플리케이션을 컨테이너로 패키징하여 어디서나 동일하게 실행할 수 있게 하는 플랫폼입니다. VM보다 가볍고 빠르며, 이미지 레이어 시스템으로 효율적인 스토리지 관리가 가능합니다. Docker Compose로 멀티 컨테이너 애플리케이션을 쉽게 관리하고, 개발/프로덕션 환경을 일관되게 유지할 수 있습니다.

Docker란?

Docker는 애플리케이션을 컨테이너로 패키징하고 실행하는 오픈소스 플랫폼입니다. “Build once, Run anywhere” 철학으로 개발, 배포, 실행을 일관되게 만듭니다.

핵심 개념

  1. 컨테이너

    • 격리된 실행 환경
    • 애플리케이션 + 의존성
    • 이식 가능
  2. 이미지

    • 컨테이너 템플릿
    • Read-only 레이어
    • Docker Hub에서 공유
  3. 레지스트리

    • 이미지 저장소
    • Docker Hub (공개)
    • Private Registry
  4. 레이어

    • 이미지 구성 단위
    • 캐싱 및 재사용
    • 효율적인 저장

VM vs Container 비교

항목가상 머신 (VM)컨테이너 (Docker)
OS각 VM마다 전체 OS호스트 OS 커널 공유
크기GB 단위MB 단위
시작 시간분 단위초 단위
리소스많이 사용적게 사용
격리완전 격리프로세스 수준 격리
이식성제한적매우 높음
용도전체 시스템 가상화애플리케이션 격리
┌─────────────────────┐  ┌─────────────────────┐
│   가상 머신 (VM)     │  │  컨테이너 (Docker)  │
├─────────────────────┤  ├─────────────────────┤
│   App A │ App B     │  │  App A │ App B      │
│   ───────────────   │  │  ─────────────────  │
│   Bins/Libs         │  │  Bins/Libs          │
│   ───────────────   │  │  ─────────────────  │
│   Guest OS          │  │  Docker Engine      │
│   ───────────────   │  │  ─────────────────  │
│   Hypervisor        │  │  Host OS            │
│   ───────────────   │  │  ─────────────────  │
│   Host OS           │  │  Infrastructure     │
└─────────────────────┘  └─────────────────────┘

설치

Windows (WSL 2)

# Docker Desktop 다운로드 및 설치
# https://www.docker.com/products/docker-desktop

# WSL 2 활성화
wsl --install

# 버전 확인
docker --version
docker compose version

macOS

# Docker Desktop 다운로드 및 설치
# 또는 Homebrew
brew install --cask docker

# 버전 확인
docker --version

Linux (Ubuntu)

# 이전 버전 제거
sudo apt-get remove docker docker-engine docker.io containerd runc

# 저장소 설정
sudo apt-get update
sudo apt-get install ca-certificates curl gnupg lsb-release

# Docker GPG 키 추가
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

# 저장소 추가
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# Docker 설치
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin

# sudo 없이 실행
sudo usermod -aG docker $USER
newgrp docker

# 버전 확인
docker --version

기본 명령어

컨테이너 실행

# 이미지 다운로드 및 실행
docker run hello-world

# 백그라운드 실행
docker run -d nginx

# 포트 매핑
docker run -d -p 8080:80 nginx

# 이름 지정
docker run -d --name my-nginx -p 8080:80 nginx

# 환경 변수 전달
docker run -d -e MYSQL_ROOT_PASSWORD=secret mysql

# 볼륨 마운트
docker run -d -v /host/path:/container/path nginx

# 대화형 쉘
docker run -it ubuntu bash

# 컨테이너 제거 후 종료
docker run --rm -it ubuntu bash

컨테이너 관리

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

# 모든 컨테이너 목록
docker ps -a

# 컨테이너 중지
docker stop my-nginx

# 컨테이너 시작
docker start my-nginx

# 컨테이너 재시작
docker restart my-nginx

# 컨테이너 제거
docker rm my-nginx

# 강제 제거
docker rm -f my-nginx

# 모든 중지된 컨테이너 제거
docker container prune

# 컨테이너 로그
docker logs my-nginx
docker logs -f my-nginx  # 실시간

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

# 컨테이너 정보
docker inspect my-nginx

# 리소스 사용량
docker stats

이미지 관리

# 이미지 목록
docker images

# 이미지 다운로드
docker pull ubuntu:22.04

# 이미지 검색
docker search nginx

# 이미지 빌드
docker build -t my-app:1.0 .

# 이미지 태그
docker tag my-app:1.0 my-app:latest

# 이미지 제거
docker rmi my-app:1.0

# 사용하지 않는 이미지 제거
docker image prune

# 이미지 저장
docker save -o my-app.tar my-app:1.0

# 이미지 로드
docker load -i my-app.tar

# 이미지 히스토리
docker history my-app:1.0

Dockerfile 작성

기본 구조

# 베이스 이미지
FROM node:18-alpine

# 메타데이터
LABEL maintainer="[email protected]"
LABEL version="1.0"

# 작업 디렉터리
WORKDIR /app

# 파일 복사
COPY package*.json ./

# 명령 실행
RUN npm ci --only=production

# 소스 코드 복사
COPY . .

# 환경 변수
ENV NODE_ENV=production
ENV PORT=3000

# 포트 노출
EXPOSE 3000

# 사용자 변경 (보안)
USER node

# 실행 명령
CMD ["node", "server.js"]

Node.js 애플리케이션

# 멀티 스테이지 빌드
FROM node:18-alpine AS builder

WORKDIR /app

# 의존성 설치 (캐싱 활용)
COPY package*.json ./
RUN npm ci

# 빌드
COPY . .
RUN npm run build

# 프로덕션 이미지
FROM node:18-alpine

WORKDIR /app

# 프로덕션 의존성만
COPY package*.json ./
RUN npm ci --only=production

# 빌드 결과물만 복사
COPY --from=builder /app/dist ./dist

# 비루트 사용자
USER node

EXPOSE 3000

CMD ["node", "dist/index.js"]

Python 애플리케이션

FROM python:3.11-slim

WORKDIR /app

# 시스템 의존성
RUN apt-get update && apt-get install -y \
    gcc \
    && rm -rf /var/lib/apt/lists/*

# Python 의존성
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 소스 코드
COPY . .

# 비루트 사용자
RUN useradd -m appuser && chown -R appuser:appuser /app
USER appuser

EXPOSE 8000

CMD ["python", "app.py"]

Go 애플리케이션

# 빌드 스테이지
FROM golang:1.21-alpine AS builder

WORKDIR /app

# 의존성
COPY go.mod go.sum ./
RUN go mod download

# 빌드
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .

# 프로덕션 스테이지 (매우 작은 이미지)
FROM scratch

COPY --from=builder /app/main /main

EXPOSE 8080

CMD ["/main"]

.dockerignore

# .dockerignore
node_modules
npm-debug.log
.git
.gitignore
.env
.env.local
.DS_Store
*.md
README.md
docker-compose*.yml
Dockerfile
.dockerignore
dist
coverage
.vscode
.idea
*.log

Docker Compose

docker-compose.yml 기본

version: '3.8'

services:
  # Node.js 애플리케이션
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DB_HOST=db
    depends_on:
      - db
      - redis
    volumes:
      - ./logs:/app/logs
    restart: unless-stopped

  # PostgreSQL 데이터베이스
  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    restart: unless-stopped

  # Redis 캐시
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    restart: unless-stopped

volumes:
  postgres_data:
  redis_data:

실전 예제: MERN 스택

version: '3.8'

services:
  # MongoDB
  mongo:
    image: mongo:7
    environment:
      MONGO_INITDB_ROOT_USERNAME: admin
      MONGO_INITDB_ROOT_PASSWORD: secret
    volumes:
      - mongo_data:/data/db
    ports:
      - "27017:27017"
    networks:
      - mern-network

  # Express API
  api:
    build:
      context: ./backend
      dockerfile: Dockerfile
    ports:
      - "5000:5000"
    environment:
      - NODE_ENV=production
      - MONGO_URI=mongodb://admin:secret@mongo:27017/myapp?authSource=admin
      - JWT_SECRET=your-secret-key
    depends_on:
      - mongo
    networks:
      - mern-network
    volumes:
      - ./backend:/app
      - /app/node_modules

  # React Frontend
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile
    ports:
      - "3000:80"
    depends_on:
      - api
    networks:
      - mern-network

volumes:
  mongo_data:

networks:
  mern-network:
    driver: bridge

Docker Compose 명령어

# 시작
docker compose up

# 백그라운드 시작
docker compose up -d

# 빌드 포함 시작
docker compose up --build

# 중지
docker compose down

# 볼륨 포함 제거
docker compose down -v

# 로그 보기
docker compose logs
docker compose logs -f api

# 특정 서비스만 실행
docker compose up api

# 스케일링
docker compose up -d --scale api=3

# 실행 중인 서비스
docker compose ps

# 서비스 재시작
docker compose restart api

네트워킹

네트워크 종류

# Bridge (기본) - 같은 호스트
docker network create my-bridge

# Host - 호스트 네트워크 사용
docker run --network host nginx

# None - 네트워크 없음
docker run --network none alpine

# 사용자 정의 네트워크
docker network create --driver bridge my-network

docker run -d --name app1 --network my-network nginx
docker run -d --name app2 --network my-network nginx

# app1에서 app2로 접근
docker exec app1 ping app2

네트워크 관리

# 네트워크 목록
docker network ls

# 네트워크 상세 정보
docker network inspect my-network

# 컨테이너를 네트워크에 연결
docker network connect my-network my-container

# 컨테이너를 네트워크에서 분리
docker network disconnect my-network my-container

# 네트워크 제거
docker network rm my-network

# 사용하지 않는 네트워크 제거
docker network prune

볼륨 관리

볼륨 종류

# Named Volume
docker volume create my-volume
docker run -d -v my-volume:/app/data nginx

# Bind Mount (호스트 디렉터리)
docker run -d -v /host/path:/container/path nginx
docker run -d -v $(pwd):/app nginx

# Tmpfs Mount (메모리)
docker run -d --tmpfs /tmp nginx

볼륨 명령어

# 볼륨 목록
docker volume ls

# 볼륨 상세 정보
docker volume inspect my-volume

# 볼륨 제거
docker volume rm my-volume

# 사용하지 않는 볼륨 제거
docker volume prune

# 볼륨 백업
docker run --rm -v my-volume:/source -v $(pwd):/backup alpine tar czf /backup/backup.tar.gz -C /source .

# 볼륨 복원
docker run --rm -v my-volume:/target -v $(pwd):/backup alpine tar xzf /backup/backup.tar.gz -C /target

이미지 최적화

멀티 스테이지 빌드

# React 앱 예제
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=builder /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Alpine 베이스 사용

# Node.js
FROM node:18-alpine  # 대신 node:18 (500MB → 170MB)

# Python
FROM python:3.11-alpine  # 대신 python:3.11 (900MB → 50MB)

# Go
FROM golang:1.21-alpine  # 빌드용
FROM scratch  # 실행용 (0MB)

레이어 캐싱 활용

# ❌ 나쁨: 소스 변경 시 의존성 재설치
COPY . .
RUN npm install

# ✅ 좋음: 의존성 먼저 설치 (캐싱)
COPY package*.json ./
RUN npm ci
COPY . .

RUN 명령 최적화

# ❌ 나쁨: 여러 레이어
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y vim
RUN rm -rf /var/lib/apt/lists/*

# ✅ 좋음: 단일 레이어 + 정리
RUN apt-get update && \
    apt-get install -y curl vim && \
    rm -rf /var/lib/apt/lists/*

보안 베스트 프랙티스

1. 비루트 사용자

# 사용자 생성
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

# 권한 설정
RUN chown -R appuser:appgroup /app

# 사용자 전환
USER appuser

2. 최소 권한 이미지

# scratch (아무것도 없음)
FROM scratch

# distroless (런타임만)
FROM gcr.io/distroless/nodejs18

3. 비밀 정보 관리

# ❌ Dockerfile에 하드코딩
ENV DB_PASSWORD=secret

# ✅ 환경 변수로 전달
docker run -e DB_PASSWORD=$DB_PASSWORD my-app

# ✅ Docker Secrets (Swarm)
echo "secret" | docker secret create db_password -
docker service create --secret db_password my-app

# ✅ .env 파일 (Docker Compose)
docker compose --env-file .env up

4. 이미지 스캔

# Docker Scout
docker scout cves my-app:latest

# Trivy
trivy image my-app:latest

# Snyk
snyk container test my-app:latest

헬스체크

# Dockerfile에 추가
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1
# Docker Compose
services:
  app:
    image: my-app
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 3s
      retries: 3
      start_period: 40s

CI/CD 통합

GitHub Actions

name: Docker Build and Push

on:
  push:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v4
    
    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v3
    
    - name: Login to Docker Hub
      uses: docker/login-action@v3
      with:
        username: ${{ secrets.DOCKERHUB_USERNAME }}
        password: ${{ secrets.DOCKERHUB_TOKEN }}
    
    - name: Build and push
      uses: docker/build-push-action@v5
      with:
        context: .
        push: true
        tags: username/app:latest
        cache-from: type=gha
        cache-to: type=gha,mode=max

GitLab CI

build:
  image: docker:24
  services:
    - docker:24-dind
  script:
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

트러블슈팅

컨테이너가 즉시 종료될 때

# 로그 확인
docker logs container-name

# 실행 후 바로 쉘 진입
docker run -it my-app sh

# 오버라이드 명령
docker run -it my-app sh

포트가 이미 사용 중일 때

# 사용 중인 포트 확인 (Linux/Mac)
lsof -i :8080

# Windows
netstat -ano | findstr :8080

# 다른 포트로 매핑
docker run -p 8081:80 nginx

디스크 공간 정리

# 모든 정리 (위험!)
docker system prune -a

# 볼륨 포함 정리
docker system prune -a --volumes

# 빌드 캐시만
docker builder prune

Docker는 현대적인 애플리케이션 개발과 배포의 핵심 도구입니다. 컨테이너 기술로 일관된 환경을 유지하고 효율적인 리소스 관리가 가능합니다.