관리 메뉴

피터의 개발이야기

[Container] Kubernetes에서 Docker 이미지 빌드는 왜 어려울까? - Daemon, Root, Privileged의 차이 본문

DevOps/Docker

[Container] Kubernetes에서 Docker 이미지 빌드는 왜 어려울까? - Daemon, Root, Privileged의 차이

기록하는 백엔드개발자 2026. 7. 24. 16:26
반응형

TL;DR

Docker 이미지를 빌드하는 작업은 단순히 파일을 압축하는 과정이 아니다.

Dockerfile의 명령을 실행하고, 파일시스템의 변경 사항을 추적하며, 이를 이미지 레이어로 만드는 작업이 필요하다.

전통적인 docker build에서는 이 작업을 Docker Daemon이 담당한다.

하지만 Docker Daemon은 일반적으로 호스트에서 root 권한으로 실행되고,

Kubernetes Pod 안에서 실행하려면 privileged 권한까지 요구하는 경우가 많다.

따라서 Kubernetes 환경에서는 Docker Daemon을 Pod 안에 실행하는 DinD보다,

제한된 권한으로 이미지를 만들 수 있는 전용 이미지 빌더가 필요하다.


ㅁ 들어가며

이전 [Container] Harbor와 Kaniko는 왜 함께 사용할까? - 이미지는 만들고, 저장하고, 배포한다 글에서는

컨테이너 이미지가 다음 세 단계를 거친다고 정리했다.

Build
  │
  ▼
Store
  │
  ▼
Deploy

Kaniko는 이미지를 만드는 Builder이고,

Harbor는 생성된 이미지를 저장하는 Registry이다.

 

이 과정에서 다음과 같이 간략히 설명했다.

Kubernetes에서는 Docker Daemon을 실행하지 않는 환경이 많다.

 

그렇다면 Kubernetes에서는 왜 Docker Daemon을 실행하기 어려울까?

단순히 Docker가 설치되어 있지 않기 때문일까?

이 문제를 이해하려면 먼저 docker build 명령 뒤에서 실제로 어떤 일이 일어나는지 살펴봐야 한다.


ㅁ docker build는 누가 실행할까?

개발자는 이미지를 만들 때 다음 명령을 실행한다.

docker build -t my-app:1.0 .

겉으로 보면 Docker CLI가 Dockerfile을 읽고 이미지를 만드는 것처럼 보인다.

하지만 Docker CLI는 실제 빌드를 직접 수행하지 않는다.

Docker CLI는 Docker Daemon에 빌드를 요청하는 클라이언트이다.

사용자
  │
  ▼
Docker CLI
  │
  │ Build 요청
  ▼
Docker Daemon
  │
  ├─ Dockerfile 해석
  ├─ Base Image 다운로드
  ├─ RUN 명령 실행
  ├─ COPY 파일 반영
  ├─ 파일시스템 변경 추적
  └─ Image Layer 생성

즉, 일반적인 docker build 구조는 다음과 같이 정리할 수 있다.

구성 요소 역할
Docker CLI 사용자의 명령을 Docker Daemon에 전달
Docker Daemon 컨테이너와 이미지 빌드 작업 수행
Docker Registry 생성된 이미지를 저장하고 전달

 

Docker CLI는 요청만 전달하고, 실제 이미지 생성은 Docker Daemon이 담당한다.

 

ㅁ 이미지 빌드는 단순한 파일 압축이 아니다

Docker 이미지는 여러 개의 읽기 전용 레이어로 구성된다.

Dockerfile의 명령어가 실행될 때마다 파일시스템이 변경되고, 그 변경 사항이 새로운 레이어로 저장된다.

예를 들어 다음 Dockerfile이 있다고 가정해보자.

FROM ubuntu:24.04

RUN apt-get update
RUN apt-get install -y curl

COPY app.sh /app/app.sh

CMD ["/app/app.sh"]

 

빌드 과정에서는 다음과 같은 일이 발생한다.

