Docker를 이해하는 핵심 구조
Docker는 애플리케이션을 환경째 패키징해 로컬 개발, CI/CD, 배포 환경의 차이를 줄이는 기술이다. 이 스크립트는 컨테이너 격리, 이미지 레이어, 네트워크, 보안, 운영 판단 기준을 중심으로 Docker의 동작 원리를 정리한다.
Script Companion
오디오와 함께 스크립트 보기
- 01
Docker를 이해해야 하는 이유는 단순하다. 내 컴퓨터에서는 되는데 서버에서는 안 되는 문제를 줄이고, BackOps에서 배포와 로컬 개발환경, CI/CD 파이프라인을 읽을 수 있게 해준다. 컨테이너를 모르면 이미지가 어떻게 빌드되고 푸시되는지, 서비스 장애 때 어디서 로그를 봐야 하는지, ECS와 Kubernetes가 무엇을 실행 단위로 삼는지 파악하기 어렵다. Docker는 앱만 옮기는 도구가 아니라, 앱이 기대하는 실행 환경까지 함께 묶는 방식에 가깝다.
- 02
먼저 컨테이너와 가상머신, VM의 차이를 잡아야 한다. VM이 하이퍼바이저 위에 각자 완전한 OS와 커널을 띄우는 구조라면, 컨테이너는 Host OS의 리눅스 커널을 공유하면서 격리된 프로세스를 실행한다. 이 격리는 Namespace와 cgroups가 나눠 맡는다. Namespace는 컨테이너 안에서 자기 프로세스와 네트워크, 파일시스템만 보이게 하고, cgroups는 컨테이너별 CPU와 메모리 사용량을 제한한다. 그래서 컨테이너 시작은 OS 부팅보다 격리된 프로세스 시작에 더 가깝다.
- 03
컨테이너가 가벼운 이유는 커널을 공유하기 때문이지만, 바로 그 점이 경계이기도 하다. 같은 VM 안에 여러 컨테이너를 올리면 자원 활용률을 높이기 쉽지만, 애플리케이션마다 다른 커널 버전이나 커널 모듈, 디바이스 드라이버가 필요하다면 Docker가 맞지 않을 수 있다. 장애 격리 단위를 커널까지 분리해야 하는 경우도 마찬가지다. 이런 요구가 있으면 컨테이너보다 VM을 선택해야 한다. Docker의 장점은 빠른 실행과 환경 패키징이고, 커널 수준 격리가 필요할 때는 그 장점이 충분한 답이 되지 않는다.
- 04
Docker 이미지가 레이어로 동작하는 핵심에는 OverlayFS가 있다. OverlayFS는 Linux 커널에 내장된 Union 파일시스템으로, 여러 디렉토리를 하나의 디렉토리처럼 합쳐 보여준다. lowerdir는 읽기 전용 이미지 레이어이고, upperdir는 컨테이너가 쓰는 변경 레이어이며, workdir는 내부 작업용이고, merged는 컨테이너 안에서 실제로 보이는 합쳐진 뷰다. 아래 레이어는 고정되어 있고, 컨테이너가 파일을 만들거나 수정하면 변경 내용은 upperdir에 기록된다. 이 구조 덕분에 원본 이미지 레이어를 건드리지 않고 여러 컨테이너가 같은 기반을 공유할 수 있다.
- 05
OverlayFS의 Copy-on-Write, 즉 CoW 메커니즘은 원본을 건드리지 않고 쓸 때만 새 복사본을 만든다는 원리다. node:20-alpine 이미지를 여러 서비스가 함께 쓰면 lowerdir는 한 번만 저장되고, 컨테이너 시작 때도 전체 파일을 복사하지 않고 기존 레이어를 참조한다. ECR로 이미지를 푸시하거나 풀할 때도 변경된 레이어만 전송되므로 배포 속도에 이점이 생긴다. 다만 쓰기가 폭증하면 upperdir 사용량이 갑자기 늘 수 있다. 레이어 공유는 디스크 절약과 빠른 시작을 주지만, 쓰기 레이어의 증가를 운영에서 놓치면 다른 형태의 병목이 된다.
- 06
CoW는 Docker만의 특수한 개념이 아니다. Git 커밋 DAG, DB MVCC, OS fork에서도 변경 전까지 공유하고 변경 시점에만 분기하는 패턴이 보인다. PostgreSQL에서는 이전 tuple 버전과 새 tuple 버전의 분리가 읽기와 쓰기 비차단으로 이어지고, OS fork에서는 부모 프로세스 페이지를 공유하다가 쓰기 시 자식 프로세스 페이지가 분리된다. ZFS 스냅샷도 원본 데이터셋과 쓰기 가능한 클론이 분리되고, 스냅샷과 클론 생성이 즉시 완료되며, 클론에서 블록을 수정하는 시점에만 새 블록이 할당된다. 이처럼 원본과 변경 격리 영역이 분리되어 있고, 쓰기 전까지 공유된다면 CoW 계열로 볼 수 있다.
- 07
이미지, 컨테이너, Dockerfile의 관계도 분명히 해야 한다. 이미지는 컨테이너를 만들기 위한 읽기 전용 설계도이고, 컨테이너는 그 이미지를 실행한 인스턴스다. Dockerfile은 이미지를 만드는 레시피 파일이며, 각 명령은 이미지 레이어를 만든다. 변경되지 않은 레이어는 캐시되므로 Dockerfile의 순서는 CI/CD 빌드 속도에 직접 영향을 준다. 코드만 바뀐 경우 의존성 설치 단계를 캐시로 건너뛰면 수 분이 절약될 수 있다. 그래서 자주 바뀌는 앱 코드와 덜 바뀌는 의존성 설치를 같은 무게로 두면 빌드 캐시의 이점을 잃는다.
- 08
멀티스테이지 빌드는 빌드 환경과 실행 환경을 분리하는 방식이다. NestJS 같은 앱을 단일 스테이지로 만들면 TypeScript 컴파일러, 테스트 도구, 개발 의존성까지 최종 이미지에 남아 1GB 안팎으로 커질 수 있다. 멀티스테이지 빌드는 빌드 도구와 중간 산출물을 최종 이미지에 저장하지 않고, COPY --from으로 실행 산출물만 가져온다. --from=builder는 Stage 1의 파일시스템에서 필요한 파일만 복사한다는 뜻이고, Stage 1에 설치된 TypeScript, ts-node, 기타 개발 도구는 Stage 2에 포함되지 않는다. 프로덕션 이미지가 500MB를 넘거나 취약점 스캔 결과가 빌드 도구와 devDependencies에 몰려 있다면 먼저 빌드와 실행 환경을 분리해야 한다.
- 09
Docker 네트워크는 컨테이너가 호스트와 통신하는 방식을 이해하는 데 중요하다. bridge 모드에서 컨테이너는 veth pair라는 가상 랜선의 한쪽 끝을 eth0로 갖고, 다른 쪽 끝은 호스트의 docker0 브리지에 연결된다. docker0는 여러 컨테이너를 연결하는 가상 허브처럼 동작한다. Docker Compose는 실행 시 프로젝트 전용 default network를 만들고 모든 서비스를 여기에 붙인다. 사용자 정의 bridge 네트워크에서는 내장 DNS가 서비스명을 컨테이너 IP로 바꿔주기 때문에 DATABASE_URL의 db 같은 서비스명이 동작한다. 반대로 서비스 IP를 환경변수에 직접 박아두면 컨테이너 재생성 때 IP가 바뀌어 조용히 깨질 수 있다.
- 10
네트워크 모드와 데이터 보존도 운영 판단에 연결된다. 기본 bridge 모드는 컨테이너에 독립적인 IP를 주고 격리를 유지하지만, 컨테이너끼리 통신하려면 같은 네트워크 연결과 이름 해석을 이해해야 한다. host 모드는 컨테이너가 호스트 네트워크 인터페이스를 그대로 공유하므로 포트 매핑 없이 직접 접근되고 성능상 유리할 수 있지만, 컨테이너 간 격리가 없어 프로덕션에서는 잘 쓰지 않는다. 또 컨테이너 내부 데이터는 종료되거나 삭제되면 함께 사라진다. DB나 로그처럼 유지해야 하는 데이터는 Volume을 마운트해 컨테이너 밖에 저장해야 한다. 이미지는 Docker Hub, ECR, 사내 레지스트리 같은 Registry에 저장하고 공유한다.
- 11
운영 관점에서는 HEALTHCHECK, BuildKit, 보안 설정을 함께 봐야 한다. 컨테이너가 실행 중이어도 앱이 실제로 응답하는지는 별개이므로, HEALTHCHECK는 주기적으로 앱 상태를 확인하고 ECS나 Kubernetes 같은 오케스트레이터가 재시작 판단을 할 수 있게 신호를 준다. NestJS 앱에는 /health 엔드포인트가 있어야 ECS ALB 헬스체크와 HEALTHCHECK가 동작한다. BuildKit은 Docker 18.09부터 도입되었고, 2023년부터 Docker Desktop에서 기본 활성화되었다. 기존 빌더가 레이어를 순서대로 직렬 실행하는 반면 BuildKit은 독립적인 빌드 단계를 병렬 실행해 멀티스테이지 빌드 시간을 줄인다.
- 12
보안에서는 컨테이너를 root로 실행하지 않는 것이 기본 출발점이다. 2025년 Sysdig Cloud-Native Security Report에 따르면 컨테이너 관련 보안 사고가 전년 대비 47% 증가했고, 주요 공격 벡터는 취약한 베이스 이미지 32%, root 실행 컨테이너 28%, 하드코딩된 시크릿 12% 순이다. 컨테이너 탈출 취약점이 발견되면 root 권한 컨테이너는 호스트 OS에 root로 접근할 위험이 있다. 비권한 사용자로 실행하면 탈출 후에도 호스트 권한이 제한된다. 버전이 고정된 베이스 이미지를 주기적으로 업데이트하고, 이미지 취약점 스캔을 수행하며, 읽기 전용 파일시스템과 .dockerignore로 민감 파일 포함을 줄이는 것도 같은 방어선에 속한다.
- 13
공격 표면을 더 줄이고 싶다면 Distroless 이미지를 고려할 수 있다. Distroless는 쉘과 패키지 매니저가 제거된 이미지라서 컨테이너가 탈취되어도 공격자가 쉘을 실행하거나 추가 도구를 설치하기 어렵다. 다만 쉘이 없기 때문에 컨테이너 내부에 들어가 디버깅하는 방식이 불편해진다. 그래서 로컬 개발은 Alpine, 프로덕션은 Distroless로 분리하는 방식이 일반적이다. Linux Capabilities도 공격 표면과 연결된다. 컨테이너는 기본적으로 14가지 Linux Capability를 갖지만, 대부분의 웹 앱은 이 중 하나도 필요하지 않으므로 --cap-drop으로 줄이는 판단이 필요하다.
- 14
Docker가 항상 맞는 선택은 아니다. GPU 집약적 HPC나 ML 학습처럼 멀티 GPU MPI 병렬 작업과 GPU 할당 보장이 필요하면 Singularity, 즉 Apptainer가 더 적합할 수 있다. 실시간 커널 요구가 있는 제어 시스템, 로봇, 오디오처럼 마이크로초 단위 결정론적 응답이 필요하면 베어 메탈 RT 커널이 판단 기준에 오른다. 극단적 저지연 네트워크에서는 veth pair와 iptables NAT가 패킷당 1에서 10 마이크로초 지연을 추가할 수 있어 host 네트워크 모드나 Unikernel이 언급된다. 루트리스 불가 HPC 클러스터는 Charliecloud나 Shifter, IoT 엣지와 초저전력 디바이스는 Unikernel 또는 containerd와 runc 직접 사용이 대안으로 나온다.
- 15
장애 대응에서는 증상과 원인을 분리해 보는 습관이 중요하다. port is already allocated 또는 address already in use가 나오면 로컬 포트를 이미 다른 프로세스나 이전 컨테이너가 점유했을 가능성이 높다. 컨테이너가 바로 종료되면 앱 크래시인지, CMD나 ENTRYPOINT가 잘못되어 메인 프로세스가 없는지 로그를 통해 확인해야 한다. 코드 수정 후 이전 코드가 실행된다면 이미 실행 중인 컨테이너와 재빌드된 이미지의 관계를 다시 봐야 한다. OOMKilled가 true이거나 ECS Stopped Reason에 OutOfMemoryError가 보이면 메모리 제한과 Node.js 힙 사이즈, 메모리 누수를 점검한다. Alpine 이미지에서 HEALTHCHECK가 unhealthy라면 curl이 기본 포함되어 있지 않아 실패했을 수도 있다.
- 16
정리하면 Docker는 앱을 환경째 패키징해 어디서든 동일하게 실행하려는 기술이고, 이미지는 설계도, 컨테이너는 실행 인스턴스다. Dockerfile의 레이어와 캐시, OverlayFS의 CoW, Compose의 사용자 정의 네트워크와 내장 DNS, Volume의 데이터 보존을 함께 이해해야 로컬 개발과 배포 문제를 같은 구조 안에서 볼 수 있다. 프로덕션에서는 멀티스테이지 빌드로 실행 이미지를 줄이고, HEALTHCHECK와 로그, 메모리 제한, 보안 설정을 운영 판단의 기준으로 삼는다. ECS와 Kubernetes를 이해하기 위한 선수지식도 결국 이 컨테이너 실행 단위에서 시작된다.
같은 레이어