Infra & Network

[Docker | Part 1] Docker 핵심 구조 이해

hrbds 2026. 4. 23. 16:17

목차

  • 환경 준비
  • 컨테이너 vs VM — 왜 컨테이너가 가벼운가
  • Image 레이어 구조
  • Image → Container 생성 원리
  • 핵심 명령어 실습
  • 전체 흐름 요약

1. 환경 준비

docker --version
# Docker version 28.0.4

docker images
# redis:7.2 이미지 준비 (없으면 아래 명령어로 pull)
docker pull redis:7.2

2. 컨테이너 vs VM — 왜 컨테이너가 가벼운가

VM의 구조

VM은 하드웨어를 통째로 가상화한다. Hypervisor 위에 Guest OS를 올리고, 그 위에 앱을 올리는 구조다.

[Hardware]
    └── Hypervisor
            ├── Guest OS (수 GB) → App A
            └── Guest OS (수 GB) → App B

Guest OS 자체가 수 GB 단위이고, 부팅에도 수십 초~수 분이 걸린다.

컨테이너의 구조

컨테이너는 Guest OS를 올리지 않는다. Host OS의 커널을 공유하고, 앱 실행에 필요한 것만 패키징해서 올린다.

[Hardware]
    └── Host OS (커널 공유)
            ├── Container A (MB 단위)
            └── Container B (MB 단위)

MB 단위이고, 시작도 수 초 안에 완료된다.

이걸 가능하게 하는 리눅스 커널 기능 2가지

기능 역할

namespace 컨테이너마다 독립된 프로세스/네트워크/파일시스템 공간 제공. 컨테이너 입장에서는 자기만의 세계처럼 보임
cgroup CPU, 메모리 등 리소스 사용량을 컨테이너별로 제한하고 격리

실무 연결 K8s에서 Pod에 아래처럼 리소스 제한을 설정하는 것이 바로 cgroup을 활용하는 것이다.

resources:
  limits:
    cpu: "500m"
    memory: "256Mi"

3. Image 레이어 구조

레이어란?

Docker Image는 여러 레이어가 쌓인 구조다. 각 레이어는 이전 레이어와의 차이(diff)만 저장한다.

Layer 8: CMD ["redis-server"]          ← 컨테이너 시작 명령어
Layer 7: COPY docker-entrypoint.sh    ← 실행 스크립트
Layer 6: WORKDIR /data                ← 작업 디렉토리
Layer 5: RUN mkdir /data              ← 데이터 디렉토리 생성
Layer 4: RUN redis 소스 컴파일        ← 38.1MB (무거운 레이어)
Layer 3: RUN gosu 설치               ← 4.28MB
Layer 2: RUN apt-get install          ← 기본 패키지
Layer 1: Debian bookworm (Base OS)    ← 108MB (제일 무거운 레이어)

레이어는 읽기 전용

모든 레이어는 읽기 전용(Read-Only)이다. 컨테이너를 실행하면, 이 읽기 전용 레이어들 위에 얇은 쓰기 가능 레이어가 하나 추가된다.

[ 쓰기 가능 레이어 ]  ← 컨테이너 실행 시 추가. 컨테이너 삭제 시 같이 사라짐
[ Layer 8 ] (읽기 전용)
[ Layer 7 ] (읽기 전용)
[ Layer 6 ] (읽기 전용)
     ...
[ Layer 1 ] (읽기 전용)

컨테이너 안에서 파일을 수정하면 쓰기 가능 레이어에만 기록된다. 컨테이너를 삭제하면 이 레이어도 같이 사라진다 — 그래서 데이터가 날아가는 것이다.

Union Filesystem (overlayfs)

여러 레이어를 하나의 파일시스템처럼 합쳐서 보여주는 기술이다. docker inspect에서 확인할 수 있다.

docker image inspect redis:7.2
"GraphDriver": {
    "Name": "overlayfs"   ← Union Filesystem 구현체
},
"RootFS": {
    "Type": "layers",
    "Layers": [
        "sha256:dc784058...",   ← Layer 1
        "sha256:1a60e8b5...",   ← Layer 2
        ...
        "sha256:7965070342..."  ← Layer 8
    ]
}

레이어 히스토리 확인

docker history redis:7.2
IMAGE          CREATED BY                                SIZE
8e674b7c7ae4   CMD ["redis-server"]                      0B
<missing>      EXPOSE map[6379/tcp:{}]                   0B
<missing>      ENTRYPOINT ["docker-entrypoint.sh"]       0B
<missing>      COPY docker-entrypoint.sh /usr/local/bin  20.5kB
<missing>      WORKDIR /data                             4.1kB
<missing>      VOLUME [/data]                            0B
<missing>      RUN mkdir /data && chown redis:redis...   8.19kB
<missing>      RUN set -eux; redis 소스 컴파일...         38.1MB  ← 무거운 레이어
<missing>      ENV REDIS_VERSION=7.2.13                  0B      ← 환경변수는 0B
<missing>      RUN apt-get install gosu...               4.28MB
<missing>      RUN apt-get update...                     41kB
<missing>      RUN groupadd -r redis...                  41kB
<missing>      # debian.sh bookworm...                   108MB   ← Base OS