ubuntu:24.04
      │
      ▼
apt-get update 실행
      │
      ▼
변경된 파일을 Layer로 저장
      │
      ▼
curl 설치
      │
      ▼
변경된 파일을 Layer로 저장
      │
      ▼
app.sh 복사
      │
      ▼
변경된 파일을 Layer로 저장

 

특히 RUN 명령은 단순한 문자열이 아니다.

실제로 명령을 실행할 수 있는 격리 환경을 만들고, 실행 결과로 변경된 파일을 찾아야 한다.

이미지 빌더는 이 과정에서 다음과 같은 작업을 수행한다.

  • 프로세스 실행
  • 파일시스템 마운트
  • 파일 생성과 삭제
  • 소유권과 권한 변경
  • 네트워크 접근
  • 이미지 레이어 생성
  • 레이어 압축 및 저장

따라서 이미지 빌드는 일반 애플리케이션 실행보다 운영체제와 파일시스템에 더 가까운 작업이다.

 

ㅁ Docker Daemon은 왜 root 권한으로 실행될까?

Docker Daemon은 컨테이너를 만들고 실행하기 위해 Linux 커널의 여러 기능을 사용한다.

대표적으로 다음 기능들이 있다.

커널 기능 역할
Namespace 프로세스, 네트워크, 파일시스템 격리
cgroup CPU와 메모리 등 자원 제한
OverlayFS 이미지 레이어와 컨테이너 파일시스템 구성
Capability root 권한을 세분화하여 제공
Virtual Network 컨테이너 네트워크와 브리지 생성

이러한 기능 중 일부는 일반 사용자 권한으로 사용할 수 없거나 추가적인 설정이 필요하다.

그래서 전통적인 Docker Daemon은 호스트에서 root 권한으로 실행된다.

Host OS
  │
  └─ Docker Daemon
       └─ root 권한

Docker Daemon이 root 권한을 갖는다는 것은 Docker Daemon을 제어할 수 있는 사용자도 매우 강한 권한을 가질 수 있다는 의미이다.

 

ㅁ Docker Socket은 왜 위험할까?

Docker CLI는 일반적으로 Unix Socket을 통해 Docker Daemon과 통신한다.

/var/run/docker.sock

 

호스트에서 다음 명령을 실행하면 Docker Socket의 위치를 확인할 수 있다.

ls -l /var/run/docker.sock

 

Kubernetes Pod 안에서 Docker 명령을 사용하기 위해 호스트의 Docker Socket을 마운트하는 방식이 있다.

이를 DooD(Docker out of Docker)라고 부른다.

apiVersion: v1
kind: Pod
metadata:
  name: docker-builder
spec:
  containers:
    - name: docker
      image: docker:cli
      volumeMounts:
        - name: docker-sock
          mountPath: /var/run/docker.sock  <== host docker 통신경로

  volumes:
    - name: docker-sock
      hostPath:
        path: /var/run/docker.sock

 

구조는 다음과 같다.

Kubernetes Pod
      │
      ▼
/var/run/docker.sock
      │
      ▼
Host Docker Daemon
      │
      ▼
Host Container Runtime

 

Pod 내부의 Docker CLI는 Pod 안에서 명령을 실행하지만, 실제 작업은 호스트의 Docker Daemon이 수행한다.

 

문제는 Docker Socket에 접근할 수 있는 프로세스가 호스트 Docker Daemon을 사실상 모두 제어할 수 있다는 점이다.

예를 들어 호스트 파일시스템을 컨테이너에 마운트할 수도 있다.

docker run \
  --rm \
  -it \
  -v /:/host \
  alpine \
  chroot /host

 

이 명령을 실행할 수 있다면 컨테이너 내부에서 호스트 파일시스템에 접근할 수 있다.

따라서 Docker Socket을 Pod에 마운트하는 것은 단순한 파일 공유가 아니다.

