Docker가 해결하는 문제
“내 컴퓨터에서는 되는데요?” — 개발자라면 한 번쯤 들어본 말입니다. 개발 환경과 운영 환경의 차이(OS, 라이브러리 버전, 설정)가 원인입니다. Docker는 애플리케이션과 실행 환경을 컨테이너라는 격리된 패키지로 묶어 이 문제를 해결합니다.
가상머신(VM)과 달리 컨테이너는 OS 커널을 공유하므로 훨씬 가볍습니다.
| 항목 | 가상머신 (VM) | 컨테이너 (Docker) |
|---|---|---|
| 시작 시간 | 수십 초~수 분 | 수 초 이내 |
| 메모리 | GB 단위 | MB 단위 |
| 격리 수준 | OS 전체 격리 | 프로세스 수준 격리 |
| 이미지 크기 | 수 GB | 수십~수백 MB |
| 이식성 | 하이퍼바이저 의존 | 어디서든 동일하게 실행 |
핵심 개념 3가지
Docker를 이해하려면 이미지, 컨테이너, 레지스트리 3가지만 알면 됩니다.
- 이미지(Image): 앱 실행에 필요한 모든 것(코드, 런타임, 라이브러리)을 담은 읽기 전용 템플릿. 설계도에 해당합니다.
- 컨테이너(Container): 이미지를 기반으로 실행된 인스턴스. 설계도로 만든 실제 건물입니다. 여러 개를 동시에 실행할 수 있습니다.
- 레지스트리(Registry): 이미지를 저장/공유하는 저장소. Docker Hub가 대표적입니다.
필수 명령어
이미지 관리
Docker Hub에서 이미지를 다운로드하고, 로컬에서 관리하는 명령어입니다.
# 이미지 다운로드 (태그를 생략하면 latest)
docker pull nginx:alpine
# 로컬 이미지 목록 확인
docker images
# REPOSITORY TAG IMAGE ID SIZE
# nginx alpine a2573b4e3f72 43MB
# 이미지 삭제
docker rmi nginx:alpine
# 이미지 상세 정보 (레이어, 환경변수 등)
docker inspect nginx:alpine
alpine 태그는 경량 Alpine Linux 기반 이미지입니다. 같은 앱이라도 node:22(약 350MB)보다 node:22-alpine(약 50MB)이 훨씬 작습니다. 프로덕션에서는 가능한 alpine 태그를 사용하세요.
컨테이너 실행과 관리
이미지를 컨테이너로 실행하고, 상태를 관리하는 명령어입니다.
# 백그라운드(-d)로 실행, 이름 지정, 포트 매핑(호스트:컨테이너)
docker run -d --name my-nginx -p 8080:80 nginx:alpine
# → http://localhost:8080 에서 접속 가능
# 실행 중인 컨테이너 목록
docker ps
# CONTAINER ID IMAGE STATUS PORTS NAMES
# a1b2c3d4e5f6 nginx:alpine Up 5s 0.0.0.0:8080->80/tcp my-nginx
# 모든 컨테이너 (중지된 것 포함)
docker ps -a
# 중지 / 재시작 / 삭제
docker stop my-nginx
docker start my-nginx
docker rm my-nginx # 중지 상태에서만 삭제 가능
docker rm -f my-nginx # 강제 삭제 (실행 중이어도)
-p 8080:80에서 앞이 호스트 포트, 뒤가 컨테이너 포트입니다. 실수하기 쉬우니 호스트:컨테이너 순서를 기억하세요.
디버깅 명령어
컨테이너 내부 확인과 로그 조회에 사용합니다.
# 실시간 로그 스트리밍
docker logs -f my-nginx
# 최근 50줄만 확인
docker logs --tail 50 my-nginx
# 컨테이너 내부 쉘 접속
docker exec -it my-nginx sh
# → 컨테이너 안에서 ls, cat 등 명령어 실행 가능
# → exit 으로 빠져나오기
# 컨테이너 리소스 사용량 모니터링
docker stats my-nginx
# CPU % MEM USAGE NET I/O BLOCK I/O
# 0.00% 2.3MiB 1kB/0B 0B/0B
Dockerfile 작성법
Dockerfile은 이미지를 만드는 레시피입니다. Node.js 앱을 예시로 각 명령어를 설명합니다.
# 1. 베이스 이미지 지정 (가능하면 alpine)
FROM node:22-alpine
# 2. 작업 디렉토리 설정
WORKDIR /app
# 3. 의존성 파일만 먼저 복사 → 캐시 최적화 핵심
COPY package.json package-lock.json ./
# 4. 의존성 설치 (devDependencies 제외)
RUN npm ci --omit=dev
# 5. 소스 코드 복사 (3번 이후에 해야 캐시가 유효)
COPY . .
# 6. 컨테이너가 사용할 포트 문서화
EXPOSE 3000
# 7. 실행 명령어
CMD ["node", "server.js"]
3~5번 순서가 중요합니다. package.json이 변경되지 않으면 npm ci 레이어가 캐시되어 빌드 속도가 크게 빨라집니다. 소스 코드만 바뀌면 5번부터만 재실행됩니다.
빌드와 실행:
# 이미지 빌드 (-t: 태그 지정, .: 현재 디렉토리)
docker build -t my-app:1.0 .
# 빌드한 이미지로 실행
docker run -d --name my-app -p 3000:3000 my-app:1.0
.dockerignore 파일로 불필요한 파일을 빌드 컨텍스트에서 제외하세요.
node_modules
.git
.env
dist
*.md
Docker Compose
여러 컨테이너를 한 번에 정의하고 실행하는 도구입니다. 웹 앱 + DB처럼 연관된 서비스를 함께 관리할 때 사용합니다.
# docker-compose.yml (또는 compose.yml)
services:
web:
build: . # 현재 디렉토리의 Dockerfile로 빌드
ports:
- "3000:3000" # 호스트:컨테이너 포트 매핑
environment:
- NODE_ENV=production
- DATABASE_URL=postgres://user:secret@db:5432/mydb
depends_on:
- db # db가 먼저 시작되도록 의존성 지정
restart: unless-stopped # 비정상 종료 시 자동 재시작
db:
image: postgres:16-alpine # 공식 이미지 사용
volumes:
- db-data:/var/lib/postgresql/data # 데이터 영속화
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=secret
- POSTGRES_DB=mydb
volumes:
db-data: # 이름 있는 볼륨 (컨테이너 삭제해도 유지)
Compose 명령어:
# 모든 서비스 빌드 + 백그라운드 실행
docker compose up -d --build
# 서비스 상태 확인
docker compose ps
# 로그 확인 (특정 서비스만)
docker compose logs -f web
# 모든 서비스 중지 + 컨테이너 삭제
docker compose down
# 볼륨까지 삭제 (DB 데이터 포함 — 주의!)
docker compose down -v
depends_on은 시작 순서만 보장합니다. DB가 “준비 완료”될 때까지 기다리지는 않습니다. 앱에서 DB 연결 재시도 로직을 구현하거나, healthcheck를 설정하세요.
실전 팁
| 상황 | 명령어 | 설명 |
|---|---|---|
| 디스크 정리 | docker system prune -a | 미사용 이미지/컨테이너/네트워크 전부 삭제 |
| 빌드 캐시 확인 | docker builder prune | 빌드 캐시만 삭제 |
| 환경변수 파일 | docker run --env-file .env | .env 파일의 변수를 컨테이너에 주입 |
| 볼륨 마운트 | docker run -v $(pwd):/app | 호스트 디렉토리를 컨테이너에 마운트 (개발용) |
| 멀티스테이지 빌드 | FROM ... AS builder | 빌드 도구를 최종 이미지에서 제외하여 크기 축소 |
프로덕션에서는 latest 태그 대신 구체적인 버전 태그(예: node:22.14-alpine)를 사용하세요. latest는 언제 바뀔지 모르므로, 동일한 Dockerfile로 빌드해도 다른 결과가 나올 수 있습니다.