포인트 두 가지

  • ENV는 파일시스템 변화가 없으므로 용량이 0B
  • 용량이 큰 레이어(Base OS, 컴파일)가 어딘지 보인다 → Chapter 2 Dockerfile 최적화의 기반

실무 연결 레이어 구조를 이해해야 Dockerfile 캐시 전략을 짤 수 있다. 자주 바뀌는 코드는 뒤에, 잘 안 바뀌는 라이브러리 설치는 앞에 배치하는 것이 기본 원칙이다. 이 내용은 Chapter 2에서 직접 실습한다.


4. Image → Container 생성 원리

docker run을 실행하는 순간 내부에서 일어나는 일:

① Image를 로컬에서 검색
   └── 없으면 Registry(Docker Hub)에서 pull

② Image 레이어들 위에 쓰기 가능 레이어 추가

③ namespace로 격리된 공간 생성
   (프로세스 / 네트워크 / 파일시스템)

④ cgroup으로 리소스 제한 설정

⑤ 컨테이너 프로세스 실행

Host OS 입장에서 컨테이너는 그냥 프로세스다

docker inspect로 확인하면:

"State": {
    "Status": "running",
    "Pid": 605   ← Host OS에서의 실제 프로세스 ID
}

Host OS 입장에서는 PID 605번 프로세스다. 컨테이너 내부에서는 namespace 덕분에 PID 1로 보인다. 격리된 것처럼 보일 뿐, 실제로는 Host OS 위에서 돌아가는 프로세스다.


5. 핵심 명령어 실습

컨테이너 실행

docker run -d --name redis-test redis:7.2

옵션 설명

-d 백그라운드 실행 (detached)
--name 컨테이너 이름 지정

자주 쓰는 명령어 정리

# 실행 중인 컨테이너 확인
docker ps

# 중지된 컨테이너 포함 전체 확인
docker ps -a

# 컨테이너 내부에서 명령어 실행
docker exec -it redis-test redis-cli ping
# PONG

# 컨테이너 로그 확인 (장애 시 제일 먼저 보는 명령어)
docker logs redis-test

# 컨테이너 상세 정보 확인
docker inspect redis-test

# 컨테이너 중지 / 삭제
docker stop redis-test
docker rm redis-test

# 이미지 목록 확인
docker images

# 이미지 삭제
docker rmi redis:7.2

# 이미지 다운로드 (레이어 단위로 내려받는 것 확인 가능)
docker pull redis:7.2

docker pull 시 레이어 단위 다운로드 확인

docker pull redis:7.2

7.2: Pulling from library/redis
9b802525d298: Pull complete   ← Layer 1
e1a1f94098c7: Pull complete   ← Layer 2
c9c9059745d0: Pull complete   ← Layer 3
9f0514948e1f: Pull complete   ← Layer 4
d7d4fb1d90cf: Pull complete   ← Layer 5
bc1b38586f4d: Pull complete   ← Layer 6
4f4fb700ef54: Pull complete   ← Layer 7
46ac7a0b9811: Pull complete   ← Layer 8

docker history에서 봤던 8개 레이어가 그대로 하나씩 내려온다.

실무 연결 레이어가 이미 로컬에 있으면 Already exists로 건너뛴다. 여러 이미지가 같은 Base OS 레이어를 공유하면 중복 다운로드가 없다. 이것이 Docker가 디스크를 효율적으로 쓰는 원리다.


6. 전체 흐름 요약

Dockerfile
    │
    │ docker build
    ▼
Image (읽기 전용 레이어들)
    │
    │ docker run
    ▼
Container
    ├── 읽기 전용 레이어 (Image 그대로)
    └── 쓰기 가능 레이어 (컨테이너 삭제 시 소멸)

항목 설명

namespace 컨테이너별 독립 공간 제공 (프로세스/네트워크/파일시스템)
cgroup 컨테이너별 리소스 제한
overlayfs 여러 레이어를 하나의 파일시스템으로 합침
쓰기 가능 레이어 컨테이너 실행 시 추가. 삭제 시 소멸 → 데이터 날아가는 이유

➡️ 다음 파트 예고

Chapter 1에서는 남이 만든 이미지(redis:7.2)의 구조를 분석했다.

Chapter 2에서는 내 앱(Spring Boot)을 직접 이미지로 만드는 법을 다룬다. Dockerfile 문법, 레이어 캐시 전략, Multi-stage Build로 이미지 크기를 줄이는 실습까지 진행한다.