호스트 Docker Daemon의 관리 권한을 Pod에 넘기는 것에 가깝다.

ㅁ DooD와 DinD는 무엇이 다를까?

Kubernetes 환경에서 Docker 이미지를 빌드할 때 자주 등장하는 방식은 DooD와 DinD이다.

 

DooD: Docker out of Docker

DooD는 Pod 안에서 호스트의 Docker Daemon을 사용한다.

Pod
 │
 ├─ Docker CLI
 │
 └─ Host Docker Socket Mount
          │
          ▼
    Host Docker Daemon

장점은 구조가 단순하고 기존 Docker 명령을 그대로 사용할 수 있다는 점이다.

하지만 Pod가 호스트 Docker Daemon을 직접 제어하기 때문에 보안 위험이 크다.

 

DinD: Docker in Docker

DinD는 Pod 안에 별도의 Docker Daemon을 실행한다.

Pod
 │
 ├─ Jenkins Agent
 │
 └─ Docker Daemon
       │
       └─ Build Container

호스트 Docker Socket을 직접 공유하지 않는다는 점에서는 DooD와 다르다.

하지만 Pod 내부에서 Docker Daemon이 컨테이너와 파일시스템을 제어하려면 강한 권한이 필요하다.

 

일반적으로 다음 설정이 필요하다.

securityContext:
  privileged: true

DinD는 호스트 Docker Daemon을 공유하지 않지만, 대신 Pod의 격리 수준을 크게 낮추는 문제가 생긴다.

 

ㅁ Root와 Privileged는 같은 권한일까?

컨테이너 보안을 이해할 때 가장 혼동하기 쉬운 개념이 rootprivileged이다.

둘은 같은 의미가 아니다.

 

컨테이너 내부 Root

컨테이너 내부에서 UID 0으로 실행되는 상태이다.

securityContext:
  runAsUser: 0

컨테이너 내부에서는 파일 소유권 변경이나 패키지 설치 같은 작업을 수행할 수 있다.

하지만 일반적인 컨테이너 격리가 유지된다면 호스트 전체를 직접 제어할 수 있는 것은 아니다.

 

Privileged 컨테이너

Privileged 컨테이너는 일반 컨테이너에 적용되는 여러 보안 제한을 해제한다.

securityContext:
  privileged: true

Privileged 모드에서는 다음과 같은 권한이 확대될 수 있다.

  • 대부분의 Linux Capability 사용
  • 호스트 장치 접근
  • 파일시스템 마운트
  • 네트워크 설정 변경
  • 커널 기능 접근
  • 보안 프로파일 제한 완화

차이를 정리하면 다음과 같다.

구분 컨테이너 내부 Root Privileged
UID 0 일반적으로 0
컨테이너 격리 유지 가능 크게 완화
Host Device 접근 제한됨 광범위하게 가능
커널 기능 사용 제한됨 대부분 허용
보안 위험 상대적으로 제한적 매우 높음

따라서 다음 문장은 정확하지 않다.

Root 컨테이너와 Privileged 컨테이너는 같다.

 

더 정확하게 표현하면 다음과 같다.

Privileged 컨테이너는 보통 root로 실행되지만,
컨테이너 내부에서 root로 실행된다고 해서 모두 Privileged 컨테이너인 것은 아니다.

ㅁ DinD는 왜 Privileged 권한을 필요로 할까?

DinD의 Docker Daemon은 Pod 안에서 다시 컨테이너를 생성해야 한다.

이를 위해서는 다음과 같은 작업이 필요하다.

  • Namespace 생성
  • cgroup 제어
  • OverlayFS 마운트
  • 컨테이너 네트워크 생성
  • iptables 설정
  • 프로세스 격리
  • 파일시스템 레이어 관리

하지만 일반적인 Kubernetes Pod는 이러한 커널 기능을 자유롭게 사용할 수 없다.

그래서 DinD는 보통 다음과 같이 구성된다.

