Docker 베이스 이미지 이해하기 - Ubuntu 컨테이너는 사실 Ubuntu가 아니라고? 이게 무슨말인것이라?
Summary Docker 컨테이너는 호스트의 Linux 커널을 공유하며, 사용자 공간 도구만 포함된 구조로, Ubuntu 컨테이너도 실제로는 호스트 커널 위에서 동작한다. VM과의 주요 차이는 가상화 방식에 있으며, 컨테이너는 빠른 시작과 낮은 메모리 오버헤드를 제공한다. 베이스 이미지는 최소한의 사용자 공간 구성 요소만 포함하고, 커널과 드라이버는 포함되지 않는다. 보안과 운영 측면에서 호스트 패치 유지가 중요하며, 컨테이너는 경량 VM이 아니라 고급 격리를 가진 프로세스이다.

[주제 1: Docker 컨테이너의 근본적 정체성 - “Ubuntu 실행"의 실체]
docker run ubuntu:22.04 명령을 실행하면 Ubuntu처럼 보이는 bash 프롬프트가 나타나고 apt update와 패키지 설치가 가능하지만, 컨테이너 내부에서 uname -r을 실행하면 호스트 커널 버전(예: 6.5.0-44-generic)이 출력된다. /etc/os-release 파일은 Ubuntu 22.04로 표시되지만 실제 커널은 호스트 머신의 것이며, “Ubuntu"라 불리는 부분은 사용자 공간(userland)을 구성하는 파일시스템에 불과하다.
이 사실은 컨테이너 기술의 핵심 원리를 드러낸다. 컨테이너는 호스트의 Linux 커널을 공유하면서 해당 배포판의 사용자 공간 도구(바이너리, 라이브러리, 설정 파일)만 레이어로 얹는 구조다. 커널 관점에서 “Ubuntu 컨테이너"와 “Debian 컨테이너"는 구분되지 않으며, 둘 다 단순히 시스템 콜을 수행하는 프로세스일 뿐이다. 호스트가 Debian 커널을 구동 중이라면 Ubuntu 컨테이너를 실행해도 실제로는 Debian 커널 위에서 동작하는 것이다.

[주제 2: 컨테이너와 가상 머신의 아키텍처 비교]
VM과 컨테이너의 근본적 차이는 가상화 대상에 있다. VM은 하드웨어를 가상화하여 각각 고유한 커널을 보유하는 반면, 컨테이너는 운영체제를 가상화하여 모든 컨테이너가 호스트 커널을 공유한다.
주요 차이점을 정량적으로 비교하면, 부팅 시간에서 VM은 수 분이 소요되는 반면 컨테이너는 밀리초 단위로 시작된다. 메모리 오버헤드의 경우 VM은 개당 512MB에서 4GB를 소비하지만 컨테이너는 1MB에서 10MB 수준이다. 디스크 사용량 역시 VM이 10GB에서 100GB인 데 비해 컨테이너 이미지는 10MB에서 500MB 범위다. 격리 수준 측면에서 VM은 하드웨어 레벨 격리를 제공하고 컨테이너는 프로세스 레벨 격리를 제공한다. 성능 면에서 VM은 약 5~10%의 오버헤드가 발생하나 컨테이너는 네이티브에 가까운 성능을 보인다.

[주제 3: 베이스 이미지의 실제 구성 요소]
ubuntu:22.04를 풀(pull)할 때 다운로드되는 tarball에는 커널, 부트로더, 드라이버가 포함되지 않는다. 포함되는 것은 다음과 같은 사용자 공간 구성 요소뿐이다.
필수 바이너리로는 /bin/bash(셸), /bin/ls(파일 목록), /bin/cat(파일 표시), /usr/bin/apt(패키지 관리자), /usr/bin/dpkg(Debian 패키지 도구) 등이 있다. 공유 라이브러리에는 glibc를 포함한 C 라이브러리(/lib/x86_64-linux-gnu/libc.so.6)와 libpthread.so.0, libm.so.6 등 필수 라이브러리가 해당한다. 설정 파일로는 /etc/apt/sources.list(패키지 저장소), /etc/passwd(사용자 데이터베이스), /etc/resolv.conf(DNS 설정, 통상 호스트에서 마운트) 등이 있다. 패키지 데이터베이스는 /var/lib/dpkg/status(설치된 패키지)와 /var/lib/apt/lists/(사용 가능한 패키지 캐시)로 구성된다.

[주제 4: 시스템 콜 인터페이스의 안정성과 호환성]
Linux 커널은 프로세스 스케줄링, 메모리 관리, 파일시스템 연산, 네트워크 스택, 디바이스 드라이버, 시스템 콜 등의 기능을 제공한다. 컨테이너 프로세스가 open(), read(), fork()를 호출하면 해당 호출은 호스트 커널로 직접 전달되며, 커널은 그 프로세스가 어느 배포판 컨테이너에서 왔는지 알지도 못하고 관심도 없다.
Linux syscall ABI의 안정성 덕분에 glibc 2.31(Ubuntu 20.04)에서 컴파일된 바이너리가 Ubuntu 24.04 커널에서도 동작한다. 커널이 하위 호환성을 유지하고, 시스템 콜 번호가 변경되지 않으며, 새 기능은 추가되지만 기존 기능은 거의 제거되지 않기 때문이다. 이로 인해 커널 6.5를 실행하는 호스트에서 Ubuntu 18.04 컨테이너를 실행하는 것이 가능하다.
단, 호환성이 깨지는 예외 상황도 존재한다. 컨테이너가 커널에 없는 기능을 요구하는 경우(예: io_uring은 커널 5.1+ 필요), 특정 커널 모듈 의존성이 있는 경우(Wireguard는 wireguard 커널 모듈 필요, NVIDIA 컨테이너는 nvidia 커널 드라이버 필요), 호스트가 seccomp나 capability 제한으로 컨테이너에 필요한 시스템 콜을 차단하는 경우가 해당한다.

[주제 5: 컨테이너 효율성의 기술적 근거]
컨테이너가 효율적인 이유는 네 가지 기술적 요인으로 설명된다.
첫째, 커널 중복이 없다. VM은 각각 전체 커널을 메모리에 로드하여 약 100~500MB를 소비하므로 10개 VM은 10개 커널이 메모리를 점유한다. 반면 10개 컨테이너는 커널 하나만 사용한다.
둘째, 즉시 시작이 가능하다. VM은 BIOS에서 부트로더, 커널, init 시스템, 서비스까지 순차적으로 부팅해야 하지만, 컨테이너는 fork()와 exec() 호출만으로 밀리초 내에 프로세스가 생성된다. 일반적인 VM 부팅은 30~60초가 소요되는 반면 컨테이너 시작은 약 0.347초 수준이다.
셋째, 이미지 레이어가 공유된다. ubuntu:22.04에서 100개 컨테이너를 실행해도 베이스 이미지 레이어는 디스크에 한 번만 존재하며, 각 컨테이너는 변경 사항을 위한 얇은 copy-on-write 레이어만 획득한다.
넷째, 커널을 통한 메모리 공유가 이루어진다. 커널의 페이지 캐시가 공유되어 50개 컨테이너가 동일 파일을 읽을 때 커널은 한 번만 캐시한다. 동일한 공유 라이브러리를 사용할 경우에도 copy-on-write로 메모리 페이지가 공유될 수 있다.

[주제 6: 컨테이너 실행 한계 계산]
16GB RAM 환경을 기준으로 실제 가용 메모리를 계산하면, 총 RAM 16,384MB에서 호스트 OS 오버헤드 1,024MB, Docker 데몬 256MB, 컨테이너 런타임 오버헤드 512MB를 제외하면 컨테이너용 가용량은 14,592MB가 된다.
컨테이너 유형별 메모리 사용량은 최소(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개만 사용 가능하다. PID는 /proc/sys/kernel/pid_max 제한(기본 32,768)이 있다. 디스크 I/O는 OverlayFS 오버헤드로 인해 많은 레이어 탐색이 필요하다.
따라서 16GB VM에서 실제 워크로드 실행 시 실용적 한계는 경량 컨테이너(API, 워커)가 50100개, 중간 컨테이너(DB, 캐시)가 1030개, 대형 컨테이너(ML 모델, JVM 앱)가 5~10개 정도다.

[주제 7: 베이스 이미지 선택 기준]
베이스 이미지 선택은 용도에 따라 달라진다. scratch는 0MB로 패키지 관리자 없이 정적 컴파일된 Go/Rust 바이너리용이다. alpine은 7MB로 apk 패키지 관리자를 사용하며 최소 컨테이너와 musl libc 환경에 적합하나, glibc 대신 musl libc를 사용하여 일부 호환성 문제가 발생할 수 있다. distroless는 20MB로 패키지 관리자 없이 보안 중심 이미지로서 셸과 패키지 관리자가 없어 디버깅이 어렵지만 더 안전하다. debian-slim은 80MB로 apt를 사용하며 크기와 호환성의 균형을 제공한다. ubuntu는 78MB로 apt를 사용하며 개발 친숙성을 제공한다. fedora는 180MB로 dnf를 사용하며 최신 패키지와 SELinux를 지원한다.
glibc와 musl의 차이는 실무에서 중요하다. Alpine에서 glibc용으로 컴파일된 바이너리를 실행하면 /lib/x86_64-linux-gnu/libc.so.6 파일 없음 오류가 발생할 수 있다.

[주제 8: 프로덕션 환경에서의 보안 및 운영 고려사항]
프로덕션 환경에서 이 아키텍처의 이해가 중요한 이유는 다음과 같다. 커널 취약점이 모든 컨테이너에 영향을 미치므로 호스트 패치 유지가 필수적이다. 호스트 커널이 컨테이너 기능을 제한하므로 io_uring 사용 시 호스트 커널 5.1+가 필요하고, eBPF 기능은 특정 옵션이 활성화된 커널 4.15+가 필요하다.
네임스페이스는 커널이 제공하는 격리 환각(illusion)으로, 컨테이너 내부에서 PID 1로 보이는 프로세스가 호스트에서는 더 높은 PID(예: 45678)로 존재한다. 커널이 이 매핑을 유지하여 가상화 없이 격리가 작동하는 방식이다.
핵심 트레이드오프는 VM보다 약한 격리다. 컨테이너가 커널을 공유하므로 커널 익스플로잇이 모든 컨테이너에 영향을 미친다. 그러나 대부분의 워크로드에서 이 트레이드오프는 밀리초 단위 시작 시간, 최소한의 메모리 오버헤드, 대규모 밀도, 네이티브에 가까운 성능이라는 이점으로 정당화된다.

[주제 9: 흔한 오해의 정정]
“컨테이너는 경량 VM이다"라는 인식은 오류다. 컨테이너는 고급 격리를 가진 프로세스이며, VM은 하드웨어를 가상화하고 별도 커널을 실행한다.
“각 컨테이너가 고유 커널을 보유한다"도 틀렸다. 모든 컨테이너가 호스트 커널을 공유하며, 컨테이너의 “OS"는 사용자 공간 파일일 뿐이다.
“Ubuntu 컨테이너 실행은 Ubuntu 실행과 같다"는 것도 오해다. 실제로는 호스트 커널과 Ubuntu 도구를 실행하는 것이다.
“베이스 이미지에 완전한 운영체제가 포함된다"도 잘못된 인식이다. 베이스 이미지는 최소 사용자 공간 도구만 포함하며 커널, 부트로더, 드라이버는 없다.
“더 많은 컨테이너가 항상 더 많은 메모리를 의미한다"도 정확하지 않다. 공유 레이어와 커널 페이지 캐싱으로 컨테이너가 종종 효율적으로 메모리를 공유한다.

GitHub 계정으로 로그인하여 댓글을 남겨보세요. GitHub 로그인
댓글 시스템 설정이 필요합니다
GitHub Discussions 기반 댓글 시스템을 활성화하려면:
- Giscus 설정 페이지에서 설정 생성
- GISCUS_SETUP_GUIDE.md 파일의 안내를 따라 설정 완료
- Repository의 Discussions 기능 활성화
Repository 관리자만 설정할 수 있습니다.