Container는 애플리케이션과 실행 환경을 하나로 구성해서 어디서든 동일하게 실행하는 기술이다.
Container
프로세스 격리
Container는 호스트 OS 위에서 실행되는 격리된 프로세스다. Guest OS가 없다. 호스트 커널을 공유하면서 프로세스, 네트워크, 파일시스템만 분리한다.
namespace는 프로세스 목록, 네트워크 인터페이스, 파일시스템을 독립된 공간으로 분리한다. Container 안의 프로세스는 다른 Container의 존재를 알 수 없다.
cgroup(Control Group)은 자원 사용량에 상한을 건다. 격리만으로는 하나의 Container가 호스트의 CPU, 메모리, 디스크 I/O를 전부 소모할 수 있기 때문이다.
VM과의 비교
namespace와 cgroup으로 프로세스를 격리하는 방식은 VM과 근본적으로 다르다.
VM은 Hypervisor 위에 Guest OS를 통째로 설치한다. 하드웨어 수준의 격리를 제공하지만, OS 전체를 포함하므로 Image가 크고 시작이 느리다.
Container는 호스트 커널을 공유하고 프로세스 수준에서만 격리한다. Guest OS가 없으니 Image가 가볍고 시작이 빠르다. 격리 수준은 VM보다 낮지만, 대부분의 배포 시나리오에서 충분하다.
flowchart TB
subgraph vm["VM"]
direction TB
HW1["하드웨어"] --> HV["Hypervisor"]
HV --> G1["Guest OS + App"]
HV --> G2["Guest OS + App"]
end
subgraph ct["Container"]
direction TB
HW2["하드웨어"] --> OS["Host OS\n커널 공유"]
OS --> CR["Container Runtime"]
CR --> C1["App"]
CR --> C2["App"]
end
Docker 아키텍처
Docker는 클라이언트-서버 구조로 동작한다.
flowchart LR
CLI["Docker CLI"] -->|REST API| D["Docker Daemon\ndockerd"]
D --> CTD["containerd"]
CTD --> RUNC["runc"]
RUNC --> C1["Container"]
RUNC --> C2["Container"]
docker run이나 docker build를 입력하면 Docker CLI가 Docker Daemon(dockerd)에 REST API로 전달한다. Daemon이 Container 생명주기를 관리하는 핵심 프로세스다.
그 아래 두 계층이 더 있다. containerd가 Container 실행, Image 관리, 스토리지를 담당한다. runc가 namespace와 cgroup을 설정해서 Container 프로세스를 시작한다. runc는 OCI(Open Container Initiative) 표준을 구현한 저수준 런타임이다.
계층이 분리되어 있으므로 Docker Daemon 없이 containerd만으로도 Container를 운영할 수 있다. Kubernetes가 Docker 의존성을 제거한 것도 이 구조 덕분이다.
Docker 핵심 개념
Image
Container를 실행하려면 Image가 필요하다. Image는 애플리케이션 코드, 런타임, 라이브러리, 설정 파일을 담은 읽기 전용 템플릿이다.
Image는 계층 구조다. 각 계층이 이전 계층 위에 변경 사항만 추가한다. 여러 Image가 공통 계층을 공유하면 디스크 사용량과 빌드 시간이 줄어든다.
Container
Container는 Image로부터 생성된 실행 인스턴스다. Image의 읽기 전용 계층 위에 쓰기 가능한 계층을 추가한다. 같은 Image로 여러 Container를 독립적으로 실행한다.
Container가 삭제되면 쓰기 계층의 데이터도 사라진다. 영속 데이터가 필요하면 볼륨으로 호스트에 별도 저장한다.
Registry
Image를 저장하고 공유하는 곳이 Registry다. Docker Hub가 대표적인 공개 Registry이고, 조직 내부에서 사설 Registry를 운영하기도 한다.
Image를 Registry에 저장하는 것이 push, 가져오는 것이 pull이다.
flowchart LR
DF["Dockerfile"] -->|docker build| IMG["Image"]
IMG -->|docker run| CT["Container"]
IMG -->|docker push| REG["Registry"]
REG -->|docker pull| IMG2["Image"]
Dockerfile
Image를 빌드하려면 Dockerfile이 필요하다. 베이스 Image 선택, 애플리케이션 복사, 의존성 설치, 실행 명령을 순서대로 기술하는 명세 파일이다.
주요 명령어
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
FROM은 베이스 Image를 지정한다. 모든 Dockerfile의 시작점이다. WORKDIR로 작업 디렉토리를 설정하고, COPY로 호스트 파일을 Image 안으로 복사한다.
RUN은 빌드 시 실행할 명령이다. 의존성 설치나 컴파일에 사용한다. 각 RUN 명령이 새로운 계층을 만든다.
EXPOSE는 Container가 수신할 포트를 문서화한다. 실제 포트 바인딩은 docker run -p 옵션으로 한다. CMD는 Container 시작 시 실행할 기본 명령이다.
CMD와 ENTRYPOINT
비슷해 보이지만 역할이 다르다.
CMD는 Container 시작 시 실행할 기본 명령이다. docker run에 인자를 전달하면 CMD 전체가 교체된다. ENTRYPOINT는 항상 실행할 명령을 고정한다. docker run의 인자는 ENTRYPOINT 뒤에 추가된다.
# CMD — docker run 인자로 교체할 수 있다
CMD ["node", "server.js"]
# ENTRYPOINT — 항상 node를 실행하고, 인자를 추가로 받는다
ENTRYPOINT ["node"]
CMD ["server.js"]
둘을 함께 쓰면 ENTRYPOINT가 실행 파일을, CMD가 기본 인자를 담당한다. docker run <image> worker.js처럼 인자를 전달하면 CMD 부분만 교체된다. CLI 도구를 Container로 배포할 때 자주 쓰는 패턴이다.
멀티 스테이지 빌드
하나의 Dockerfile에서 여러 FROM을 사용해 빌드 단계를 분리한다. 빌드에 필요한 도구와 소스는 빌드 단계에만 남기고, 최종 Image에는 실행에 필요한 결과물만 담는다.
# 빌드 단계
FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN go build -o server .
# 실행 단계
FROM alpine:3.19
COPY --from=builder /app/server /server
CMD ["/server"]
컴파일러와 소스가 최종 Image에서 빠지므로 크기가 줄어든다. Go 같은 컴파일 언어에서 효과가 크다.
Docker Compose
실제 서비스는 웹 서버, 데이터베이스, 캐시 등 여러 Container를 함께 실행한다. 각각 docker run으로 구동하면 관리가 번거롭다.
Docker Compose는 여러 Container를 하나의 파일로 정의하고 관리하는 도구다.
기본 구조
services:
api:
build: .
ports:
- "8080:3000"
depends_on:
- db
- redis
environment:
DATABASE_URL: postgres://user:pass@db:5432/mydb
db:
image: postgres:16
volumes:
- db-data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
volumes:
db-data:
services에서 각 Container를 정의한다. build로 Dockerfile 경로를 지정하거나, image로 기존 Image를 사용한다.
volumes는 데이터를 Container 외부에 영속 저장한다. Container를 삭제해도 데이터가 유지된다.
depends_on은 서비스 간 시작 순서를 정한다. 서비스가 “준비 완료"될 때까지 기다리지는 않으므로, 애플리케이션에서 재시도 로직이 필요하다.
주요 명령어
docker compose up으로 모든 서비스를 시작한다. -d 옵션을 붙이면 백그라운드로 실행된다. docker compose down은 Container와 네트워크를 제거하지만 볼륨은 유지한다. docker compose logs -f로 실시간 로그를 확인한다.
정리
Container는 프로세스 격리 기술이고, Docker는 이를 Image, Container, Registry 구조로 다룬다. Dockerfile로 빌드하고, Compose로 여러 Container를 구성하면 개발부터 배포까지 동일한 환경을 유지한다.