Container
컨테이너
애플리케이션과 의존성을 격리된 환경에 패키징하는 기술. Docker가 사실상 표준이며 VM보다 경량하고 시작이 빠릅니다.
컨테이너
애플리케이션과 의존성을 격리된 환경에 패키징하는 기술. Docker가 사실상 표준이며 VM보다 경량하고 시작이 빠릅니다.
컨테이너(Container)는 애플리케이션과 그 의존성(라이브러리, 설정 파일, 런타임 환경)을 하나의 패키지로 묶어 격리된 환경에서 실행하는 기술입니다. 2013년 Docker의 등장으로 대중화되었으며, 현대 클라우드 네이티브 아키텍처의 핵심 기반 기술입니다.
기술적 원리: 컨테이너는 Linux 커널의 namespaces(프로세스, 네트워크, 파일시스템 격리)와 cgroups(CPU, 메모리 리소스 제한)를 활용합니다. 가상머신(VM)이 하이퍼바이저 위에 게스트 OS 전체를 가상화하는 것과 달리, 컨테이너는 호스트 OS 커널을 공유하여 훨씬 가볍습니다. 시작 시간은 초 단위이고, 이미지 크기도 수십~수백 MB로 작습니다.
| 항목 | 가상머신 (VM) | 컨테이너 |
|---|---|---|
| 시작 시간 | 분 단위 | 초 단위 (밀리초도 가능) |
| 이미지 크기 | 수 GB ~ 수십 GB | 수십 MB ~ 수백 MB |
| 격리 수준 | 강함 (하드웨어 수준) | 약함 (프로세스 수준) |
| 리소스 오버헤드 | 높음 (게스트 OS 전체) | 낮음 (애플리케이션만) |
이미지 레이어: 컨테이너 이미지는 읽기 전용 레이어의 스택으로 구성됩니다. Dockerfile의 각 명령어(RUN, COPY 등)가 새 레이어를 생성하며, 동일한 베이스 이미지를 사용하는 컨테이너들은 레이어를 공유하여 디스크 공간을 절약합니다. 컨테이너 실행 시 최상단에 쓰기 가능한 레이어가 추가됩니다.
OCI 표준: Docker가 사실상 표준이었지만, 현재는 OCI(Open Container Initiative) 표준으로 containerd, CRI-O, Podman 등 다양한 런타임이 있습니다. Kubernetes는 CRI(Container Runtime Interface)를 통해 여러 런타임을 지원합니다.
# === Dockerfile (Node.js 앱 예제) ===
# 1. 베이스 이미지 선택 (alpine = 경량 Linux)
FROM node:20-alpine
# 2. 작업 디렉토리 설정
WORKDIR /app
# 3. 의존성 파일만 먼저 복사 (캐시 최적화)
COPY package*.json ./
# 4. 의존성 설치
RUN npm ci --only=production
# 5. 애플리케이션 코드 복사
COPY . .
# 6. 비루트 사용자로 전환 (보안)
RUN addgroup -g 1001 -S nodejs \
&& adduser -S nodejs -u 1001
USER nodejs
# 7. 환경 변수 설정
ENV NODE_ENV=production
ENV PORT=3000
# 8. 포트 노출 (문서화 목적)
EXPOSE 3000
# 9. 헬스체크 설정
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
# 10. 컨테이너 시작 명령
CMD ["node", "server.js"]
# === 멀티스테이지 빌드 (빌드 도구 제외로 이미지 경량화) ===
# ========== Stage 1: 빌드 ==========
FROM node:20-alpine AS builder
WORKDIR /app
# 의존성 설치 (devDependencies 포함)
COPY package*.json ./
RUN npm ci
# TypeScript 빌드
COPY tsconfig.json ./
COPY src/ ./src/
RUN npm run build
# 프로덕션 의존성만 재설치
RUN npm ci --only=production && npm cache clean --force
# ========== Stage 2: 런타임 ==========
FROM node:20-alpine AS runner
# 보안: 비루트 사용자 설정
RUN addgroup -g 1001 -S nodejs \
&& adduser -S nodejs -u 1001
WORKDIR /app
# 빌드 결과물과 프로덕션 의존성만 복사
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/package.json ./
USER nodejs
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "dist/server.js"]
# 결과: 빌드 스테이지의 TypeScript, 소스코드, devDependencies 제외
# 이미지 크기: 약 1GB → 약 150MB로 감소
# === docker-compose.yml (로컬 개발 환경) ===
version: "3.9"
services:
# 애플리케이션
app:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "3000:3000"
environment:
- NODE_ENV=development
- DATABASE_URL=postgres://user:pass@db:5432/myapp
- REDIS_URL=redis://redis:6379
volumes:
- .:/app # 소스 코드 바인드 마운트
- /app/node_modules # node_modules는 컨테이너 것 사용
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
networks:
- app-network
# PostgreSQL 데이터베이스
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: myapp
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d myapp"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app-network
# Redis 캐시
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redis_data:/data
networks:
- app-network
# Nginx 리버스 프록시
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- app
networks:
- app-network
volumes:
postgres_data:
redis_data:
networks:
app-network:
driver: bridge
# === Docker 기본 명령어 ===
# 이미지 빌드
docker build -t myapp:v1.0 .
docker build -f Dockerfile.prod -t myapp:prod .
# 컨테이너 실행
docker run -d --name myapp -p 3000:3000 myapp:v1.0
docker run -it --rm myapp:v1.0 /bin/sh # 대화형 쉘
# 컨테이너 관리
docker ps # 실행 중인 컨테이너
docker ps -a # 모든 컨테이너
docker logs -f myapp # 로그 실시간 확인
docker exec -it myapp /bin/sh # 실행 중 컨테이너 접속
docker stop myapp && docker rm myapp # 정지 및 삭제
# 이미지 관리
docker images # 이미지 목록
docker rmi myapp:v1.0 # 이미지 삭제
docker image prune -a # 미사용 이미지 정리
# === Docker Compose 명령어 ===
docker compose up -d # 백그라운드 실행
docker compose up --build # 이미지 재빌드 후 실행
docker compose logs -f app # 특정 서비스 로그
docker compose exec app sh # 컨테이너 접속
docker compose down # 정지 및 컨테이너 삭제
docker compose down -v # 볼륨까지 삭제
# === 이미지 레지스트리 ===
docker login # Docker Hub 로그인
docker tag myapp:v1.0 username/myapp:v1.0 # 태깅
docker push username/myapp:v1.0 # 푸시
docker pull username/myapp:v1.0 # 풀
# === 보안 스캔 ===
docker scout cves myapp:v1.0 # Docker Scout 취약점 스캔
trivy image myapp:v1.0 # Trivy로 스캔
"로컬에서 되는데 서버에서 안 된다는 이슈가 또 나왔어요. 개발 환경도 Docker로 통일합시다. docker-compose up 한 번이면 DB, Redis, 앱까지 전부 올라오게요. 신규 입사자 온보딩도 훨씬 빨라질 거예요."
"Dockerfile 이미지 크기가 1.2GB네요. Alpine 베이스로 바꾸고 멀티스테이지 빌드로 빌드 도구 제외하면 150MB 이하로 줄일 수 있어요. 그리고 root로 실행하고 있는데, USER 지시자로 비루트 사용자 설정 추가해 주세요."
"컨테이너와 가상머신의 차이점은 격리 수준과 리소스 효율성입니다. VM은 하이퍼바이저 위에 게스트 OS 전체를 가상화하지만, 컨테이너는 호스트 커널을 공유하고 namespaces와 cgroups로 프로세스 수준에서 격리합니다. 컨테이너는 시작이 빠르고 가볍지만, 커널을 공유하므로 VM보다 격리가 약합니다. 보안이 중요한 멀티테넌트 환경에서는 VM이나 Firecracker 같은 microVM을 고려합니다."
"컨테이너가 OOMKilled로 재시작되고 있어요. docker stats로 확인해보니 메모리 limit에 계속 도달하네요. Node.js라면 --max-old-space-size 옵션으로 힙 크기를 컨테이너 limit의 75% 정도로 설정하고, 메모리 누수가 있는지 확인해봐야 할 것 같아요."
컨테이너를 root로 실행하면 컨테이너 탈출(escape) 취약점 발생 시 호스트 시스템 전체가 위험해집니다. 반드시 비루트 사용자(USER 지시자)를 설정하세요. 또한 --privileged 플래그 사용은 절대 피하세요.
컨테이너는 휘발성입니다. 컨테이너 삭제 시 내부 데이터도 사라집니다. DB 데이터, 로그, 업로드 파일 등 영구 데이터는 반드시 볼륨(-v 또는 volumes)을 사용하거나 외부 스토리지(S3 등)에 저장하세요.
Docker Hub의 임의 이미지에는 악성코드나 취약점이 포함될 수 있습니다. 공식 이미지나 검증된 퍼블리셔의 이미지만 사용하고, Trivy나 Docker Scout으로 취약점을 스캔하세요.
빌드 도구(TypeScript, Webpack 등)와 devDependencies는 런타임에 필요 없습니다. 멀티스테이지 빌드로 빌드 스테이지와 런타임 스테이지를 분리하면 이미지 크기를 80% 이상 줄일 수 있습니다.