Docker 베이스 이미지 이해하기: 컨테이너 속 Ubuntu는 진짜 Ubuntu가 아님
(oneuptime.com)-
docker run ubuntu를 실행해도 호스트 Linux 커널을 공유하며 Ubuntu는 사용자 공간 도구만 제공함 -
uname -r결과가 호스트 커널 버전으로 표시되고,/etc/os-release만 Ubuntu 정보로 나타남 - VM은 각각 고유한 커널을 가지고 부팅에 수 분이 걸리지만, 컨테이너는 밀리초 단위로 시작되고 하드웨어 가상화 없이 OS 수준 격리로 호스트 커널을 공유하여 오버헤드가 낮음
- 리눅스 시스템 콜 ABI의 안정성으로 다양한 배포판 컨테이너가 동일 커널에서 동작 가능함
- 16GB RAM 환경에서 경량 컨테이너는 50-100개, 중간 규모는 10-30개, 대형 컨테이너는 5-10개 정도가 실용적인 한계
- 이 아키텍처 이해가 중요한 이유는 커널 취약점이 모든 컨테이너에 영향을 미치고, 베이스 이미지 선택이 호환성과 보안에 직접적 영향을 주기 때문
“Ubuntu를 실행한다”는 의미
-
docker run ubuntu:22.04실행 시 Ubuntu처럼 보이는 bash 프롬프트를 얻고apt update와 패키지 설치가 가능 - 그러나 컨테이너 내부에서
uname -r실행 시 호스트 커널 버전(예: 6.5.0-44-generic)이 표시됨 -
/etc/os-release파일은 Ubuntu 22.04로 표시되지만, 커널은 호스트 머신의 것이며 "Ubuntu" 부분은 사용자 공간을 구성하는 파일시스템에 불과
컨테이너 vs 가상 머신: 아키텍처 비교
- VM은 하드웨어를 가상화하고, 컨테이너는 운영체제를 가상화
- 주요 차이점:
- 커널: VM은 각각 고유 커널 보유, 컨테이너는 호스트 커널 공유
- 부팅 시간: VM은 수 분, 컨테이너는 밀리초
- 메모리 오버헤드: VM은 512MB-4GB, 컨테이너는 1-10MB
- 디스크 사용량: VM은 10-100GB, 컨테이너 이미지는 10-500MB
- 격리 수준: VM은 하드웨어 레벨, 컨테이너는 프로세스 레벨
- 성능: VM은 약 5-10% 오버헤드, 컨테이너는 네이티브에 가까운 성능
베이스 이미지의 실제 구성 요소
-
ubuntu:22.04풀 시 다운로드되는 tarball 내용: -
1. 필수 바이너리 (
/bin,/usr/bin)-
/bin/bash(셸),/bin/ls(파일 목록),/bin/cat(파일 표시) -
/usr/bin/apt(패키지 관리자),/usr/bin/dpkg(Debian 패키지 도구)
-
-
2. 공유 라이브러리 (
/lib,/usr/lib)- glibc와 기타 프로그램이 링크하는 공유 라이브러리
-
/lib/x86_64-linux-gnu/libc.so.6(C 라이브러리 - 모든 C 프로그램의 기반) -
libpthread.so.0,libm.so.6등 필수 라이브러리
-
3. 설정 파일 (
/etc)-
/etc/apt/sources.list(패키지 저장소) -
/etc/passwd(사용자 데이터베이스) -
/etc/resolv.conf(DNS 설정, 보통 호스트에서 마운트)
-
-
4. 패키지 데이터베이스
-
/var/lib/dpkg/status(설치된 패키지) -
/var/lib/apt/lists/(사용 가능한 패키지 캐시)
-
- 커널·부트로더·드라이버는 포함되지 않음
커널은 그대로, 모든 것이 변함
- Linux 커널이 제공하는 기능: 프로세스 스케줄링, 메모리 관리, 파일시스템 연산, 네트워크 스택, 디바이스 드라이버, 시스템 콜
- 컨테이너 프로세스가
open(),read(),fork()를 호출하면 호스트 커널로 직접 전달 - 커널은 해당 프로세스가 "Ubuntu 컨테이너"인지 "Alpine 컨테이너"인지 알지도 신경 쓰지도 않음
-
시스템 콜 인터페이스의 안정성
- Linux syscall ABI는 매우 안정적
- glibc 2.31(Ubuntu 20.04)에서 컴파일된 바이너리가 Ubuntu 24.04 커널에서도 동작하는 이유:
- 커널이 하위 호환성 유지
- 시스템 콜 번호 변경 없음
- 새 기능은 추가되지만 기존 기능은 거의 제거되지 않음
- 커널 6.5를 실행하는 호스트에서 Ubuntu 18.04 컨테이너 실행이 가능한 이유
실습 해보기: 동일 커널, 다른 사용자 공간
- 여러 베이스 이미지에서 동일한 커널 쿼리 실행 시 모든 이미지가 호스트 커널을 공유한다는 것을 볼수 있음
- ubuntu:22.04, debian:12, alpine:3.19, fedora:39, archlinux:latest 모두 동일한 커널 버전(6.5.0-44-generic)임
- 컨테이너별 차이는
uname바이너리와 libc 등 유저랜드 구성임
컨테이너가 매우 효율적인 이유
-
1. 커널 중복 없음
- VM은 각각 전체 커널을 메모리에 로드(약 100-500MB)
- 10개 VM은 10개 커널이 메모리 소비, 10개 컨테이너는 커널 하나만 사용
-
2. 즉시 시작
- VM 부팅 순서: BIOS → 부트로더 → 커널 → init 시스템 → 서비스
- 컨테이너는
fork()와exec()호출만으로 밀리초 내 프로세스 존재 - 일반적인 VM 부팅: 30-60초 / 컨테이너 시작: 약 0.347초
-
3. 공유 이미지 레이어
-
ubuntu:22.04에서 100개 컨테이너 실행 시 베이스 이미지 레이어는 디스크에 한 번만 존재 - 각 컨테이너는 변경 사항을 위한 얇은 copy-on-write 레이어만 획득
-
-
4. 커널을 통한 메모리 공유
- 커널의 페이지 캐시가 공유됨
- 50개 컨테이너가 동일 파일 읽기 시 커널은 한 번만 캐시
- 동일한 공유 라이브러리 사용 시 copy-on-write로 메모리 페이지 공유 가능
컨테이너 실행 가능 한계 계산
-
메모리 분석 (16GB RAM VM 기준)
- 총 RAM: 16,384 MB
- 호스트 OS 오버헤드: -1,024 MB
- Docker 데몬: -256 MB
- 컨테이너 런타임 오버헤드: -512 MB
- 컨테이너 가용량: 14,592 MB
-
컨테이너 유형별 메모리 사용량
- 최소(sleep): 약 1MB
- Alpine + 소형 앱: 약 25MB
- Ubuntu + Python 앱: 약 120MB
- Ubuntu + Java 앱: 약 500MB
- Node.js 서비스: 약 200MB
-
이론적 최대치
- 최소 컨테이너(1MB): 14,592개
- Alpine + 소형 앱(25MB): 583개
- Ubuntu + Python(120MB): 121개
- Java 마이크로서비스(500MB): 29개
-
실제 한계
- 메모리 외 고려사항:
- CPU 스케줄링: 너무 많은 컨테이너 경쟁 시 지연 스파이크 발생
- 파일 디스크립터: 기본 ulimit 1024
- 네트워크 포트: 포트 매핑에 65,535개만 사용 가능
-
PIDs:
/proc/sys/kernel/pid_max제한(기본: 32,768) - 디스크 I/O: OverlayFS 오버헤드, 많은 레이어 탐색 필요
- 16GB VM에서 실제 워크로드 실행 시 실용적 한계:
- 경량 컨테이너(API, 워커): 50-100개
- 중간 컨테이너(DB, 캐시): 10-30개
- 대형 컨테이너(ML 모델, JVM 앱): 5-10개
- 메모리 외 고려사항:
Linux 배포판 호환성
-
커널 ABI 약속
- Linux는 안정적인 syscall 인터페이스 유지
- 오래된 커널용으로 컴파일된 바이너리가 새 커널에서 동작
- Ubuntu 18.04 바이너리가 커널 6.5에서 정상 실행
-
호환성이 깨지는 경우
- 커널 기능 요구사항: 컨테이너가 커널에 없는 기능 필요 시 (예: io_uring은 커널 5.1+ 필요)
- 커널 모듈 의존성: Wireguard는 wireguard 커널 모듈 필요, NVIDIA 컨테이너는 nvidia 커널 드라이버 필요
-
Seccomp/capability 제한: 호스트가 컨테이너에 필요한 syscall 차단 시 (예: ptrace 사용 시
--cap-add SYS_PTRACE필요)
베이스 이미지 선택 가이드
| 베이스 이미지 | 크기 | 패키지 관리자 | 용도 |
|---|---|---|---|
scratch |
0 MB | 없음 | 정적 컴파일된 Go/Rust 바이너리 |
alpine |
7 MB | apk | 최소 컨테이너, musl libc |
distroless |
20 MB | 없음 | 보안 중심, 셸·패키지 관리자 없음 |
debian-slim |
80 MB | apt | 크기와 호환성 균형 |
ubuntu |
78 MB | apt | 개발 친숙성 |
fedora |
180 MB | dnf | 최신 패키지, SELinux |
-
각 이미지 사용 시점
- scratch: 정적 컴파일 바이너리용, OS 전혀 없이 바이너리만 포함
- alpine: 셸 접근이 필요한 최소 이미지, glibc 대신 musl libc 사용으로 일부 호환성 문제 가능
- distroless: 보안 중심 프로덕션 이미지, 셸과 패키지 관리자 없어 디버깅 어렵지만 더 안전
사용자 공간-커널 경계
-
베이스 이미지에서 오는 것 (사용자 공간)
- 셸 (
/bin/bash,/bin/sh) - C 라이브러리 (glibc, musl)
- 패키지 관리자 (apt, apk, yum)
- 핵심 유틸리티 (ls, cat, grep)
- init 시스템 설정 (보통 systemd 자체는 아님)
- 기본 사용자와 그룹 (
/etc/passwd) - 라이브러리 경로와 설정
- 셸 (
-
호스트에서 오는 것 (커널)
- 프로세스 스케줄링과 메모리 관리
- 네트워크 스택 (TCP/IP, 라우팅)
- 파일시스템 연산 (읽기, 쓰기, 마운트)
- 보안 기능 (네임스페이스, cgroups, seccomp)
- 디바이스 드라이버 (GPU, 네트워크, 스토리지)
- 시간과 클럭 관리
- 암호화와 난수 생성
-
네임스페이스가 만들어 내는 환각
- 커널이 네임스페이스를 제공하여 컨테이너가 격리된 것처럼 느끼게 함
- 컨테이너 내부에서 PID 1로 보이는 프로세스가 호스트에서는 더 높은 PID(예: 45678)로 존재
- 커널이 매핑 유지: 컨테이너 PID 1 → 호스트 PID 45678
- 가상화 없이 격리가 작동하는 방식
프로덕션 환경에서의 의미
-
1. 커널 취약점이 모든 컨테이너에 영향
- 호스트 커널에 취약점이 있으면 모든 컨테이너가 노출됨
- 호스트 패치 유지 필수
-
2. 호스트 커널이 컨테이너 기능 제한
- io_uring 사용하려면 호스트 커널 5.1+ 필요
- eBPF 기능은 특정 옵션이 활성화된 커널 4.15+ 필요
-
3. glibc vs musl 중요성
- Alpine은 musl libc 사용
- glibc용으로 컴파일된 일부 바이너리가 동작하지 않을 수 있음
- 예: Alpine에서 glibc 바이너리 실행 시
/lib/x86_64-linux-gnu/libc.so.6파일 없음 오류 발생 가능
-
4. 컨테이너 "OS"는 순전히 조직적 개념
- 커널 관점에서 "Ubuntu 컨테이너"와 "Debian 컨테이너"는 차이 없음
- 모두 syscall을 만드는 프로세스일 뿐
흔한 오해
- ❌ "컨테이너는 경량 VM": 컨테이너는 고급 격리를 가진 프로세스이며, VM은 하드웨어를 가상화하고 별도 커널 실행
- ❌ "각 컨테이너가 고유 커널 보유": 모든 컨테이너가 호스트 커널 공유, 컨테이너의 "OS"는 사용자 공간 파일일 뿐
- ❌ "Ubuntu 컨테이너 실행 = Ubuntu 실행": 호스트 커널과 Ubuntu 도구 실행, 호스트가 Debian이면 실제로는 Debian 커널 실행
- ❌ "베이스 이미지에 완전한 운영체제 포함": 베이스 이미지는 최소 사용자 공간 도구만 포함, 커널·부트로더·드라이버 없음
- ❌ "더 많은 컨테이너 = 더 많은 메모리": 공유 레이어와 커널 페이지 캐싱으로 컨테이너가 종종 효율적으로 메모리 공유
핵심 요약
- Docker 베이스 이미지는 Linux 배포판의 사용자 공간 구성 요소에 대한 파일시스템 스냅샷
- Ubuntu를 Ubuntu처럼 느끼게 하는 바이너리, 라이브러리, 설정
- 실제 운영체제인 커널은 호스트와 공유함
- 이 아키텍처가 가능하게 하는 것:
- 밀리초 단위 시작 시간 (커널 부팅 없음)
- 최소한의 메모리 오버헤드 (하나의 커널, 공유 페이지)
- 대규모 밀도 (호스트당 수백 개 컨테이너)
- 네이티브에 가까운 성능 (커널로의 직접 syscall)
- 트레이드오프는 VM보다 약한 격리 - 컨테이너가 커널을 공유하므로 커널 익스플로잇이 모든 컨테이너에 영향
- 대부분의 워크로드에서 이 트레이드오프는 가치 있음
https://news.hada.io/topic?id=9531
그래서 좀 과장되게 말해서 chroot + cgroup = docker 이렇게 봐도 되겠더라고요