도커는 단순히 '가상화'를 편하게 해주는 툴이 아닙니다.
소프트웨어의 배포, 운영, 이식성(Portability)에 대한 패러다임을 바꾼 혁명입니다.
2023년 Stack Overflow 설문 기준, 개발자 툴 1위를 차지할 만큼 현대 개발 생태계에서 사실상 필수 도구가 되었습니다.

Docker를 처음 접하면 "리눅스 컨테이너 기반 OS 레벨 가상화 기술"이라는 정의부터 만나게 됩니다. 하지만 이 한 줄로는 아무것도 와닿지 않습니다. 치킨 프랜차이즈 비유로 시작해봅시다.
여러분이 치킨집 사장이라고 가정합니다.
| 상황 | 치킨집 비유 | 소프트웨어 세계 |
|---|---|---|
| 문제 | 본점 셰프의 레시피가 맛있는데, 가맹점마다 주방 환경(가스레인지, 기름)이 달라 맛이 다름 | 개발자 PC에선 잘 되는 코드가, 서버에 올리면 라이브러리/OS 차이로 오류 발생 |
| 해결책 | 레시피 + 재료 + 양념 + 조리도구를 밀키트(Meal Kit)로 만들어 배송 | 코드 + OS + 라이브러리 + 설정을 컨테이너 이미지로 패키징 |
| 결과 | 전국 100개 가맹점에서 똑같은 맛 | 개발/스테이징/운영 서버 어디서든 100% 동일하게 동작 |
Docker = 코드 밀키트 제조기입니다.
내 프로그램이 돌아가는 데 필요한 모든 것(코드, OS, 라이브러리, 환경변수)을 하나의 밀키트(=이미지)로 포장해서,
어떤 컴퓨터에서든 그 밀키트만 뜯으면(docker run) 똑같이 실행되게 만드는 프로그램입니다.
인프라 엔지니어를 괴롭히는 가장 큰 원인은 의존성 충돌(Dependency Hell)입니다.
ex)
AS-IS (Docker 이전):
개발자 PC: Python 3.11 + Ubuntu 22.04 + OpenSSL 3.0 -> 정상 작동
운영 서버: Python 3.8 + CentOS 7 + OpenSSL 1.0 -> 크래시
고객사 A: Python 3.9 + RHEL 8 + 방화벽 정책 다름 -> 타임아웃
고객사마다 인프라와 플랫폼이 달랐기 때문에, 개발자가 만든 프로그램을 배포할 때마다 "이 서버에 Python 몇 버전이 깔려있지?" 하는 문제를 매번 겪어야 했습니다.
TO-BE (Docker 이후):
컨테이너 이미지 안에 Python 3.11 + 필요 라이브러리 + 설정 파일 전부 포함
-> 개발 서버든, AWS든, 고객사 서버든 docker run 한 줄이면 끝
-> "환경의 영향을 받지 않고 어디서든 동일하게 동작"
컨테이너는 갑자기 등장한 것이 아닙니다. IT 인프라의 진화 과정에서 필연적으로 태어난 기술입니다.
[1세대] 베어메탈 -> [2세대] 가상화(VM) -> [3세대] 컨테이너
+---------------+ +---------------+ +---------------+
| App A/B/C | | VM1 | VM2 | | C1 | C2 | C3 |
|---------------| | OS | OS | | (100MB씩) |
| Host OS | |---------------| |---------------|
| Hardware | | Hypervisor | | Container Eng.|
+---------------+ | Hardware | | Host OS |
+---------------+ | Hardware |
+---------------+
확장성 비교: 일반 애플리케이션은 스케일 아웃 시 OS(1GB) + App(100MB)이 함께 복제되어 비효율적입니다. 컨테이너는 App(100MB)만 복제되므로, 동일 하드웨어에서 수십 배 더 많은 인스턴스를 띄울 수 있습니다.
| 연도 | 기술 | 의미 |
|---|---|---|
| 1979 | chroot (Unix V7) | 프로세스의 루트 디렉터리를 변경하여 파일 시스템 격리. 컨테이너의 씨앗 |
| 2000 | FreeBSD Jails | 파일 시스템 + 네트워크 + 사용자 격리를 제공하는 최초의 완전한 샌드박스 |
| 2006 | cgroups (Google) | 프로세스 그룹의 CPU/메모리 사용량을 제한하는 커널 기능 |
| 2008 | LXC (LinuX Containers) | Namespace + cgroups를 결합한 최초의 실용적 컨테이너. 하지만 복잡했음 |
| 2013 | Docker (Solomon Hykes) | PaaS 기업 dotCloud의 내부 프로젝트에서 탄생. 누구나 쉽게 컨테이너를 쓸 수 있게 만든 혁신 |
| 2014 | libcontainer | Docker가 LXC 의존성을 제거하고 자체 컨테이너 런타임 개발 |
Docker는 완전히 새로운 기술을 발명한 것이 아닙니다. 이미 존재하던 리눅스 커널 기능들을 조합하여 누구나 쓸 수 있게 포장한 것에 가치가 있습니다.
현대 IT 시대가 컨테이너를 필요로 하는 궁극적인 이유는 MSA(마이크로서비스 아키텍처)와 DevOps입니다.
컨테이너가 이 두 가지에 최적인 이유:
1. Push and Run: 개발자가 만든 프로그램을 어디서든 즉시 실행 가능
2. 초 단위 Scale Out/In: 트래픽 폭주 시 컨테이너를 수초 만에 수십 개 복제, 한가해지면 즉시 축소
3. 장애 격리: 컨테이너 하나가 죽어도 다른 컨테이너가 서비스 연속성 유지
4. IaC(Infrastructure as Code): 서버 구축 과정이 Dockerfile이라는 텍스트 파일로 관리되어 Git 버전 관리 가능
컨테이너는 리눅스 커널의 기능을 직접 활용하여 생성됩니다. 이것이 컨테이너를 리눅스에서 운영하는 이유이자, Windows나 Mac에서 Docker Desktop을 설치하면 내부적으로 경량 리눅스 VM(하이퍼바이저)을 띄우는 이유입니다.
[리눅스 환경] [Windows/Mac 환경]
+-----------------+ +-----------------+
| Container | | Container |
| Docker Engine | | Docker Engine |
| Linux Kernel | <- 직접 | Linux Kernel | <- 에뮬레이션
| Hardware | | Hypervisor |
+-----------------+ | Host OS (Win) |
| Hardware |
+-----------------+
클라우드 서비스(AWS, GCP, Azure)는 모두 리눅스 환경에서 운영되므로 Docker를 네이티브로 사용할 수 있습니다. 현장에서는 라이선스 비용이 없는 리눅스 기반 운영을 선호합니다.
수행하는 기능은 동일하지만 구조가 완전히 다릅니다. Node.js 웹 서버를 예로 들어보겠습니다.
일반 프로그램 방식:
1. 리눅스 서버에 Node.js를 직접 설치 (apt install nodejs)
2. 소스 코드(app.js)를 서버에 복사
3. node app.js로 직접 실행
-> 문제: 서버 OS에 종속. 다른 서버로 이동하면 환경 차이로 오류 가능
컨테이너 프로그램 방식:
1. Dockerfile에 "Node.js 18 + app.js + npm install" 명세 작성
2. docker build로 이미지 생성 (Node.js 환경 + app.js가 내부에 포함)
3. docker run으로 어디서든 실행
-> 핵심: 필요한 환경과 애플리케이션이 컨테이너 내부에 포함되어 독립적으로 동작
| 비교 항목 | 일반 프로그램 | 컨테이너 프로그램 |
|---|---|---|
| 환경 구성 | Host OS에 직접 설치 | 이미지 내부에 포함 |
| 이식성 | OS/라이브러리 버전에 종속 | 어디서든 동일 실행 |
| 격리 | 같은 OS 자원 공유 (충돌 위험) | 프로세스 단위 완전 격리 |
| 배포 | 서버마다 환경 세팅 필요 | 이미지 하나로 즉시 배포 |
| 확장 | OS + App 통째로 복제 (무거움) | App 환경만 복제 (가벼움) |
둘 다 "격리된 환경"을 제공하지만, 격리하는 방식과 수준이 근본적으로 다릅니다.
| 비교 항목 | VM (가상머신) | Container (Docker) |
|---|---|---|
| 가상화 수준 | 하드웨어 레벨 (전체 OS 가상화) | OS 레벨 (프로세스 격리) |
| 핵심 엔진 | Hypervisor (VMware, KVM 등) | Container Engine (Docker) |
| 커널 | VM마다 독립된 Guest OS 커널 | Host OS 커널을 공유 |
| 부팅 속도 | 수십 초~수 분 (OS 부팅 필요) | 수 초 이내 (프로세스 실행) |
| 리소스 | 무거움 (Guest OS 포함 수 GB) | 가벼움 (수십~수백 MB) |
| 격리 강도 | 강력 (완전 독립 OS) | 프로세스 수준 (커널 공유) |
| 보안 | 커널 분리로 높은 보안 | 커널 공유로 취약점 공유 위험 |
| 적합 용도 | 이기종 OS 실행, 레거시 앱 | MSA, 빠른 배포, 스케일링 |
chroot는 프로세스의 루트 디렉터리를 변경하여 파일 시스템을 격리하는 최초의 기술이었지만, 파일 시스템만 격리할 뿐 네트워크/프로세스 등은 격리하지 못했습니다. 이를 발전시킨 것이 Namespace입니다.
| Namespace | 격리 대상 | 효과 |
|---|---|---|
PID | 프로세스 ID | 컨테이너 내부 프로세스는 자신이 1번(init)인 줄 앎 |
NET | 네트워크 | 독립적인 IP 주소/포트/라우팅 테이블 할당 |
MNT | 파일 시스템 | 독립적인 파일 시스템 트리 구성 |
UTS | 호스트명 | 컨테이너마다 고유한 hostname 설정 |
IPC | 프로세스 간 통신 | 세마포어/메시지 큐 격리 |
USER | 사용자/그룹 | 컨테이너 내부 root != 호스트 root |
한 줄 정리: Namespace는 프로세스가 "무엇을 볼 수 있는지" 범위를 제한합니다.
특정 컨테이너가 CPU나 메모리를 무한정 끌어다 쓰면 서버 전체가 뻗어버리는 Noisy Neighbor 문제가 발생합니다. Cgroups는 컨테이너별로 사용 가능한 하드웨어 자원의 상한선을 엄격하게 제한합니다.
# 예: 컨테이너에 CPU 50%, 메모리 512MB 제한
docker run --cpus="0.5" --memory="512m" myapp:latest
한 줄 정리: Cgroups는 프로세스가 "얼마나 많은 자원을 사용할 수 있는지" 양을 제한합니다.
Docker 이미지는 하나의 거대한 파일이 아니라, 여러 개의 읽기 전용(Read-Only) 레이어가 쌓인 구조입니다.
[컨테이너 실행 시 레이어 구조]
+---------------------+
| | <- Upperdir (R/W) : 컨테이너 쓰기 레이어
| 파일 수정/생성/삭제 | (컨테이너 종료 시 사라짐)
|---------------------|
| COPY app.js /app | <- Layer 3 (R/O)
|---------------------|
| RUN npm install | <- Layer 2 (R/O)
|---------------------|
| FROM node:18-alpine| <- Layer 1 (R/O) : Base Image
+---------------------+
Copy-on-Write(CoW): 컨테이너에서 파일을 수정할 때, 읽기 전용 레이어의 원본을 건드리지 않고 상위 쓰기 레이어에 복사한 뒤 수정합니다.
레이어 공유의 위력: node:18-alpine 베이스 이미지가 로컬에 이미 있다면, 다른 이미지를 빌드/pull할 때 공통 레이어는 재다운로드하지 않습니다.
Docker를 실무에서 다루려면 이 세 가지 요소의 관계를 명확히 이해하는 것이 모든 것의 출발점입니다. 코딩애플의 밀키트 비유를 이어가면 이렇습니다:
| Docker 요소 | 밀키트 비유 | 프로그래밍 비유 | 설명 |
|---|---|---|---|
| Dockerfile | 밀키트 제조 레시피 | 클래스 설계서 | 어떤 재료(Base OS)를 쓰고, 어떤 양념(패키지)을 넣고, 소스(코드)는 어디에 담을지 적은 텍스트 명세서 |
| Image | 완성된 밀키트 패키지 | 클래스(Class) | Dockerfile을 docker build하면 생성되는 읽기 전용 템플릿. 모든 재료가 동결(Immutable) 상태로 밀봉 |
| Container | 밀키트를 뜯어서 조리 중인 상태 | 인스턴스(Instance) | 이미지를 docker run하여 메모리에 올린 실행 상태. 읽기 전용 이미지 위에 쓰기 가능한 레이어를 얹어서 실제로 동작 |
Dockerfile은 컨테이너를 어떻게 만들지 정의해 둔 텍스트 파일입니다.
이것이 바로 Infrastructure as Code(IaC)의 실현입니다.
# 예시: Node.js 웹서버 Dockerfile
FROM node:18-alpine # 1. 베이스 이미지 지정 (어떤 OS + 런타임)
WORKDIR /app # 2. 작업 디렉터리 설정
COPY package.json . # 3. 의존성 파일 먼저 복사 (캐시 최적화)
RUN npm install # 4. 의존성 설치
COPY . . # 5. 소스 코드 복사
EXPOSE 3000 # 6. 사용할 포트 선언
CMD ["node", "server.js"] # 7. 컨테이너 실행 시 실행할 명령어
각 명령어의 의미:
| 명령어 | 역할 | 밀키트 비유 |
|---|---|---|
FROM | 베이스 이미지 선택 | 밀키트에 쓸 기본 육수 선택 |
WORKDIR | 작업 디렉터리 설정 | 조리대 위치 지정 |
COPY | 파일을 이미지 안으로 복사 | 재료를 밀키트 상자에 담기 |
RUN | 빌드 시 실행할 명령 (레이어 생성) | 재료 손질 (밑간, 양념 배합) |
EXPOSE | 컨테이너가 사용할 포트 선언 | 완성된 요리를 서빙할 창구 지정 |
CMD | 컨테이너 실행 시 기본 명령 | 손님이 밀키트를 뜯었을 때 첫 조리 동작 |
IaC의 가치: 서버 세팅 과정이 Git으로 버전 관리됩니다. 신입 사원이 와도, 서버가 한 대 더 늘어나도, Dockerfile 하나면 100% 동일한 서버가 구축됩니다.
# Dockerfile을 이미지로 빌드
docker build -t myapp:v1 .
# 이미지 목록 확인
docker images
이미지의 핵심 특성:
npm install 레이어는 캐시를 재사용하여 빌드 속도를 대폭 향상시킵니다.# 이미지를 컨테이너로 실행
docker run -d -p 8080:3000 --name my-web myapp:v1
# 실행 중인 컨테이너 목록
docker ps
# 컨테이너 로그 확인
docker logs my-web
# 컨테이너 중지 및 삭제
docker stop my-web && docker rm my-web
하나의 이미지에서 여러 개의 컨테이너를 동시에 실행할 수 있습니다. 마치 하나의 밀키트 레시피로 여러 주방에서 동시에 조리하는 것과 같습니다.
# 같은 이미지로 3개의 컨테이너 동시 실행
docker run -d -p 8081:3000 --name web-1 myapp:v1
docker run -d -p 8082:3000 --name web-2 myapp:v1
docker run -d -p 8083:3000 --name web-3 myapp:v1
[Dockerfile] ──build──▶ [Image] ──run──▶ [Container]
(레시피) (밀키트) (조리 중)
│ │
│ push/pull │ stop/start
▼ │ rm
[Registry] ▼
(Docker Hub) [삭제됨]
(AWS ECR 등) (쓰기 레이어만 소멸,
이미지는 그대로 보존)
Docker는 클라이언트-서버(Client-Server) 아키텍처로 동작합니다. 개발자가 터미널에 명령어를 치면, 백그라운드에서 돌아가는 엔진이 모든 무거운 작업을 대신 처리합니다.
┌─────────────────────────────────────────────────────────────┐
│ Docker Client (CLI) │
│ docker build / run / pull / push / ... │
└───────────────────────┬─────────────────────────────────────┘
│ REST API (Unix Socket)
▼
┌─────────────────────────────────────────────────────────────┐
│ Docker Host (Daemon) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Images │ │Containers│ │ Networks │ │ Volumes │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ dockerd (백그라운드 프로세스) │
└───────────────────────┬─────────────────────────────────────┘
│ HTTPS
▼
┌─────────────────────────────────────────────────────────────┐
│ Docker Registry │
│ Docker Hub / AWS ECR / Harbor 등 │
└─────────────────────────────────────────────────────────────┘
우리가 터미널에서 docker run, docker build 등의 명령어를 입력하는 사용자 인터페이스입니다.
# 클라이언트 → Daemon 통신 흐름 예시
docker run nginx
# 1. CLI가 "run nginx" 요청을 REST API로 변환
# 2. Unix Socket(/var/run/docker.sock)을 통해 dockerd로 전송
# 3. dockerd가 이미지 확인 → 없으면 Registry에서 pull → 컨테이너 생성·실행
dockerd라는 이름의 백그라운드 프로세스(데몬)입니다. Docker의 실질적인 두뇌 역할을 합니다.
containerd → runc라는 컴포넌트 체인을 통해 실제 컨테이너 프로세스를 생성합니다.[Docker Daemon 내부 구조]
dockerd (API 수신 + 이미지/네트워크/볼륨 관리)
└─▶ containerd (컨테이너 생명주기 관리)
└─▶ runc (리눅스 커널 Namespace/Cgroups 호출하여 실제 컨테이너 프로세스 생성)
완성된 이미지를 업로드(Push)하고 다운로드(Pull)할 수 있는 원격 이미지 저장소입니다.
| Registry 종류 | 설명 | 사용 사례 |
|---|---|---|
| Docker Hub | 전 세계 개발자가 공유하는 공개 저장소 | nginx, node, python 등 공식 이미지 |
| AWS ECR | AWS 관리형 Private Registry | 사내 서비스 이미지 관리 |
| Harbor | 오픈소스 Private Registry | 온프레미스 환경 보안 요구 |
| GitHub GHCR | GitHub 연동 Registry | OSS 프로젝트 CI/CD 파이프라인 |
# Registry 활용 흐름
docker login # 1. Registry 인증
docker build -t myapp:v1 . # 2. 이미지 빌드
docker tag myapp:v1 ryujungbin/myapp:v1 # 3. 이름표 달기 (Tag)
docker push ryujungbin/myapp:v1 # 4. Registry에 업로드
docker pull ryujungbin/myapp:v1 # 5. 다른 서버에서 다운로드
docker run nginx 명령 하나를 쳤을 때 내부에서 일어나는 일을 순서대로 추적하면:
[1] 사용자 입력: docker run nginx
│
[2] Docker Client가 REST API로 변환 → dockerd에 전송
│
[3] dockerd가 로컬에 nginx 이미지가 있는지 확인
│
├── 있음 → [5]로 이동
└── 없음 → [4] Docker Hub에서 pull (레이어 단위 병렬 다운로드)
│
[5] dockerd → containerd → runc 체인으로 컨테이너 프로세스 생성
│
[6] 리눅스 커널의 Namespace(격리) + Cgroups(자원 제한) 적용
│
[7] UnionFS로 읽기 전용 이미지 레이어 위에 쓰기 레이어 생성
│
[8] 컨테이너 프로세스 시작 → nginx가 80 포트에서 서비스 시작