개요
Docker는 애플리케이션을 컨테이너라는 격리된 환경에서 실행할 수 있게 해주는 도구입니다. 이미지에 애플리케이션과 의존성을 함께 담아 실행 환경을 고정합니다.
이 글에서는 컨테이너 개념, Dockerfile, 네트워크, 볼륨, Compose를 정리합니다.
1. 컨테이너 vs 가상머신
정리 보기
가상머신 (VM)
┌─────────────┐ ┌─────────────┐
│ App A │ │ App B │
│ Libs/Bins │ │ Libs/Bins │
│ Guest OS │ │ Guest OS │
├─────────────┴──┴─────────────┤
│ Hypervisor │
├──────────────────────────────┤
│ Host OS │
└──────────────────────────────┘- 각 VM마다 완전한 게스트 OS가 올라감
- 실행하려면 게스트 OS 부팅 과정이 필요함
- 완전한 격리
컨테이너
┌─────────────┐ ┌─────────────┐
│ App A │ │ App B │
│ Libs/Bins │ │ Libs/Bins │
├─────────────┴──┴─────────────┤
│ Docker Engine │
├──────────────────────────────┤
│ Host OS │
└──────────────────────────────┘- 호스트 OS의 커널을 공유
- 게스트 OS가 없어 별도 부팅 과정이 필요하지 않음
- 프로세스 수준 격리 (namespace, cgroup)
비교
| 항목 | 가상머신 | 컨테이너 |
|---|---|---|
| 구성 | 게스트 OS 포함 | 게스트 OS 없음 (호스트 커널 공유) |
| 시작 | 게스트 OS 부팅 필요 | 호스트 커널 공유로 부팅 불필요 |
| 격리 수준 | 완전 (OS 분리) | 프로세스 수준 |
| 실행 경로 | 하이퍼바이저 계층을 거침 | 호스트 커널을 직접 사용 |
| 용도 | 다른 OS 필요할 때 | 앱 배포, 개발 환경 통일 |
컨테이너가 가상머신을 대체하는 건 아닙니다. 용도가 다릅니다.
2. Docker 핵심 개념
정리 보기
이미지 (Image)
- 컨테이너를 만들기 위한 읽기 전용 템플릿
- 여러 레이어로 구성 (각 레이어는 Dockerfile의 명령어 하나)
- 같은 이미지를 여러 호스트에서 동일한 구성으로 실행 가능 (호스트 커널·아키텍처 호환 범위 내)
컨테이너 (Container)
- 이미지를 기반으로 실행된 인스턴스
- 읽기/쓰기 가능한 레이어가 추가됨
- 삭제하면 내부 데이터도 사라짐 (볼륨 제외)
레지스트리 (Registry)
- 이미지를 저장하고 공유하는 저장소
- Docker Hub (공개), ECR (AWS), GCR (GCP), GHCR (GitHub)
흐름
Dockerfile → (빌드) → Image → (실행) → Container
↓
Registry에 push
↓
서버에서 pull → Container3. 기본 명령어
정리 보기
이미지 관련
# 이미지 빌드 (-t: 태그 지정, .: 현재 디렉터리의 Dockerfile 사용)
docker build -t myapp:1.0 .
# 이미지 목록
docker images
# 이미지 다운로드
docker pull nginx:latest
# 이미지 삭제
docker rmi myapp:1.0
# 사용하지 않는 이미지 정리
docker image prune컨테이너 관련
# 컨테이너 실행
# -d: 백그라운드, -p: 포트 매핑 (호스트:컨테이너), --name: 이름 지정
docker run -d -p 8080:80 --name web nginx
# 실행 중인 컨테이너 목록
docker ps
# 모든 컨테이너 (중지 포함)
docker ps -a
# 컨테이너 중지 / 시작 / 재시작
docker stop web
docker start web
docker restart web
# 컨테이너 삭제
docker rm web
# 실행 중인 컨테이너에 접속
docker exec -it web /bin/bash
# -i: interactive, -t: tty (터미널)
# 컨테이너 로그
docker logs web
docker logs -f web # -f: 실시간 (follow)정리 명령어
# 중지된 컨테이너, 사용 안 하는 이미지/네트워크 전부 정리 (볼륨 제외)
docker system prune -a
# 현재 디스크 사용량 확인
docker system df4. Dockerfile 작성법
정리 보기
기본 구조
# 베이스 이미지 지정
FROM node:20-alpine
# 작업 디렉터리 설정
WORKDIR /app
# 의존성 파일 복사 (캐시 활용을 위해 먼저 복사)
COPY package.json package-lock.json ./
# 의존성 설치
RUN npm ci
# 소스 코드 복사
COPY . .
# 빌드
RUN npm run build
# 포트 선언 (문서화 용도)
EXPOSE 3000
# 실행 명령어
CMD ["node", "dist/server.js"]EXPOSE는 포트를 발행(publish)하지 않습니다. 이미지를 만든 사람과 실행하는 사람 사이의 문서 역할이고, 실제로 포트를 열려면 실행할 때 -p(특정 포트 매핑)나 -P(EXPOSE된 포트를 호스트의 임의 포트에 매핑)를 써야 합니다. 프로토콜을 안 적으면 TCP가 기본값입니다.
명령어
| 명령어 | 용도 |
|---|---|
FROM |
베이스 이미지 지정 |
WORKDIR |
작업 디렉터리 설정 |
COPY |
파일/디렉터리 복사 |
RUN |
빌드 시 실행할 명령어 |
CMD |
컨테이너 시작 시 실행할 명령어 |
ENV |
환경 변수 설정 |
EXPOSE |
포트 문서화 |
ARG |
빌드 시 전달할 인자 (이미지에 남지 않음) |
ENTRYPOINT |
컨테이너의 기본 실행 파일 지정 |
CMD vs ENTRYPOINT
# CMD: 기본 명령어 (docker run에 명령을 주면 대체됨)
CMD ["npm", "start"]
# ENTRYPOINT: 기본 실행 파일
ENTRYPOINT ["node", "server.js"]
# 조합: ENTRYPOINT가 실행, CMD가 기본 인자
ENTRYPOINT ["node"]
CMD ["server.js"]ENTRYPOINT는 무엇을 실행할지, CMD는 그 뒤에 붙는 기본 인자입니다.
exec form ENTRYPOINT가 있으면 docker run <image> 뒤에 붙인 인자가 ENTRYPOINT 뒤에 이어 붙고, CMD에 적어둔 값은 그 인자로 대체됩니다. ENTRYPOINT도 docker run --entrypoint로 덮어쓸 수 있습니다.
5. 레이어 캐시와 Multi-stage Build
정리 보기
레이어 캐시
Dockerfile의 각 명령어는 하나의 레이어를 만듭니다. 변경되지 않은 레이어는 캐시에서 재사용됩니다.
# 의존성 파일을 먼저 복사한 경우
COPY package.json package-lock.json ./ ← 자주 안 바뀜 (캐시 활용)
RUN npm ci
COPY . . ← 자주 바뀜 (여기부터 다시 빌드)# 소스를 먼저 복사한 경우: 소스가 바뀌면 npm ci 레이어도 다시 실행됨
COPY . .
RUN npm ci캐시 원리: 위에서부터 변경이 감지되면 그 아래 모든 레이어가 다시 빌드됩니다.
비교 방식은 명령어 종류에 따라 다릅니다. ADD와 COPY(그리고 RUN --mount=type=bind)는 파일 메타데이터로 캐시 체크섬을 계산해서 파일이 바뀌었는지 봅니다. 그 외 RUN은 컨테이너 안의 파일을 들여다보지 않고 명령어 문자열만 비교합니다. 그래서 RUN apt-get update는 시간이 지나도 캐시가 자동으로 무효화되지 않고, 강제로 다시 실행하려면 앞 레이어를 바꾸거나 --no-cache 계열 옵션을 써야 합니다.
Multi-stage Build
빌드 환경과 실행 환경을 분리해서 최종 이미지 크기를 줄입니다.
# 1단계: 빌드
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# 2단계: 실행 (빌드 도구 없이 결과물만)
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/server.js"]효과
최종 이미지에 빌드 도구와 개발 의존성이 포함되지 않아 이미지 크기가 줄어듭니다. 실제 크기는 베이스 이미지와 애플리케이션에 따라 달라집니다.
이미지가 작아지면 전송할 데이터가 줄고, 포함되는 패키지가 적어져 점검 대상이 줄어듭니다.
6. 볼륨과 데이터 관리
정리 보기
왜 필요한가
컨테이너는 삭제하면 내부 데이터도 사라집니다. DB 데이터, 로그, 설정 파일처럼 유지돼야 하는 데이터는 볼륨으로 분리합니다.
볼륨 종류
# Named Volume (Docker가 생성·관리하는 저장 위치 사용)
docker run -d -v mydata:/var/lib/mysql mysql
# Bind Mount (호스트 경로 직접 지정)
docker run -d -v /host/path:/container/path nginx
# 읽기 전용 (:ro)
docker run -d -v "$(pwd)"/config:/app/config:ro nginx-v의 세 번째 필드가 옵션 자리이고, 여기에 ro(= readonly)를 주면 컨테이너가 해당 마운트에 쓸 수 없습니다. bind mount에서 호스트 경로는 $(pwd)처럼 절대 경로로 넘깁니다. 상대 경로를 그대로 쓰려면 절대·상대 경로를 모두 받는 --mount type=bind,src=...,dst=...를 씁니다.
차이
| 종류 | 관리 | 용도 |
|---|---|---|
| Named Volume | Docker가 관리 | DB 데이터, 영구 저장 |
| Bind Mount | 호스트 경로 직접 | 개발 중 소스 코드 마운트 |
bind mount는 기본적으로 호스트 파일에 쓰기 권한을 가집니다. 컨테이너 안 프로세스가 호스트 파일을 지우거나 바꿀 수 있다는 뜻입니다. 읽기 전용으로 마운트하면 컨테이너에서 해당 경로에 쓰기가 차단됩니다.
볼륨 명령어
# 볼륨 목록
docker volume ls
# 볼륨 생성
docker volume create mydata
# 볼륨 삭제
docker volume rm mydata
# 컨테이너가 참조하지 않는 볼륨 정리 (기본값: 익명 볼륨만)
docker volume prune
# 이름 붙인 볼륨까지 정리
docker volume prune -adocker volume prune은 기본값으로 익명 볼륨만 지웁니다. 이름을 붙인 볼륨까지 지우려면 -a(--all)가 필요합니다.
7. Docker 네트워크
정리 보기
기본 네트워크 종류
| 네트워크 | 설명 |
|---|---|
| bridge | 기본값. 같은 bridge 안의 컨테이너끼리 통신 가능 |
| host | 호스트 네트워크 그대로 사용 (포트 매핑 불필요) |
| none | 네트워크 없음 |
커스텀 네트워크
# 네트워크 생성
docker network create app-net
# 네트워크에 연결하면서 실행
docker run -d --name api --network app-net myapi
docker run -d --name db --network app-net mysql
# 같은 네트워크 안에서는 컨테이너 이름으로 통신 가능
# api 컨테이너에서: mysql -h db -u root왜 커스텀 네트워크를 쓰는가
같은 커스텀 네트워크에 넣으면 Docker가 내장 DNS를 제공합니다. 컨테이너 이름이 곧 호스트명이 됩니다.
api 컨테이너 안에서:
mysql -h db -u root
↑
"db"라는 컨테이너 이름이 자동으로 해당 컨테이너의 IP로 해석됨| 항목 | 기본 bridge | 커스텀(사용자 정의) bridge |
|---|---|---|
| 이름으로 통신 | 안 됨 (IP로만 가능, 레거시 --link 제외) |
가능 (컨테이너 이름 또는 alias) |
| 격리 | --network를 안 주면 전부 같은 bridge에 붙음 |
그 네트워크에 붙은 컨테이너끼리만 통신 |
| 컨테이너 이름 DNS 해석 | 미제공 | 자동 제공 |
기본 bridge에서는 mysql -h db가 안 됩니다. 기본 bridge의 컨테이너는 레거시 --link를 쓰지 않으면 IP로만 서로 접근할 수 있습니다. 그런데 컨테이너 IP는 시작할 때마다 서브넷에서 새로 할당되고 재시작·재생성 사이에 유지되지 않습니다. 사용자 정의 네트워크에서는 컨테이너 이름이 DNS로 해석되므로 IP가 바뀌어도 같은 이름으로 접근합니다.
docker-compose에서 서비스끼리 이름으로 통신할 수 있는 이유도 이 원리입니다. Compose는 docker compose up을 하면 <프로젝트명>_default라는 bridge 네트워크를 만들고 모든 서비스를 여기에 붙입니다. 각 서비스는 자기 이름을 내부 DNS에 등록합니다.
네트워크 명령어
# 네트워크 목록
docker network ls
# 네트워크 상세 정보
docker network inspect app-net
# 실행 중인 컨테이너를 네트워크에 연결
docker network connect app-net web
# 네트워크 삭제
docker network rm app-net8. Docker Compose
정리 보기
왜 쓰는가
여러 컨테이너를 한 번에 정의하고 실행할 때 사용합니다. 앱 서버 + DB + Redis 같은 조합을 한 파일로 관리합니다.
docker-compose.yml 예시
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DB_HOST=db
- REDIS_HOST=redis
depends_on:
- db
- redis
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: admin
POSTGRES_PASSWORD: secret
volumes:
- db-data:/var/lib/postgresql/data
ports:
- "5432:5432"
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
db-data:명령어
# 전체 실행 (-d: 백그라운드)
docker compose up -d
# 전체 중지 + 삭제
docker compose down
# 로그 확인
docker compose logs -f app
# 특정 서비스만 재빌드
docker compose up -d --build app
# 상태 확인
docker compose psdepends_on 동작
depends_on은 시작 순서만 보장하고 서비스가 준비됐는지는 보장하지 않습니다- 준비 상태까지 기다리려면 healthcheck와
depends_on.condition: service_healthy를 함께 씁니다
9. .dockerignore
정리 보기
이미지 빌드 시 불필요한 파일을 제외합니다. .gitignore와 같은 원리입니다.
# .dockerignore
node_modules
dist
.git
.env
*.log
.DS_Store
docker-compose.yml
README.md제외했을 때의 동작
COPY . .할 때 불필요한 파일이 이미지에 들어가는 것 방지- 호스트에서 설치한
node_modules가 복사되면 네이티브 모듈이 컨테이너 환경과 맞지 않을 수 있음 - 빌드 속도 향상 (빌드 컨텍스트가 작아짐)
- 민감한 파일 (
.env, 키 파일) 이미지에 포함되는 것 방지
10. 디버깅과 문제 해결
정리 보기
컨테이너가 바로 죽을 때
# 컨테이너 표준 출력·표준 에러 확인
docker logs <container-name>
# 종료 코드 확인
docker inspect <container-name> --format='{{.State.ExitCode}}'
# 0: 정상 종료
# OOM Kill로 죽었는지 확인
docker inspect <container-name> --format='{{.State.OOMKilled}}'
# 이미지로 셸 띄우기 (ENTRYPOINT를 셸로 바꿔서 디버깅)
docker run -it --entrypoint /bin/sh myapp:1.0종료 코드가 128보다 크면 신호로 죽은 경우입니다. 셸은 신호 N으로 종료된 명령에 128 + N을 종료 상태로 사용하기 때문에, 137은 SIGKILL(9), 143은 SIGTERM(15)에 대응합니다. 137은 OOM으로만 발생하지 않습니다. docker stop의 타임아웃이 지나 SIGKILL이 나간 경우도 같은 값이 됩니다. OOM 여부는 docker inspect의 State.OOMKilled 필드에 기록됩니다.
이미지 빌드 실패 시
# 빌드 로그 자세히 보기
docker build --no-cache --progress=plain -t myapp .
# 특정 스테이지까지만 빌드
docker build --target builder -t myapp-debug .네트워크 문제
# 컨테이너 내부에서 네트워크 확인
docker exec -it web sh -c "curl http://api:3000/health"
# 컨테이너의 IP 확인
docker inspect web --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'
# DNS 확인
docker exec -it web nslookup db디스크 공간 문제
# Docker 디스크 사용량
docker system df
# 정리 (중지된 컨테이너, 사용 안 하는 이미지/네트워크 + 익명 볼륨)
docker system prune -a --volumesdocker system prune은 기본적으로 볼륨을 지우지 않습니다. 볼륨을 쓰는 컨테이너가 없다는 이유로 중요한 데이터가 삭제되는 걸 막기 위한 동작이고, --volumes를 줘야 정리 대상에 포함됩니다. 이때도 대상은 익명 볼륨입니다.