Docker가 2013년 첫 공개 이후 Linux 네임스페이스에서 macOS·Windows로의 확장, 네트워킹·스토리지 설계, 멀티아키텍처와 TEE·GPU 지원 등 최신 개발 워크플로에 맞춰 진화해 온 기술적 기반을 살펴본다.
Docker는 널리 사용되는 개발자 도구로, 먼저 애플리케이션 스택의 조립을 단순화하고(docker build), 이어서 생성된 실행 파일과 데이터의 빠른 배포를 가능하게 하며(docker push), 그다음에는 동일한 머신에서 여러 애플리케이션을 서로 격리해 실행할 수 있도록 지원한다(docker run). 개발자는 소스 코드와 나란히 단일 Dockerfile을 통해 자신의 Docker 이미지를 컴파일할 수 있고, 다른 공개 이미지를 재사용해 전 세계의 프로그래밍 언어와 애플리케이션 스택 전반에서 패키징 노력을 공유할 수 있다.
Docker가 2013년에 처음 공개된 이후14, Proxima Fusion의 스텔러레이터 시뮬레이션부터 Netflix의 스트리밍 서비스, BalenaOS로 우주에서 소프트웨어를 배포하는 사례에 이르기까지 다양한 분야에서 빠르게 채택되었다. 또한 개발자들이 사용을 즐기는 도구처럼 보이며, Stack Overflow 커뮤니티 순위에서 “가장 선호되는” 그리고 “가장 많이 사용되는” 개발자 도구로 꾸준히 최상위권에 오른다.a 이미지 공유가 가능한 여러 레지스트리 중 하나에 불과한 Docker Hub에는 1,400만 개가 넘는 애플리케이션 이미지가 호스팅되어 있으며, 매달 110억 회가 넘는 이미지 풀(pull)을 제공한다.
Docker의 인기는 많은 개발자가 오랫동안 마주해 온 문제, 즉 점점 더 다양한 언어로 작성되는 마이크로서비스를 어떻게 개발하고 배포할 것인가를 해결했기 때문이다.26 Kubernetes 같은 멀티테넌트 플랫폼에서 클라우드 네이티브 애플리케이션을 관리하는 사실상의 표준이 되었고,5 재현 가능한 과학 연구를 위한 기준을 더 높은 수준으로(아직 완벽하진 않지만) 끌어올렸다.4
하지만 겉보기에는 단순한 커맨드라인 인터페이스 뒤를 들여다보면 Docker는 _무엇_일까? Docker는 운영체제 스택 전반에 걸친 수십 년의 발전을 기반으로 하는 시스템이며, 마찰 없는 개발자 경험을 제공하려는 과정에서 원래의 공개 이후에도 많은 시스템 연구를 흡수하며 진화해 왔다. 이 글에서는 Linux에서의 기원부터 시작해, 사용성을 해치지 않으면서 macOS와 Windows에서 동작하도록 어떻게 재구축했는지까지 Docker의 기술적 토대를 설명한다. 오늘날에는 AI 기반 워크로드로 인해 개발자 워크플로가 빠르게 진화하고 있으므로, GPGPU와 FPGA 같은 이종 하드웨어를 지원하도록 적응해 가는 Docker의 미래도 논의한다.
2000년대 초반에는 수많은 의존성을 포함한 Linux 배포판을 수동으로 설치하고, 새 머신에서 실행할 소프트웨어 묶음을 직접 컴파일하고 구성하는 일이 흔했다.11 2010년 무렵에는 클라우드 컴퓨팅의 부상으로 이 과정이 더 복잡해졌는데, 애플리케이션이 서로 다른 자원 요구사항을 가진 호스트들에 걸친 여러 가상 머신에서 실행되기를 기대하게 되었기 때문이다.13 Docker는 개발자가 자신의 애플리케이션과 모든 의존성을 일련의 파일시스템 이미지, 즉 “컨테이너”로 패키징할 수 있게 함으로써 이 과정을 단순화했으며, Docker만 설치되어 있으면 어떤 머신에서도 실행할 수 있게 했다. 그리고 전체 운영체제를 설치해야 했던 가상 머신 경험과 달리, 몇 개의 명령만으로 곧바로 실행할 수 있었다.
전형적인 워크플로. Docker를 사용하는 개발자는 Dockerfile을 작성하는데, 이는 익숙한 셸 문법을 확장해 애플리케이션을 단계별로 빌드하는 방법을 기술한다. 예를 들어 Python 기반 웹사이트는 다음과 같은 Dockerfile을 가질 수 있다.
FROM python:3
COPY requirements.txt /app/requirements.txt
WORKDIR /app
RUN pip install -r requirements.txt
COPY . /app
EXPOSE 80
CMD ["python", "app.py"]
그다음 개발자는 docker build를 실행해 이 Dockerfile을 수행함으로써 Docker 컨테이너 이미지를 생성한다. 이 이미지는 중앙 이미지 레지스트리 역할을 하는 Docker Hub로 푸시할 수 있다.
$ docker build -t avsm/my-python-app .
$ docker push avsm/my-python-app
이미지는 Docker가 설치된 어떤 머신에서도 다운로드해 실행할 수 있다. 예컨대 로컬 데이터 볼륨을 마운트하고 호스트에 단일 네트워크 포트를 노출해 실행하려면, 사용자는 다음을 실행하면 된다.
docker run -v data:/app/data -p 80:80 avsm/my-python-app
이제 애플리케이션은 컨테이너 안에서 실행되며, 호스트 시스템 및 같은 머신에서 실행되는 다른 컨테이너들과 격리된다. 개발자는 애플리케이션을 반복적으로 개선하고, 새 버전을 릴리스할 준비가 되면 이미지를 다시 빌드해 Docker Hub로 푸시한다. 사용자는 서로 독립적으로 자신이 사용하는 이미지를 업데이트할 수 있으며, 동일한 소프트웨어의 서로 다른 버전 사이 충돌을 걱정할 필요가 없다.
Docker 커맨드라인 인터페이스는 수년에 걸쳐 훨씬 더 많은 명령을 포함하도록 진화했고, 사용하는 백엔드 시스템도 완전히 재설계되었지만, Dockerfile을 작성하고 docker build와 docker run을 사용하는 원래 워크플로는 2013년 이후 일관되게 유지되어 왔다. GitHub 검색을 해보면 공개 저장소 루트에 340만 개가 넘는 Dockerfile이 존재하는데, 이는 거의 모든 유형의 소프트웨어 프로젝트에서 이 배포 메커니즘이 얼마나 인기 있는지 보여준다.17
내부 동작. 이제 Linux 내부에서 Docker 컨테이너가 더 낮은 수준에서 어떻게 동작하는지 이해해 보자. 운영체제 커널은 프로세스 메모리 공간을 서로 격리하지만, 의도적으로 여러 다른 유형의 시스템 자원은 공유한다.12 OS 커널은 구성 파일, 동적 라이브러리, 애플리케이션별 상태를 포함하는 단일 공유 파일시스템에서 부팅된다. 이는 편리하지만, 동적 라이브러리 요구사항이 충돌하는 경우 여러 애플리케이션을 동시에 설치하기가 매우 어렵다. 또한 프로세스들은 서로 통신해야 한다. 예를 들어 웹 프런트엔드는 백엔드 데이터베이스와 통신해야 한다. Linux는 네트워킹, Unix 시그널, Unix 도메인 소켓 등 여러 프로세스 간 통신 방법을 지원한다.34 이러한 공유 채널은 협력하는 프로세스에 필수적이지만, 애플리케이션 간 충돌이 있을 때—예컨대 네트워크 포트 선택에서—원치 않는 _간섭_을 유발할 수도 있다.
이러한 충돌을 해결하는 한 가지 접근은 각 애플리케이션을 개별 가상 머신(VM)에서 실행하는 것이다. 즉, 별도의 게스트 커널, 유저스페이스, 파일시스템을 사용하는 방식이다. 하이퍼바이저는 공유 하드웨어 위에서 여러 VM을 다중화한다.1 이는 효과적이지만 무겁다. 여러 커널, 중복 파일시스템, 중복 캐시, 브리지된 네트워크 인터페이스가 필요하고, 사용자가 단지 몇 개 애플리케이션을 빠르게 실행하고자 할 때도 상당한 복잡성을 초래한다. 또한 각 게스트 OS가 자신이 하드웨어의 유일한 사용자라고 가정하며 독립적으로 동작하기 때문에, 스토리지와 메모리를 효율적으로 중복 제거하기도 어렵다.36
이러한 도전들은 질문을 던졌다. 무거운 VM 대신 OS 프리미티브를 사용할 수 있을까? 1978년 Unix v7은 프로세스가 완전히 분리된 루트 파일시스템을 사용하도록 chroot()를 추가했지만, 서로 다른 애플리케이션에서 비롯된 여러 파일시스템을 합성하는 기능은 지원하지 않았다. Nix8와 Guix6 같은 시스템은 소프트웨어를 애플리케이션별로 구분된 디렉터리로 재패키징하고, 동적 링킹으로 올바른 라이브러리 버전을 해결하도록 요구한다. 이는 효과적이지만 모든 소프트웨어 패키징을 수정해야 하며, 독점 소프트웨어에는 항상 가능하지 않다. 또한 애플리케이션 간 네트워크 포트 충돌 문제를 해결하지 못하므로 부분적인 해법에 그친다.
Docker는 대신 Nix가 처음 만들어졌을 때에는 존재하지 않았던 Linux의 기능인 _네임스페이스(namespaces)_를 사용하기로 했다.38 네임스페이스는 각 프로세스가 파일과 디렉터리 같은 공유 자원에 접근하는 방식을 더 잘 제어할 수 있게 해준다. 예를 들어 그림 1에서 /alice/etc/passwd와 /bob/etc/passwd를 포함하는 루트 파일시스템이 있을 때, 서로 다른 네임스페이스 아래의 두 프로세스는 /etc/passwd를 서로 다르게 볼 수 있으며, /alice 또는 /bob 아래의 버전으로 해석될 수 있다. 프로세스 자체는 자신의 요청이 더 넓은 루트 파일시스템으로 리매핑되고 있다는 사실을 전혀 알지 못하며, 자신의 범위 밖의 파일을 결코 “볼” 수 없다. 결정적으로 네임스페이싱은 자원을 _열 때(opening)_에만 적용되며, 그 결과로 얻은 파일 디스크립터는 이후 읽기나 쓰기 같은 작업에서 추가 오버헤드 없이 일반적인 커널 자원처럼 동작한다. 이는 Linux 커널이 공유 자원을 효율적으로 관리하면서도, 애플리케이션이 기반 파일시스템으로부터 필요로 하는 수준의 격리를 제공할 수 있게 한다. 또한 파일을 열고 나면 파일 디스크립터는 일반적인 방식으로 프로세스 간 전달될 수 있어 Unix 프로그래밍 관습과의 호환성도 보장한다.
Figure 1.Linux 마운트 네임스페이스는 프로세스가 파일명이 해석되는 방식을 제어할 수 있게 한다.
네임스페이스는 Linux의 최신 기능이 아니라 수년에 걸쳐 점진적으로 추가되어 왔다. 파일시스템(또는 “마운트”) 네임스페이싱은 2001년 Linux 2.5.2 커널에 처음 추가되었고,38 이어서 2006년 Linux 2.6.19에서 프로세스 간 통신 네임스페이싱이,16 2007년 Linux 2.6.24에서 네트워크 스택 네임스페이싱이 추가되었다.3 시간이 흐르며 Linux는 서로 다른 일곱 가지 유형의 네임스페이스 지원을 축적했는데,27 이를 함께 사용하면 단일 커널이 최소 오버헤드로 프로세스에 자원을 할당하는 방식에 엄청난 유연성을 부여한다. 그러나 Plan 928처럼 OS가 이를 염두에 두고 설계된 것이 아니라 조각조각 도입되었기 때문에, 이러한 네임스페이스는 저수준이었고 사용하기 어려웠다. FreeBSD15나 Solaris30 같은 다른 운영체제의 유사한 변형들도 대중적 사용으로 이어지지 못했다. 따라서 2013년에 Docker가 이룬 큰 진전은 네임스페이스를 활용해 VM이 제공하는 무거운 격리와, OS 프리미티브가 제공하는 사용성 및 기존 소프트웨어와의 호환성 사이에서 실용적인 균형점을 찾아낸 것이었다. 다음으로 이것이 어떻게 동작하는지 살펴본다.
Docker가 Linux 컨테이너를 실행하는 방법. Docker는 클라이언트-서버 애플리케이션으로, 호스트 머신에서 동작하는 서버 데몬(dockerd)과 RESTful Docker API를 통해 요청을 보내는 docker CLI 클라이언트로 구성된다. 데몬은 컨테이너, 이미지, 네트워크, 볼륨 등 모든 시스템 자원을 생성하고 관리한다. 개발자가 docker CLI 명령을 호출하면, 잘 알려진 Unix 도메인 소켓을 통해 API 호출을 보낸다. 데몬은 한때 모놀리식 프로그램이었지만, 2015년 무렵 우리는 이를 분리해 그림 2에 보인 전문화된 구성요소들로 나누었다.7 첫 구성요소인 buildkit은 파일시스템 이미지를 조립하고, containerd는 그 이미지를 네트워크 및 스토리지 자원과 함께 실행 중인 컨테이너로 인스턴스화하는 일을 관리한다.
Figure 2.Docker 구성요소 아키텍처.
컨테이너 이미지. docker build가 호출되면 Docker는 입력 Dockerfile로부터 실행 파일과 데이터를 나타내는 파일시스템 이미지를 빌드한다. 컨테이너 이미지는 계층형 파일시스템 포맷으로 저장되며, 각 레이어는 이전 레이어 위에 적용된다. 이 레이어들의 바닥은 대개 Debian이나 Alpine Linux 같은 운영체제 배포판에서 부트스트랩되지만, 단순한 tar 아카이브로 수작업 구성할 수도 있다. 이후 레이어들은 Dockerfile의 개별 명령을 실행한 결과로 생기는 파일시스템 차이에 해당한다. 이것이 Docker Hub가 인터넷을 통해 이미지를 공유할 수 있는 기반이다. 이미지 포맷 자체는 2016년부터 Open Container Initiative(OCI)의 사용자 커뮤니티에 의해 표준화되었고,b 이제 여러 독립 구현체를 사용할 수 있다.
이미지 자체는 콘텐츠 주소 기반(content-addressable) 스토리지 시스템에 저장되며, 파일시스템 이미지의 해시가 이를 관리하는 키로 사용된다. 이는 스토리지 중복 제거를 효율적으로 수행하게 해주고, Docker Hub로 푸시된 이후에는 이미지가 불변(immutable)임을 보장한다. 어떤 사용자든 이미지를 풀(pull)해서 어떤 머신에서든 실행할 수 있으며, 해시를 사용해 이미지가 변조되지 않았음을 검증할 수 있다. Docker는 overlayfs, btrfs, ZFS 같은 최신 Linux 파일시스템을 사용해 효율적인 스냅샷과 클로닝으로 copy-on-write 레이어를 직접 관리한다. 또한 stargz 스토리지 스냅샷터를 통해 이미지의 지연 풀(lazy-pulling)도 지원한다.c
컨테이너 인스턴스. OCI 이미지에 대해 docker run을 호출하면, 파일시스템 이미지에서 부트스트랩된 네임스페이스 격리 프로세스(또는 “컨테이너”)를 만들기 위해 시스템 자원이 할당된다. containerd 프로세스는 각 컨테이너에 필요한 네임스페이스를 동적으로 구성하며, 예를 들어 다음과 같은 작업을 수행한다.
자원 격리 및 I/O 속도 제한을 위한 프로세스 “control groups” 정의
컨테이너 내부의 로컬 네트워크 포트를 호스트 인터페이스의 외부 노출 포트로 리매핑
영속적 애플리케이션 상태를 위해 호스트 파일시스템의 변경 가능한 스토리지 볼륨을 부착
PID 네임스페이스로 컨테이너의 프로세스 트리를 격리
사용자 네임스페이스로 컨테이너의 로컬 사용자 ID를 호스트의 다른 ID로 매핑하여, 예컨대 avsm 사용자가 컨테이너 안에서는 항상 UID 1000으로 일관되게 보이되, 실제로는 서로 다른 호스트에서 충돌하지 않는 UID 12345 또는 23456으로 매핑되도록 함
이러한 네임스페이스 구성에는 약간의 오버헤드가 있지만, 완전한 Linux VM을 생성하는 것보다 훨씬 낮으며20 대부분의 경우 1초도 안 되는 시간에 수행될 수 있다. Linux 커널 자체는 일반 프로세스와 마찬가지로 종료된 컨테이너를 가비지 컬렉션한다.
이 클라이언트-서버 아키텍처는 원격 Docker 인스턴스를 관리하기 쉽게 만들었다. CLI는 예컨대 클라우드에서 실행 중인 Docker 호스트로, 보안 네트워크 연결을 통해 명령을 보내도록 설정하기만 하면 됐다. 2015년 우리는 이 유연성을 활용해 Docker의 영향력이 커지며 생긴 또 다른 시급한 문제를 해결했다. 출시 후 2년 동안 Docker는 Linux 개발을 위한 널리 채택된 도구로 자리 잡았지만, 사용성의 벽에 부딪혔다. 대부분의 개발자는 여전히 macOS나 Windows를 주 개발 환경으로 사용하고 있었는데, Docker 파일시스템 이미지는 Linux 커널에서만 실행될 수 있었기 때문이다. 한편 퍼블릭 클라우드의 부상으로 Linux는 배포를 위한 선호 선택지가 되었다. 우리는 클라우드 서비스를 개발하는 데 큰 장벽을 제거하기 위해 macOS와 Windows에서 Linux 컨테이너를 실행할 방법을 신속히 찾아야 했다.
Mac용 Docker 애플리케이션을 매끄럽게 구축하기. Mac과 Windows용 Docker를 설계할 때의 핵심 제약은 Linux 버전의 Docker에 이미 익숙한 개발자에게 추가 설정 없이 동작해야 한다는 점, 그리고 동일한 Docker 이미지를 실행할 수 있어야 한다는 점이었다. 해답은 최신 하이퍼바이저 가상화와 Linux 네임스페이스의 장점을 결합하는 데 있었다. 데스크톱 OS 옆에서 Linux를 실행하는 전통적 접근 대신, 우리는 소프트웨어 아키텍처를 뒤집어 macOS 또는 Windows에서 실행되는 유저스페이스 애플리케이션 안에 하이퍼바이저를 임베드하고, 그 애플리케이션 내부에서 Linux를 실행했다. 이러한 접근의 영감은 유니커널에 대한 연구에서 얻었는데,22 이는 운영체제 구성요소를 더 큰 애플리케이션 안에 유연하게 임베드하는 것이 가능함을 보여주었다.
애플리케이션 안에 Linux 임베드하기. 우리는 먼저 HyperKit이라는 라이브러리 가상 머신 모니터(VMM)를 설계했는데, 이는 Intel CPU의 하드웨어 가상화 확장을 사용해 일반 유저 프로세스에서 Linux 커널을 실행한다19 (그림 3). 이 임베디드 Linux 커널은 Docker 데몬을 실행하고, 데몬은 다시 컨테이너를 실행하며 일반적인 Docker 서버 엔드포인트로 동작한다(그림 4). 우리는 데스크톱 애플리케이션 안에 Linux 관리 세부사항을 모두 숨겼고, 데스크톱에서 실행되는 docker build와 docker run이 임베디드 Linux 인스턴스로 호출을 포워딩함으로써 “그냥 동작”하도록 했다. 이 접근은 너무나 성공적이어서 Podman39 같은 다른 컨테이너 시스템에도 채택되었고, 이제 macOS와 Windows에서 컨테이너를 실행하는 표준 방식이 되었다.
Figure 3.전통적인 독립형 하이퍼바이저(위)와, 라이브러리 가상 머신 모니터를 사용하고 Linux VM을 임베드하는 Docker 접근(아래)의 대비.
우리는 LinuxKit이라는 맞춤형 Linux 배포판도 설계했는데, 이는 전통적인 독립형 Linux 배포판이 아니라 구성요소로 사용되어 더 큰 애플리케이션 안에 임베드되도록 의도되었음을 반영한다. 애플리케이션 시작 시간을 최소화하기 위해, 우리는 Docker 컨테이너를 실행하는 데 필요한 구성요소만 포함한 맞춤형 유저스페이스를 만들었고, 모든 구성요소를 컨테이너 안에서 실행하여 부팅 시 사용되는 루트 네임스페이스에는 아무것도 실행되지 않도록 했다. 이를 통해 Docker 컨테이너 자체가 사용하는 동일한 copy-on-write 파일시스템과 네트워크 네임스페이스를 활용할 수 있었고, 전체 시스템을 고도로 격리된 방식으로 실행할 수 있었다. LinuxKit과 HyperKit의 결합은 네이티브 macOS 프로세스만큼 빠르게 Linux 프로세스를 부팅할 수 있었으며, 그 결과 Docker for Mac과 Windows 애플리케이션이 2016년에 탄생해 공개되었다.
Figure 4.Docker for Mac 애플리케이션 아키텍처.
네트워킹. 그러나 Linux 컨테이너가 이제 macOS와 Windows에서 잘 실행되게 되었음에도, 임베디드 Linux 컨테이너로 네트워킹을 연결하는 작업은 놀랄 만큼 까다로웠다. 데스크톱에서 Linux VM으로 이더넷 네트워크 트래픽을 브리지하는 전통적 접근은 복잡한 네트워크 관리가 필요했다. 더 나쁜 점은, 브리징 접근이 기업용 데스크톱의 방화벽과 바이러스 검사 도구에 의해 잠재적으로 악성 트래픽으로 감지되어 베타 사용자로부터 수천 건의 버그 리포트가 발생했다는 것이다. 다행히도 SLIRP31라는 오래된 도구가 유효한 해결책을 제공했는데, 이는 1990년대 중반 Palmpilot PDA를 인터넷에 연결하는 데 처음 사용된 접근이었다!
아웃바운드 네트워크 트래픽은 보안 스캐너에서 오탐을 유발했는데, 이들은 종종 호스트 OS 네트워크 스택을 우회하는 미지의 프로세스에서 나오는 모든 트래픽을 차단하도록 설정되어 있기 때문이다. 이는 Linux VM이 트래픽을 데스크톱 네트워크 스택으로 직접 브리지할 때 정확히 발생한다. 우회책으로 우리는 MirageOS21의 유니커널 라이브러리를 활용해 Linux 네트워킹 요청과 macOS/Windows 네이티브 소켓 호출 간의 변환을 수행했다. 컨테이너가 TCP 핸드셰이크를 시도하면, TCP SYN을 포함하는 이더넷 프레임이 Mac에서 공유 메모리를 사용하는 virtio 프로토콜33을 통해 호스트로 전송된다(그림 5). 이는 “라이브러리 VMM”이 수신한 뒤, 호스트 OS에서 실행되는 유저스페이스 TCP/IP 스택으로 sendmsg를 통해 전달된다. OCaml로 작성된23 이 유저스페이스 스택은 vpnkit이라 불리며, macOS의 connect() 시스템 콜을 호출해 TCP 핸드셰이크를 완료하거나 오류를 신호한다. 이 아키텍처에서는 Linux 컨테이너에서 나가는 트래픽이 별도의 머신이 아니라 Docker 애플리케이션에서 비롯된 것으로 VPN 정책에 인식된다. 2016년 베타 테스트에서 vpnkit을 배포하자 기업 사용자로부터의 버그 리포트가 99% 이상 감소했으며, 이 접근은 그 이후로도 Docker for Mac과 Windows의 핵심 구성요소로 자리잡았다. SLIRP 접근은 이후 서버리스 클라우드 세계에서도 채택되었고,40 오래된 다이얼업 네트워킹 트릭을 되살려 컨테이너 관리의 새로운 문제를 해결했다.
Figure 5.전통적인 브리지 네트워크의 트래픽은 로컬 정책에 의해 차단되는 반면, 로컬 프로세스에서 나오는 트래픽과 SLIRP를 통해 간접화된 VM 트래픽은 허용된다.
인바운드 네트워크 트래픽도 도전 과제였지만, 이유는 달랐다. 기본적으로 Linux 컨테이너가 어떤 포트에서 리슨하더라도 CLI에서 요청하지 않는 한 인터넷에 자동으로 노출되지는 않는다(예: docker run -p 80:80 nginx로 80번 포트의 nginx를 노출). 컨테이너를 실행할 때 이상적인 사용자 경험은 컨테이너 포트가 데스크톱 IP 주소에 직접 나타나, http://localhost:8080 같은 URL로 브라우저에서 접근 가능해지는 것이다. VMware Fusion 같은 데스크톱 가상화 소프트웨어의 전통적 접근은 localhost 대신 임시 중간 IP를 노출했다. 우리 LinuxKit 커널은 맞춤형 eBPF 프로그램을 설치했는데,25 이는 데스크톱 호스트에 대응하는 리슨 소켓이 생성되도록 트리거하고, 포트 포워더를 활성화해 컨테이너가 큰 오버헤드 없이 투명하게 연결을 수신하도록 했다. 이를 통해 Mac에서 Linux 컨테이너를 실행하자마자 네이티브 Linux 머신에서처럼 즉시 localhost로 접근 가능한 완벽한 개발자 경험을 제공할 수 있었다.
스토리지. 파일 스토리지에서도 유사한 문제가 있었는데, 개발자는 로컬에서 코드를 편집하고 데이터 파일에 접근해야 하는 동시에 컨테이너 안에서 코드와 테스트를 실행할 수 있어야 하기 때문이다. 이러한 라이브 파일 접근은 보통 Linux에서 “바인드 마운트(bind mount)”로 수행되며 docker run -v /host:/container처럼 표현된다. 바인드 마운트는 파일시스템의 일부를 트리의 다른 부분에 접붙이는 비이식적인 Linux 커널 파일시스템 개념이다. macOS와 Windows는 다른 커널이므로 이 방식이 동작하지 않는다. 그래서 Docker는 KVM 하이퍼바이저에서 유래한 virtio-fs 공유 메모리 프로토콜을 사용해 파일시스템 연산을 FUSE 요청 형태로 호스트에 전달한다. 호스트는 이 요청을 수신하고 대응하는 open, read, write 시스템 콜을 호출한다. 이는 또한 개발자의 코드와 데이터가 호스트 파일시스템에 그대로 머물 수 있음을 의미하며, Linux VM 내부에 이러한 도구들을 통합할 필요 없이 Apple의 Time Capsule이나 Spotlight 같은 백업 및 검색 도구를 사용할 수 있게 한다.
Windows Services for Linux의 등장. 2017년이 되자 클라우드에서의 Linux 배포 인기 추세가 뚜렷해졌고, Microsoft는 Windows에서 Linux 애플리케이션을 직접 실행할 수 있게 하는 Windows Services for Linux(WSL) 서브시스템을 공개했다. 이 서브시스템의 첫 버전은 가상화를 사용하지 않고, 대신 다른 라이브러리 운영체제를 통해 Linux 바이너리가 호출하는 시스템 콜을 대응하는 Windows 시스템 콜로 동적으로 변환하는 방식을 선호했다.29 이 접근은 많은 애플리케이션에서 성공적이었지만, Docker 컨테이너는 이를 적용하기에는 한 단계 더 어려운 대상이었다. Linux 커널에는 매우 많은 시스템 콜이 있으며, WSL은 Docker 컨테이너를 실행하기에 충분한 수를 지원하지 못했다.
2018년 Microsoft는 WSL을 재설계해 버전 2를 공개했고, Docker for Mac과 유사하게 백그라운드에서 완전한 Linux VM을 실행하는 접근을 채택했다. 이 시점에서 Docker for Windows 통합은 매끄러워졌다. WSL2 Docker는 LinuxKit WSL 배포판 안에서 데몬과 사용자 컨테이너를 실행하고, Windows 자체 및 다른 Linux 배포판 양쪽에서 Docker API와 네트워크 포트를 포워딩하는 일을 처리한다(그림 6).
Figure 6.WSL2에서의 Docker for Windows 아키텍처.
정리하자면, Docker 컨테이너가 플랫폼을 넘어 진화할 수 있었던 아키텍처적 접근은 전통적으로 “커널 전용 코드”였던 것을 유저스페이스 라이브러리로 재목적화해 다른 애플리케이션 안에 임베드하는 라이브러리 OS 접근이었다. 이 아키텍처의 성공은 눈에 띄지 않으면서도 어디에나 존재한다는 사실로 증명된다. 수백만 개발자가 Docker 및 그 파생 도구를 매일 사용하면서도, 자신이 어떤 운영체제에서 실행 중인지에 대해 걱정할 필요가 없다.
여러 CPU 아키텍처. Docker 초기에는 클라우드 워크로드의 대부분이 Intel 아키텍처 기반이었다. 이는 2018년 클라우드 워크로드용 Amazon Graviton ARM 프로세서가 출시되고, 2020년 Apple M1 ARM CPU 시리즈가 출시되면서 완전히 바뀌었다. 갑자기 ARM에서 워크로드를 실행함으로써 비용 절감과 성능 향상을 동시에 얻을 수 있게 되었고, 개발자들은 이를 활용하고 싶어 했다. 오늘날 개발자가 Intel, ARM, POWER, 또는 새로 부상하는 오픈 소스 RISC-V CPU에서 애플리케이션을 실행할 수 있도록, 같은 Docker 이미지 안에서 여러 CPU 아키텍처를 지원하는 것이 필요하다. 서버 측에서는 어떤 아키텍처를 위해 이미지가 빌드되었는지 기록하는 “멀티아키텍처 매니페스트(multiarch manifests)” 지원을 OCI 이미지 포맷에 확장함으로써 이 능력이 Docker 이미지에 추가되었다.
그럼에도 단일 호스트에서 여러 CPU 아키텍처용 이미지를 어떻게 빌드할 것인가라는 문제가 남았는데, 악명 높은 크로스 컴파일의 복잡성을 도입하지 않아야 했다. 우리는 Linux의 비교적 덜 알려진 기능인 binfmt_misc로 눈을 돌렸는데, 이는 실행 파일을 사용자 정의 유저스페이스 애플리케이션을 통해 실행할 수 있게 한다. QEMU2는 여러 CPU 아키텍처 간 변환을 수행할 수 있으므로, Docker for Desktop의 임베디드 LinuxKit 안에 이를 설치해 ARM과 Intel 바이너리 사이를 투명하게 변환했다. 이는 상당한 오버헤드가 있었지만, 보통 빌드 단계에서만 필요했으며 생성된 멀티아키텍처 이미지는 수정 없이 어떤 호스트에서도 네이티브로 실행될 수 있었다. 이후 Apple은 CPU 시리즈에 “Rosetta”를 통한 CPU 명령어 집합 변환을 위한 하드웨어·소프트웨어 지원을 도입했는데,24 이는 Docker 아키텍처에 쉽게 통합되었다. 오늘날 Intel과 ARM 컨테이너를 나란히 실행하는 것은 개발자에게 흔한 워크플로가 되었다.
신뢰 실행 환경으로 시크릿 관리하기. 비밀번호나 API 키 같은 시크릿을 관리하는 일은 항상 컨테이너화된 환경에서의 도전 과제였는데, 파일시스템 이미지에 구워 넣는 것이 아니라 컨테이너에 동적으로 주입해야 하기 때문이다. Docker는 항상 소켓 포워딩을 지원해 왔으며, 로컬 도메인 소켓을 컨테이너에 마운트할 수 있고, Docker for Mac/Windows의 경우 그 소켓을 Linux VM 안으로 포워딩하는 것도 포함된다. 이를 통해 사용자는 키를 직접 노출하지 않고도 컨테이너 안에서 ssh-agent 같은 키 관리 시스템을 사용할 수 있다. 소켓 포워딩은 좋은 1차 보호를 제공하지만, 지속적으로 커지는 소프트웨어 공급망 속에 숨어 있는 악성코드에 대비하려면 현대 환경에서는 더 많은 방어 계층이 필요하다.
첫 번째로는 컨테이너 런타임 내부에서 하이퍼바이저 보호를 직접 사용해 컨테이너 간 보호 수준을 높이는 것이다.32 그 너머로 Docker는 호스트 운영체제조차도 시크릿 데이터에 접근하지 못하게 보호할 수 있는 현대 CPU의 하드웨어 기능을 통합해 왔다. 신뢰 실행 환경(TEE)은 “기밀 VM(confidential VMs)”을 생성해 애플리케이션, 커널, 심지어 하이퍼바이저 경계를 가로질러 데이터 접근 제한을 강제할 수 있게 한다.35 하지만 TEE를 구성하고 사용하는 일은 OS 가상화와 유사한 수준의 관리 복잡성을 갖는데, 사실상 TEE 내부에서 작은 운영체제 커널을 부팅하기 때문이다.
Confidential Containers 워킹 그룹의 사용자 커뮤니티는 TEE 안에서 실행될 수 있고 Docker를 통해 관리될 수 있는 애플리케이션을 개발해 왔다. Docker의 클라이언트-서버 아키텍처는 이러한 애플리케이션과 잘 맞는데, 데스크톱에서 실행되는 Docker CLI가 로컬 TEE에서 암호화된 메시지를 호스트를 가로질러 여러 포워딩 소켓을 통해, 클라우드 환경 안의 원격 TEE 환경까지 포워딩할 수 있기 때문이다.9 이는 개발자가 현장에 있지 않아도 민감한 클라우드 환경에 인증할 수 있게 하고, 데스크톱 엔클레이브 안에 자격 증명을 안전하게 저장하면서도 로컬 개발의 편의성을 유지하게 한다.
AI 워크로드를 위한 GPGPU 지원. 지금까지 Docker가 서로 다른 운영체제와 CPU에서 실행되도록 진화해 온 과정을 살펴보았지만, AI 워크로드의 부상은 완전히 새로운 도전 과제를 가져왔다. 머신러닝 워크로드는 대부분 GPU에서 실행되며, Docker 생태계도 이를 지원하도록 적응해 왔다. 핵심 도전은 GPU 워크로드가 정확히 일치하는 커널 GPU 드라이버와 유저스페이스 라이브러리를 요구하는 반면, 여러 컨테이너가 단일 공유 커널 위에서 실행된다는 점이다. 이는 Docker가 처음 설계된 목적과 같은 기본 충돌을 다시 도입한다. 즉, 동일한 머신에서 의존성이 충돌하는 여러 애플리케이션을 어떻게 실행할 것인가? 두 애플리케이션이 같은 커널 GPU 드라이버의 서로 다른 버전을 요구한다면 어떻게 될까?
2023년 3월부터 Docker는 컨테이너 디바이스 인터페이스(CDI)를 지원하기 시작했는데,10 이는 컨테이너 시작 시점에 파일시스템 이미지를 커스터마이징할 수 있게 하여 GPU 디바이스 파일과 GPU 전용 동적 라이브러리를 바인드 마운트하고 ld.so 캐시를 재생성할 수 있게 한다. 이는 특정 종류 또는 특정 벤더의 GPU 집합에 대해 Docker 이미지를 이식 가능하게 해주지만, 서로 다른 운영체제와 하드웨어 브랜드 전반에서 완전히 매끄럽지는 않다. CDI가 추가하는 사용 가능한 동적 라이브러리는 사실상 서로 다른 API를 정의하므로, 전통적으로 CPU에서 실행되는 컨테이너의 인터페이스였던 안정적인 Linux 시스템 콜 ABI에 대응하는 무언가가 존재하지 않는다. Nvidia GPU를 위해 설계된 애플리케이션은, 기저 GPU 가상화 지원이 아직 성숙하여 벡터 명령을 이렇게 다양한 하드웨어 사이에서 변환할 수 있는 수준이 아니기 때문에, Apple M 시리즈 CPU에서 여전히 실행이 어렵다. 우리는 더 유연하고 안전한 방식으로 GPU 관련 의존성을 관리하기 위해 더 넓은 컨테이너 커뮤니티 및 GPU 제조사들과 계속 협력하고 있으며, 이식 가능한 인터페이스에 대한 이니셔티브가37 합의점으로 수렴하길 기대한다.
Docker는 2013년에 어떤 애플리케이션이든 더 쉽게 빌드하고, 공유하고, 실행할 수 있도록 돕는 것을 목표로 출발했다. 이제는 표준적인 클라우드 및 데스크톱 개발 워크플로에 깊이 통합되어, 전 세계 수백만 개발자가 매일 사용하고 매달 수십억 건의 요청이 발생한다. 우리의 일관된 목표 중 하나는 상호운용을 위한 표준을 구축하는 활기차고 다양한 오픈 소스 커뮤니티를 유지하여, 단일 벤더에 종속되는 락인을 없애는 것이었다. Cloud Native Computing Foundation(CNCF)은 여러 핵심 구성요소의 관리자 역할을 하며,7 Open Container Initiative(리눅스 재단의 일부)는 이미지 포맷의 관리자다. 오늘날 이러한 요소들 중 많은 것들의 여러 구현체가 번성하고 있으며, 클라우드, 데스크톱, 그리고 자동차·모바일·심지어 우주선까지 포함하는 엣지에서의 배포가 증가하는 추세를 보고 있다.18
소프트웨어 개발은 빠르게 움직이므로, 우리는 최신 발전을 따라가기 위해 Docker의 내부를 지속적으로 진화시키고 있다. 그림 7은 지속적 테스트 및 배포, 통합 개발 환경(IDE) 언어 서버, 그리고 에이전틱 코딩을 통한 AI 지원을 통합한 2025년의 전형적인 개발자 워크플로를 보여준다. Docker 관점에서 핵심 “빌드와 실행” 워크플로는 10년 전 사용자 경험과 매우 유사하게 유지되지만, 다양한 환경에서 견고한 샌드박싱이 필요하다는 점에서 발생하는 마찰을 줄이기 위한 훨씬 더 많은 시스템 지원이 뒤따른다.23
Figure 7.2026년의 Docker 개발자 워크플로.
개발자라면, 우리의 목표는 Docker가 눈에 보이지 않는 동반자가 되어 더 빠르게 코드를 출시하도록 돕는 것—그리고 그 과정 자체를 즐기게 하는 것이다. Docker는 특히 현대의 AI 코딩 워크플로를 마주한 상황에서, 여러분의 필요에 맞춰 진화할 수 있을 만큼 확장 가능하도록 설계되었다. 여러분이 처한 어떤 소프트웨어 환경에서든 이를 자유롭게 재구성해 활용하고, 배운 것을 커뮤니티와 공유해 주길 바란다.
이 글에 대한 의견을 주신 Jon Crowcroft, Michael W. Dales, Patrick Ferris, Ryan Gibb, Hamed Haddadi께 감사드린다. 또한 Docker 프로젝트에 대해 Solomon Hykes(프로젝트 창립자)를 포함하되 이에 국한되지 않는, Harald Albers, Kevin Alvarez, Jeff Anderson, Mary Anthony, Gianluca Arbezzano, Vincent Batts, Morgan Bauer, Laura Brehm, David Calavera, Michael Crosby, Doug Davis, Stephen Day, Bruno de Sousa, Vincent Demeester, Sven Dowideit, Alex Ellis, Phil Estes, Lorenzo Fontana, Jessie Frazelle, Thomas Gazagnaire, Brian Goff, Tianon Gravi, Paweł Gronowski, Evan Hazlett, Erik Hollensbe, John Howard, Andrew Hsu, Olli Janatuinen, Lei Jitang, Scott Johnston, Vishnu Kannan, Samuel Karp, Albin Kerouanton, Kir Kolyshkin, Kenfe-Mickaël Laventure, Aaron Lehmann, Djordje Lukic, Andrea Luzzardi, Derek McGowan, Alexander Morozov, Richard Mortier, Antonio Murdaca, Rob Murray, Bjorn Neergaard, Daniel Nephin, Arnaud Porterie, Jana Radhakrishnan, Anusha Ragunathan, David Sheets, Boaz Shuster, Cory Snider, Cristian Staretu, John Stephens, Akihiro Suda, Yong Tang, Sam Thibault, Shaun Thompson, Tõnis Tiigi, James Turnbull, Sebastiaan van Stijn, Tibor Vass, Austin Vazquez, Madhu Venugopal, Victor Vieux, Sam Whited, Jeremy Yallop 등 과거와 현재의 Docker 커뮤니티 구성원들께도 피드백과 기여에 감사드린다.