[2026] Docker Multi-Stage Builds | Smaller Images, Separate Build and Runtime
이 글의 핵심
Split builder and runtime stages with COPY --from, shrink C++ and Node images, pick slim or distroless bases, fix missing.so errors, and avoid Alpine/glibc mismatches.
Introduction
Multi-stage builds put several FROM lines in one Dockerfile, name stages with AS, compile in the builder stage, then COPY —from only the binary and required .so files into a small runtime image. The final image does not ship compilers, headers, or source—smaller pulls and smaller attack surface.
This article focuses on patterns illustrated with C++ toolchains; the same ideas apply to Node, Go, and other compiled or bundled apps.
After reading this post
- You understand builder vs runtime separation
- You can write a minimal multi-stage Dockerfile
- You can troubleshoot missing shared libraries and Alpine vs glibc issues
Table of contents
Multi-stage build concepts
Builder vs runtime
- Multi-stage: Multiple FROM lines; name stages with AS. Build in the first stage; in the second, use a light base (alpine, distroless, debian-slim) and COPY —from=build_stage to bring only the binary and needed
.sofiles. - The final image excludes compilers, headers, and build tools → smaller size and attack surface. 아래 코드는 mermaid를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
flowchart TB
subgraph stage1["Stage 1: builder"]
S1[ubuntu:22.04] --> S2[Install g++, cmake]
S2 --> S3[Copy source]
S3 --> S4[Build]
S4 --> S5[myapp binary]
end
subgraph stage2["Stage 2: runtime"]
R1[debian:bookworm-slim] --> R2[Install libstdc++6 only]
R2 --> R3[COPY --from builder]
R3 --> R4["Final image ~50MB"]
end
S5 -.->|COPY --from| R3
Basic multi-stage Dockerfile
The builder stage compiles with g++/cmake; the debian:bookworm-slim stage copies only myapp and installs libstdc++6 for dynamic linking. 다음은 dockerfile를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
# Stage 1: build
FROM ubuntu:22.04 AS builder
RUN apt-get update && apt-get install -y --no-install-recommends \
g++ cmake \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /src
COPY . .
RUN cmake -B build && cmake --build build
# Stage 2: runtime
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
libstdc++6 \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /src/build/myapp /usr/local/bin/
CMD [myapp]
Dynamic vs static linking
- Dynamic: Install libstdc++6 (and friends) in the runtime stage—usually easier to maintain.
- Static: Options like
-static-libstdc++can reduce runtime packages—verify licensing and ABI policies.
vcpkg + multi-stage example
다음은 dockerfile를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
# ========== Stage 1: build ==========
FROM ubuntu:22.04 AS builder
RUN apt-get update && apt-get install -y --no-install-recommends \
g++ cmake git ca-certificates \
&& rm -rf /var/lib/apt/lists/*
ENV VCPKG_ROOT=/opt/vcpkg
RUN git clone https://github.com/Microsoft/vcpkg.git ${VCPKG_ROOT} \
&& ${VCPKG_ROOT}/bootstrap-vcpkg.sh -disableMetrics
WORKDIR /src
COPY vcpkg.json .
RUN ${VCPKG_ROOT}/vcpkg install
COPY CMakeLists.txt .
COPY src/ src/
RUN cmake -B build \
-DCMAKE_TOOLCHAIN_FILE=${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake \
&& cmake --build build --target myapp
# ========== Stage 2: runtime ==========
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
libstdc++6 libgcc-s1 \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /src/build/myapp /usr/local/bin/
USER nobody
CMD [myapp]
Optimize the deployment image
Slim bases and clean layers
- Base images: Alpine is tiny but uses musl—a glibc binary may not run. debian-slim, ubuntu minimal, or distroless are safer defaults for many glibc binaries.
- Cleanup: Use apt-get install —no-install-recommends and remove /var/lib/apt/lists/ after installs to shrink layers.
- Fewer layers: Combine RUN steps when practical.
Base image comparison
| Base | Size | glibc | Typical C++ | Notes |
|---|---|---|---|---|
| ubuntu:22.04 | ~77MB | Yes | Yes | Easy default |
| debian:bookworm-slim | ~80MB | Yes | Yes | Often recommended |
| alpine:3.19 | ~7MB | No (musl) | Caution | glibc binaries won’t run |
| gcr.io/distroless/cc-debian12 | ~50MB | Yes | Yes | No shell—strong isolation |
distroless example
아래 코드는 dockerfile를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
FROM debian:bookworm-slim AS builder
# ....build ...
FROM gcr.io/distroless/cc-debian12
COPY --from=builder /usr/local/bin/myapp /app/myapp
ENTRYPOINT [/app/myapp]
distroless has no shell or package manager—minimal attack surface. For debugging, override entrypoint at docker run time.
Common errors
“cannot find -lstdc++” / “undefined reference”
Cause: Runtime stage missing libstdc++. Fix:
RUN apt-get update && apt-get install -y --no-install-recommends \
libstdc++6 libgcc-s1 \
&& rm -rf /var/lib/apt/lists/*
“error while loading shared libraries: libxxx.so.X”
Cause: Dynamic dependency missing in the runtime image. Fix: On the builder, run:
ldd build/myapp
Install missing .so in the runtime stage or COPY --from=builder the needed libraries.
Alpine: “FATAL: kernel too old” or glibc mismatch
Cause: Alpine uses musl; binaries built for glibc won’t run. Fix: Use debian-slim or distroless for glibc binaries, or build inside Alpine if you standardize on musl.
”no such file or directory” when running the binary
Cause: Missing dynamic linker or wrong architecture.
Fix: file build/myapp and ldd build/myapp.
Conclusion
Multi-stage Docker builds keep heavy toolchains out of production and make runtime images easier to scan and deploy. Pair with layer caching, .dockerignore, and a registry workflow—see Docker Compose production patterns and the Node.js deployment guide for full-stack examples.