apiVersion: v1
kind: Pod
metadata:
  name: dind-builder
spec:
  containers:
    - name: docker
      image: docker:dind
      securityContext:
        privileged: true

이 설정을 사용하면 Docker Daemon은 동작하기 쉬워진다.

대신 Kubernetes가 제공하는 컨테이너 격리의 상당 부분을 포기하게 된다.

즉, DinD의 문제는 Docker Daemon이 Pod 안에 있다는 사실만이 아니다.

Docker Daemon을 실행하기 위해 Pod에 과도한 권한을 부여해야 한다는 점이 더 큰 문제이다.

 

ㅁ Kubernetes의 보안 정책과 충돌한다

Kubernetes에는 Pod가 사용할 수 있는 권한을 제한하기 위한 Pod Security Standards가 있다.

대표적인 보안 수준은 다음과 같다.

정책 설명
Privileged 대부분의 권한 허용
Baseline 일반적인 위험 설정 차단
Restricted 최소 권한 원칙을 강하게 적용

DinD에서 사용하는 privileged: true 설정은 Baseline과 Restricted 정책에서 허용되지 않는다.

예를 들어 Namespace에 Restricted 정책이 적용되어 있다면 다음과 같이 설정할 수 있다.

apiVersion: v1
kind: Namespace
metadata:
  name: ci
  labels:
    pod-security.kubernetes.io/enforce: restricted

이 Namespace에 Privileged DinD Pod를 생성하면 Pod 자체가 거부될 수 있다.

Error from server (Forbidden):
pod violates PodSecurity "restricted":
privileged container is not allowed

보안 정책이 강화된 Kubernetes에서는 DinD 파이프라인이 실패하는 이유가 Docker 명령 오류가 아닐 수도 있다.

빌드가 시작되기 전에 Pod 생성 단계에서 거부될 수 있다.

 

ㅁ Rootless와 Daemonless도 구분해야 한다

이미지 빌드 도구를 비교할 때 다음 세 가지 표현이 자주 등장한다.

Daemonless
Rootless
Unprivileged

비슷해 보이지만 서로 다른 개념이다.

 

Daemonless

상주하는 Docker Daemon 없이 이미지를 빌드한다.

Dockerfile
   │
   ▼
Builder Process
   │
   ▼
Container Image

 

Rootless

빌드 프로세스나 관련 런타임을 비root 사용자로 실행한다.

User ID != 0

 

Unprivileged

Kubernetes Pod에 privileged: true를 설정하지 않고 실행한다.

securityContext:
  privileged: false

 

 

차이를 정리하면 다음과 같다.

개념 의미
Daemonless 상주하는 Docker Daemon 없이 빌드
Rootless 비root 사용자 권한으로 실행
Unprivileged Privileged 컨테이너 없이 실행

Daemonless라고 해서 반드시 Rootless인 것은 아니다.

Root로 실행되더라도 Privileged 권한이나 호스트 Docker Socket 없이 동작하는 빌더가 있을 수 있다.

 

따라서 이미지 빌더를 선택할 때는 단순히 “Docker Daemon을 사용하지 않는다”는 설명만 확인해서는 부족하다.

다음 항목을 함께 확인해야 한다.

  • 컨테이너 내부에서 root로 실행되는가?
  • privileged: true가 필요한가?
  • 호스트 Socket을 마운트하는가?
  • HostPath를 사용하는가?
  • Linux Capability를 추가하는가?
  • Seccomp와 AppArmor를 비활성화하는가?

ㅁ Kaniko는 무엇을 바꿨을까?

Kaniko는 Docker Daemon 없이 Dockerfile을 해석하고 컨테이너 이미지를 생성하는 빌더이다.

전통적인 Docker 빌드 구조는 다음과 같다.

Docker CLI
    │
    ▼
Docker Daemon
    │
    ▼
Build Container
    │
    ▼
Container Image

 

Kaniko의 구조는 다음과 같다.

Dockerfile
    │
    ▼
