Docker의 핵심 개념인 이미지와 컨테이너를 이해하고, 실무에서 활용하는 방법을 상세히 알아봅니다
Docker를 시작할 때 가장 먼저 이해해야 할 두 가지 핵심 개념이 있습니다: **이미지(Image)**와 **컨테이너(Container)**입니다. 이 포스트에서는 이 두 개념을 깊이 있게 다루고, 실무에서 바로 활용할 수 있는 팁과 베스트 프랙티스를 공유하겠습니다.
먼저 간단한 비유로 시작해보겠습니다:
이미지는 읽기 전용 템플릿이고, 컨테이너는 이 템플릿에서 생성된 실행 가능한 인스턴스입니다.
Docker 이미지의 가장 중요한 특징 중 하나는 레이어 기반 아키텍처입니다.
Dockerfile의 각 명령어는 하나의 레이어를 생성합니다. 예를 들어보겠습니다:
FROM node:14 # 레이어 1: 베이스 이미지
WORKDIR /app # 레이어 2: 작업 디렉토리 설정
COPY package.json . # 레이어 3: package.json 복사
RUN npm install # 레이어 4: 의존성 설치
COPY . . # 레이어 5: 나머지 파일 복사
CMD ["node", "app.js"] # 레이어 6: 실행 명령
Docker는 각 레이어를 캐싱하여 빌드 속도를 향상시킵니다. 하지만 레이어가 변경되면 그 이후의 모든 레이어도 다시 빌드됩니다.
FROM node:14
WORKDIR /app
COPY . . # 코드가 변경될 때마다
RUN npm install # npm install이 다시 실행됨 (시간 낭비!)
CMD ["node", "app.js"]
FROM node:14
WORKDIR /app
COPY package*.json . # 의존성 파일만 먼저 복사
RUN npm install # 의존성이 변경될 때만 재실행
COPY . . # 소스 코드 복사
CMD ["node", "app.js"]
이 작은 변경으로 개발 중 빌드 시간을 크게 단축할 수 있습니다!
변경 빈도가 낮은 것부터 높은 순으로 배치
# 거의 변경되지 않음
FROM node:14
RUN apt-get update && apt-get install -y curl
# 가끔 변경됨
COPY package*.json .
RUN npm install
# 자주 변경됨
COPY . .
관련 명령어 합치기
# 여러 레이어 생성 (비효율적)
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git
# 하나의 레이어로 (효율적)
RUN apt-get update && apt-get install -y \
curl \
git \
&& rm -rf /var/lib/apt/lists/*
Docker 컨테이너는 두 가지 모드로 실행할 수 있습니다:
docker run -p 8080:80 nginx
docker run -d -p 8080:80 nginx
docker stop 명령으로 종료# 개발 환경: 로그를 보면서 작업
docker run --name myapp -p 3000:3000 myapp:dev
# 프로덕션 환경: 백그라운드 실행
docker run -d --name myapp -p 80:3000 --restart always myapp:prod
# 나중에 로그 확인이 필요할 때
docker logs -f myapp # -f는 실시간 follow
일부 애플리케이션은 사용자 입력이 필요합니다. Python 스크립트를 예로 들어보겠습니다:
# app.py
name = input("이름을 입력하세요: ")
print(f"안녕하세요, {name}님!")
docker run python-app
# EOFError: EOF when reading a line
docker run -it python-app
# 이름을 입력하세요: Docker
# 안녕하세요, Docker님!
-i (--interactive): 표준 입력 유지-t (--tty): 가상 터미널 할당# 실행 중인 컨테이너에 쉘 접속
docker exec -it myapp /bin/bash
# 컨테이너 재시작 시 인터랙티브 모드 유지
docker start -ai myapp
개발하다 보면 불필요한 이미지와 컨테이너가 쌓입니다. 정기적인 정리가 필요합니다.
# 실행 중인 컨테이너
docker ps
# 모든 컨테이너 (중지된 것 포함)
docker ps -a
# 이미지 목록
docker images
# 디스크 사용량 확인
docker system df
# 특정 컨테이너 삭제
docker rm container_name
# 특정 이미지 삭제
docker rmi image_name:tag
# 종료된 모든 컨테이너 삭제
docker container prune
# 사용되지 않는 모든 리소스 정리
docker system prune
# 더 공격적인 정리 (태그 없는 이미지 포함)
docker system prune -a
# 볼륨까지 포함한 완전 정리
docker system prune -a --volumes
# crontab에 추가하여 매주 정리
0 0 * * 0 docker system prune -f
docker inspect는 이미지와 컨테이너의 상세 정보를 제공합니다.
# 전체 정보 보기
docker image inspect nginx
# 특정 정보만 추출
docker image inspect nginx --format='{{.Config.ExposedPorts}}'
# 환경 변수 확인
docker image inspect nginx --format='{{json .Config.Env}}'
# 이미지 크기 확인
docker image inspect nginx --format='{{.Size}}'
# 레이어 정보 확인
docker image inspect nginx --format='{{json .RootFS.Layers}}'
# 생성 날짜 확인
docker image inspect nginx --format='{{.Created}}'
실행 중인 컨테이너와 호스트 간 파일 복사가 가능합니다.
# 호스트 → 컨테이너
docker cp ./config.json myapp:/app/config.json
# 컨테이너 → 호스트
docker cp myapp:/var/log/app.log ./logs/
✅ 좋은 사용 예:
❌ 피해야 할 사용:
# 코드 업데이트는 이미지 재빌드로
docker build -t myapp:v2 .
docker stop myapp
docker run -d --name myapp myapp:v2
의미 있는 이름을 사용하면 관리가 훨씬 쉬워집니다.
# 랜덤 이름 대신
docker run -d nginx # → festive_nobel
# 명확한 이름 사용
docker run -d --name web-server nginx
docker run -d --name api-server node-app
docker run -d --name db postgres
docker build -t myapp:1.0.0 .
docker build -t myapp:1.0.1 .
docker build -t myapp:latest .
docker build -t myapp:dev .
docker build -t myapp:staging .
docker build -t myapp:prod .
docker build -t myapp:feature-auth .
docker build -t myapp:hotfix-123 .
실제 Node.js 애플리케이션을 Docker화하는 완전한 예제를 살펴보겠습니다.
myapp/
├── src/
│ └── index.js
├── package.json
├── package-lock.json
└── Dockerfile
# 멀티 스테이지 빌드 사용
FROM node:14-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:14-alpine
WORKDIR /app
COPY /app/node_modules ./node_modules
COPY ./src ./src
COPY package*.json ./
# 보안을 위한 non-root 사용자
USER node
EXPOSE 3000
CMD ["node", "src/index.js"]
# 이미지 빌드
docker build -t myapp:1.0.0 -t myapp:latest .
# 개발 환경 실행
docker run -d \
--name myapp-dev \
-p 3000:3000 \
-v $(pwd)/src:/app/src \
-e NODE_ENV=development \
myapp:latest
# 프로덕션 환경 실행
docker run -d \
--name myapp-prod \
-p 80:3000 \
-e NODE_ENV=production \
--restart always \
myapp:1.0.0
# 오류: bind: address already in use
docker run -p 3000:3000 myapp
# 해결: 다른 포트 사용 또는 기존 프로세스 종료
docker run -p 3001:3000 myapp
# 오류: permission denied
# 해결: 사용자 지정
docker run --user $(id -u):$(id -g) myapp
# 디버깅을 위한 로그 확인
docker logs myapp
# 인터랙티브 모드로 디버깅
docker run -it myapp /bin/sh
이번 포스트에서는 Docker의 핵심인 이미지와 컨테이너에 대해 깊이 있게 다뤘습니다. 주요 내용을 정리하면:
다음 포스트에서는 컨테이너의 일시적인 특성을 극복하고 데이터를 영구적으로 저장하는 방법인 **볼륨(Volume)**에 대해 알아보겠습니다.
시리즈 네비게이션
Docker 컨테이너 간 통신, 외부 네트워크 연결, 그리고 다양한 네트워크 드라이버를 활용한 효과적인 네트워킹 구성 방법을 알아봅니다
로컬에 개발 도구를 설치하지 않고 Docker 컨테이너로 npm, composer, artisan 등을 실행하는 유틸리티 컨테이너 패턴을 알아봅니다
실제 프로덕션 환경과 같은 다중 컨테이너 애플리케이션을 Docker로 구축하는 방법을 React, Node.js, MongoDB를 활용한 실전 예제로 알아봅니다
Nginx, PHP-FPM, MySQL, Redis를 조합한 프로덕션급 Laravel 개발 환경을 Docker로 구축하는 방법을 상세히 알아봅니다