alpine보다 더 줄이고 더 안전하게 만들고 싶다면 다음 후보는 distroless입니다. 구글이 만든 이 베이스 이미지는 OS 배포판의 셸·패키지매니저·기본 유틸리티를 전부 빼고, 앱을 돌리는 데 꼭 필요한 런타임과 라이브러리만 남깁니다. 이름 그대로 "배포판(distro)이 없는" 이미지입니다.
distroless가 더 안전한 이유
컨테이너의 공격면은 그 안에 든 바이너리 개수에 비례합니다. 일반 이미지에는 sh, bash, apt, curl, wget 같은 도구가 들어 있는데, 공격자가 컨테이너에 침투하면 바로 이 도구들로 정찰하고 페이로드를 내려받습니다. distroless에는 셸 자체가 없어서, 컨테이너가 뚫려도 kubectl exec로 들어가 sh를 띄울 수도, 안에서 명령을 실행할 수도 없습니다.
크기도 줄어듭니다. 셸과 패키지 DB, 도구 체인이 빠지면서 같은 앱이라도 베이스가 가벼워집니다.
| 베이스 | 대략 크기 | 셸 | 패키지매니저 |
|---|---|---|---|
debian:12 | ~120MB | 있음 | apt |
alpine:3.20 | ~7MB | 있음(sh) | apk |
gcr.io/distroless/static | ~2MB | 없음 | 없음 |
gcr.io/distroless/java21 | ~200MB | 없음 | 없음 |
멀티스테이지로 적용하기
distroless에는 빌드 도구가 없으므로 반드시 멀티스테이지로 빌드 단계와 분리합니다. 빌드는 풀 이미지에서 하고, 산출물만 distroless로 복사합니다.
# 빌드 단계 — 컴파일에 필요한 모든 도구
FROM golang:1.22 AS build
WORKDIR /src
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app ./cmd/server
# 실행 단계 — 셸도 없는 distroless
FROM gcr.io/distroless/static-debian12
COPY --from=build /app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]
Go처럼 정적 링크되는 바이너리는 static을, glibc가 필요하면 base를, 런타임이 필요한 언어는 java21·python3 같은 전용 태그를 씁니다.
트레이드오프 — 디버깅이 어렵다
셸이 없다는 건 양날의 검입니다. 컨테이너에 들어가 cat이나 ps로 상태를 볼 수 없습니다. 두 가지로 대응합니다.
| 상황 | 방법 |
|---|---|
| 빠른 디버깅이 필요하다 | :debug 태그 사용(busybox 셸 포함) |
| 실행 중 컨테이너를 들여다본다 | kubectl debug로 임시 디버그 컨테이너 주입 |
| 로컬 검증 | 빌드 단계 이미지로 먼저 동작 확인 |
체크리스트
docker build -t app:distroless .
docker images app:distroless # 크기 확인
docker run --rm app:distroless # 정상 기동 확인
docker run --rm -it app:distroless sh # 셸 없음 → 정상(에러나야 맞음)
마지막 명령이 실패하는 것이 distroless가 제대로 적용됐다는 증거입니다.
멀티스테이지 빌드와 베이스 이미지 선택이 크기·보안에 미치는 영향을 직접 빌드해 비교하는 실습은 도커 트랙에서 무료로 할 수 있습니다.