Kaniko Executor
    │
    ├─ Base Image 추출
    ├─ Dockerfile 명령 실행
    ├─ 파일시스템 변경 탐색
    ├─ Image Layer 생성
    └─ Registry Push

Kaniko는 각 Dockerfile 명령이 실행된 후 파일시스템의 변경 사항을 탐색한다.

변경된 파일을 기준으로 스냅샷을 생성하고, 이를 이미지 레이어로 만든다.

이 과정에서는 Docker Daemon을 실행하지 않는다.

 

따라서 다음과 같은 구성이 필요하지 않다.

securityContext:
  privileged: true

 

또한 호스트 Docker Socket을 마운트할 필요도 없다.

/var/run/docker.sock

Kaniko가 Kubernetes CI/CD 환경에서 사용된 핵심 이유는 단순히 Docker 명령을 대체했기 때문이 아니다.

Docker Daemon과 Privileged 권한에 의존하던 이미지 빌드 구조를 변경했기 때문이다.

ㅁ Kaniko가 Root 권한 문제를 모두 해결한 것은 아니다

Kaniko를 설명할 때 다음과 같이 표현하기도 한다.

Kaniko는 Rootless 이미지 빌더이다.

하지만 이 표현은 주의해서 사용해야 한다.

 

Kaniko의 핵심은 다음 두 가지이다.

  • Docker Daemon을 요구하지 않는다.
  • Privileged 컨테이너를 요구하지 않는다.

그러나 Dockerfile에 따라 컨테이너 내부에서 root 권한이 필요한 작업은 여전히 존재할 수 있다.

예를 들어 다음 명령은 파일의 소유권이나 시스템 경로를 변경할 수 있다.

RUN apt-get update
RUN apt-get install -y curl
RUN chown -R app:app /app

따라서 Kaniko의 특징은 다음처럼 표현하는 것이 더 정확하다.

Kaniko는 Docker Daemon과 Privileged 컨테이너 없이 이미지를 빌드할 수 있도록 설계된 이미지 빌더이다.

 

중요한 것은 컨테이너 내부의 UID가 항상 0이 아닌가가 아니라,

빌드 컨테이너가 호스트 커널과 런타임에 어느 정도의 권한을 갖는가이다.

ㅁ 빌드 방식별 권한 비교

Kubernetes 환경에서 사용할 수 있는 대표적인 빌드 구조를 비교하면 다음과 같다.

방식 Docker Daemon Docker Socket Privileged 주요 위험
Host Docker 필요 로컬 사용 Host root 빌드 서버 자체 권한
DooD Host Daemon 사용 필요 보통 불필요 Host Docker 제어
DinD Pod 내부 실행 불필요 일반적으로 필요 Pod 격리 약화
Daemonless Builder 불필요 불필요 일반적으로 불필요 빌더별 권한 검토 필요
Rootless Builder 도구에 따라 다름 불필요 불필요 커널과 파일시스템 호환성

단순히 Docker Socket을 제거했다고 보안 문제가 모두 해결되는 것은 아니다.

반대로 Docker Daemon을 사용한다고 해서 모든 구성이 동일하게 위험한 것도 아니다.

 

중요한 것은 다음 세 가지이다.

Daemon이 어디에서 실행되는가?
          │
          ▼
어떤 사용자 권한으로 실행되는가?
          │
          ▼
Host 자원에 얼마나 접근할 수 있는가?

ㅁ Kubernetes 이미지 빌더를 선택하는 기준

Kubernetes에서 이미지 빌더를 선택할 때는 기능과 성능뿐 아니라 권한 모델을 함께 확인해야 한다.

확인 항목 질문
Daemon 별도의 빌드 Daemon이 필요한가?
Root 비root 사용자로 실행할 수 있는가?
Privileged privileged: true가 필요한가?
Socket Host Docker Socket을 마운트하는가?
Storage OverlayFS나 FUSE 같은 기능이 필요한가?
Security Profile Seccomp와 AppArmor를 유지할 수 있는가?
Cache 원격 캐시를 사용할 수 있는가?
Registry 결과 이미지를 직접 Registry에 Push할 수 있는가?
Compatibility 기존 Dockerfile을 그대로 사용할 수 있는가?
Maintenance 현재도 유지보수되고 있는 도구인가?

 

결국 이미지 빌더의 선택은 “어떤 도구가 가장 빠른가?”만의 문제가 아니다.

보안 정책, 빌드 속도, Dockerfile 호환성, 캐시 구조, 운영 난이도를 함께 고려해야 한다.

ㅁ Dockerfile은 같은데 빌드 방식은 달라질 수 있다

이미지 빌더가 달라지더라도 Dockerfile은 동일하게 사용할 수 있는 경우가 많다.

FROM eclipse-temurin:21-jre

COPY app.jar /app/app.jar

ENTRYPOINT ["java", "-jar", "/app/app.jar"]

 

그러나 Dockerfile을 실행하는 방식은 서로 다를 수 있다.

Docker
  └─ Docker Daemon이 Dockerfile 실행

DinD
  └─ Pod 내부 Docker Daemon이 Dockerfile 실행

Kaniko
  └─ Executor가 Dockerfile 실행 후 Snapshot 생성

BuildKit
  └─ LLB 그래프로 변환하여 빌드 작업 실행

Buildah
  └─ OCI 기반 도구로 컨테이너와 이미지를 구성

즉, Dockerfile은 이미지의 조립 방법을 정의하는 문서이고,

어떤 빌더가 그 문서를 실행하는지는 별도의 문제이다.

ㅁ 마무리 — 문제는 Docker 명령이 아니라 권한 구조다

Kubernetes에서 Docker 이미지 빌드가 어려운 이유는 docker build 명령을 사용할 수 없기 때문이 아니다.

이미지 빌드는 다음과 같은 시스템 작업을 포함하기 때문이다.

  • 프로세스 실행
  • 파일시스템 마운트
  • 레이어 생성
  • 네트워크 접근
  • 파일 권한 변경
  • 컨테이너 격리 환경 구성

전통적인 Docker는 이러한 작업을 root 권한의 Docker Daemon에 맡겼다.

개발자 PC나 전용 빌드 서버에서는 이 구조가 자연스럽게 동작한다.

 

하지만 Kubernetes에서는 빌드 작업도 일반적인 Pod로 실행된다.

Pod 안에서
  다시 Docker Daemon을 실행하려면 Privileged 권한이 필요하고,

  호스트 Docker Socket을 공유하면 호스트 제어 권한이 노출될 수 있다.

전통적인 방식

Docker CLI
    │
    ▼
Root Docker Daemon
    │
    ▼
Container Image
Kubernetes에서 필요한 방향

Build Pod
    │
    ▼
제한된 권한의 Image Builder
    │
    ▼
Container Registry

따라서 Kubernetes 이미지 빌드의 핵심 질문은 다음과 같다.

Docker Daemon을 사용할 것인가?

보다 더 정확한 질문은 다음과 같다.

이미지 빌드에 필요한 권한을 어떤 프로세스에, 어느 범위까지 허용할 것인가?

Kaniko와 같은 빌더가 필요했던 이유도 여기에 있다.

Docker Daemon을 단순히 다른 위치로 옮긴 것이 아니라, Kubernetes의 보안 모델에 맞게 이미지 빌드 방식을 다시 설계한 것이다.

 

ㅁ 함께 보면 좋은 사이트

오픈소스컨설팅 기술 블로그 글 보기

 - 컨테이너 이미지 빌드 방식 비교

 - 보안 • 성능 • 운영 관점에서 Kaniko를 권장하는 이유

컨테이너 빌드 도구 선택을 위한 특성 및 성능 비교 (Kaniko, Buildah, Buildkit)

 

 

 

반응형
Comments