C++ DevContainer & Docker Guide — Standardize Builds,
이 글의 핵심
Standardize C++ builds with Docker: multi-stage images, vcpkg/Conan, VS Code Dev Containers, Compose, ccache/Ninja, GDB in containers, and deployment optimization.
Introduction: “Run it exactly like our environment”
One image for dev, CI, and deploy
If parts 40-1 and 40-2 covered dependency management and CI, containers (technology that bundles an app with its runtime in an isolated environment) fix that environment as an image (the read-only template used to create containers) so you get the same environment everywhere.
Docker (the most widely used container platform) lets you split build-only images (compiler, CMake, vcpkg/Conan) from run-only images (minimal runtime), so development, CI, and deployment share one toolchain.
With multi-stage builds (multiple build and run stages in one Dockerfile):
- Compile in the build stage
- Copy only the binary and required libraries into the runtime stage
- Result: smaller images and a smaller attack surface
Related stack:
- Orchestration: Docker Compose, minikube
- Infra: Nginx reverse proxy, Node.js deployment
- CI/CD: Node.js GitHub Actions, C++ Google Test
- Databases: Node.js DB, C++ DB, PostgreSQL vs MySQL, Redis caching
- Ops: Linux disk/inode
This article covers:
- Problem scenarios: “It works locally”, “every teammate has a different setup”
- Docker images for building: base image choice, dependency install, cache layers
- Multi-stage builds: build stage → copy into runtime stage
- Deployment image optimization: slim bases, removing unnecessary packages
- Common errors and fixes
- Optimization tips and production patterns
- Local development: volume mounts, docker compose, Dev Container
- Faster builds: Ninja, ccache + BuildKit cache mounts
- GDB inside containers: practical caveats
Practical note: This article is grounded in real issues from large C++ projects, including pitfalls and debugging tips you rarely see in books.
Table of contents
- Problem scenario: environment mismatch hell
- Build environment image
- Multi-stage builds
- Deployment image optimization
- Common errors and fixes
- Optimization tips
- Production patterns
- Local development, Compose, Dev Container
- Complete example project
- Summary
Analogy
Build, test, and deploy pipelines resemble a factory QC line: same inputs should yield same outputs; Sanitizers and static analysis act like pre-shipment defect checks.
1. Problem scenario: environment mismatch hell
What actually happens
"It builds locally but only fails in CI."
"Teammate A is on Ubuntu 22.04, B on 24.04—and results differ."
"The server has a different glibc version and the binary won't run."
"The image is 2GB and includes all the build tools."
"I built on Alpine and get glibc link errors."
Root causes
- Compiler and library version drift: different gcc/clang and glibc per developer
- Platform mismatch: binaries built on macOS won’t run on Linux servers
- Hidden dependencies: headers/libs only installed on one machine
- Bloated images: build tools and sources shipped inside the deployment image
Direction: pin the environment with Docker
flowchart LR
subgraph before["Without Docker (Before)"]
B1["Developer A / Ubuntu 22.04"] --> B2[Different binary]
B3["Developer B / Ubuntu 24.04"] --> B4[Different binary]
B5["CI / different env"] --> B6[Build may fail]
end
subgraph after["Docker (After)"]
A1[Same base image] --> A2[Same build]
A2 --> A3[Developer A]
A2 --> A4[Developer B]
A2 --> A5[CI]
end
Pinning compiler, CMake, and dependencies in a Docker image yields consistent results no matter who builds or where.
More scenarios
Scenario 5: onboarding
"What gcc and CMake versions should I install to build?"
"My vcpkg path differs—why doesn't it build on my machine?"
Manual setups can cost hours for new hires. Docker + a single docker build line gives everyone the same environment.
Scenario 6: multi-platform deploy
"Local is M1 Mac but production is x86_64 Linux."
"We need both ARM and x86 servers."
Docker Buildx with --platform linux/amd64,linux/arm64 can produce multi-arch images in one go.
Scenario 7: security scan failures
"The image scan reports CVEs."
"Build tools widen the attack surface."
Multi-stage builds let the runtime image contain only the binary, excluding compiler, shell, and package managers. Distroless bases may have no shell at all.
2. Build environment image
Compiler, CMake, dependencies in one place
- Base: from ubuntu:22.04, debian-slim, etc., install gcc, clang, cmake, git. Pin versions for reproducibility.
- vcpkg/Conan: structure Dockerfile so cache layers reuse when dependency manifests (
vcpkg.json,conanfile.txt) are unchanged. Order COPY as dependency files first → install → source → build so source-only changes don’t invalidate dependency layers. - CI: run jobs in this image via
container:, ordocker runfor build-only and export artifacts.
Minimal C++ example (build inside the container)
// main.cpp — compiled in the Docker build stage
#include <iostream>
int main() {
std::cout << "Compiled in the Docker build stage.\n";
return 0;
}
Basic Dockerfile (layer caching)
Copy vcpkg.json first, then RUN vcpkg install. Until vcpkg.json changes, that layer stays cached and rebuilds stay fast. rm -rf /var/lib/apt/lists/ after apt-get shrinks image size.
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y --no-install-recommends \
g++ cmake git ca-certificates \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /src
COPY vcpkg.json .
# vcpkg bootstrap + install (cached)
COPY . .
RUN cmake -B build -DCMAKE_TOOLCHAIN_FILE=....&& cmake --build build
Note: CMAKE_TOOLCHAIN_FILE must match your vcpkg bootstrap path; in multi-stage setups vcpkg usually lives only in the build stage.
Full vcpkg example
Project layout:
my-cpp-app/
├── CMakeLists.txt
├── vcpkg.json
├── Dockerfile
└── src/
└── main.cpp
vcpkg.json:
{
"dependencies": ["spdlog"]
}
CMakeLists.txt:
# Example
cmake_minimum_required(VERSION 3.20)
project(my-cpp-app LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
add_executable(myapp src/main.cpp)
find_package(spdlog CONFIG REQUIRED)
target_link_libraries(myapp PRIVATE spdlog::spdlog)
Dockerfile (vcpkg manifest mode):
# Build stage
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/*
# vcpkg install (cached layer)
ENV VCPKG_ROOT=/opt/vcpkg
RUN git clone https://github.com/Microsoft/vcpkg.git ${VCPKG_ROOT} \
&& ${VCPKG_ROOT}/bootstrap-vcpkg.sh -disableMetrics
WORKDIR /src
# Dependency files first → vcpkg install cache
COPY vcpkg.json .
RUN ${VCPKG_ROOT}/vcpkg install
# Then source and build
COPY CMakeLists.txt .
COPY src/ src/
RUN cmake -B build \
-DCMAKE_TOOLCHAIN_FILE=${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake \
&& cmake --build build
Conan example
Sample Dockerfile for Conan-based projects.
conanfile.txt:
[requires]
spdlog/1.12.1
[generators]
CMakeDeps
CMakeToolchain
Dockerfile (Conan):
FROM ubuntu:22.04 AS builder
RUN apt-get update && apt-get install -y --no-install-recommends \
g++ cmake git python3 python3-pip \
&& rm -rf /var/lib/apt/lists/*
RUN pip3 install --break-system-packages conan
WORKDIR /src
COPY conanfile.txt .
RUN conan install . --output-folder=build --build=missing
COPY CMakeLists.txt .
COPY src/ src/
RUN cmake -B build -DCMAKE_TOOLCHAIN_FILE=build/conan_toolchain.cmake \
&& cmake --build build
Using Docker builds in CI
Example: build with Docker in GitHub Actions and extract artifacts:
- name: Build with Docker
run: |
docker build -t myapp:build .
docker create --name extract myapp:build
docker cp extract:/src/build/myapp ./myapp
docker rm extract
Or use a container: job to build inside the same image:
jobs:
build:
runs-on: ubuntu-latest
container:
image: ubuntu:22.04
options: --user root
steps:
- uses: actions/checkout@v4
- run: apt-get update && apt-get install -y g++ cmake
- run: cmake -B build && cmake --build build
3. Multi-stage builds
How multi-stage builds work internally:
Dockerfile:
FROM ubuntu:22.04 AS builder # Stage 1
RUN apt-get install g++ cmake
COPY . .
RUN cmake -B build && make
FROM debian:slim AS runtime # Stage 2
COPY --from=builder /src/build/myapp /bin/
CMD ["/bin/myapp"]
Docker build flow:
1. Image layers:
Stage 1 (builder):
Layer 0: ubuntu:22.04 base (~200MB)
Layer 1: RUN apt-get install... (+150MB)
Layer 2: COPY source (+10MB)
Layer 3: RUN cmake build (+50MB)
Total builder image: ~410MB
2. Stage 2 starts:
New image (independent of previous stage!)
Layer 0: debian:slim base (~40MB)
Layer 1: COPY --from=builder /src/build/myapp
→ extracts only files from builder Layer 3
→ copies binary myapp only (+5MB)
Total runtime image: ~45MB
3. Final image selection:
docker build result:
→ Only the last FROM is tagged!
→ Builder stage is intermediate output only
docker images:
myapp:latest → 45MB (runtime)
Builder may be discarded or kept as cache
Layer reuse:
When the Dockerfile changes:
FROM ubuntu:22.04 AS builder
RUN apt-get install g++ cmake # unchanged → cached
COPY main.cpp . # file changed! → rebuild
RUN cmake build # must rebuild
Cache checks:
1. Compare instruction + previous layer hash
2. COPY uses content checksums
3. Match → reuse cache; mismatch → rebuild
Layer chain:
Stage 1:
┌─────────────────┐
│ ubuntu:22.04 │ Layer 0 (cached)
├─────────────────┤
│ RUN apt-get... │ Layer 1 (cached)
├─────────────────┤
│ COPY source │ Layer 2 (changed, rebuild)
├─────────────────┤
│ RUN build │ Layer 3 (rebuild)
└─────────────────┘
↓
COPY --from=builder (extract files from Layer 3 only)
↓
┌─────────────────┐
│ debian:slim │ Layer 0 (new image)
├─────────────────┤
│ myapp (5MB) │ Layer 1
└─────────────────┘
COPY --from internals:
COPY --from=builder /src/build/myapp /bin/
1. Mount filesystem from builder stage final layer
2. Read /src/build/myapp
3. Write to current stage (runtime) /bin/
4. Creates a new layer
→ Compiler, sources, and other builder files are NOT copied!
BuildKit parallelism:
Dockerfile:
FROM ubuntu AS base
RUN install common
FROM base AS builder1
RUN build app1
FROM base AS builder2
RUN build app2
FROM slim AS final
COPY --from=builder1 /app1 .
COPY --from=builder2 /app2 .
BuildKit:
┌────────────┐
│ base │
└─────┬──────┘
├─────────────┬────────────
↓ ↓
┌──────────┐ ┌──────────┐
│ builder1 │ │ builder2 │ ← parallel builds
└────┬─────┘ └────┬─────┘
└──────┬───────┘
↓
┌──────────┐
│ final │
└──────────┘
Image size comparison:
Single stage:
FROM ubuntu:22.04
RUN apt-get install g++ cmake git (150MB)
COPY source (10MB)
RUN build (50MB)
→ Final image: ~410MB
Multi-stage:
Stage 1 (builder): 410MB (discarded)
Stage 2 (runtime): 45MB (kept)
→ Final image: 45MB
Security benefits:
- Remove tools attackers could abuse
- No compiler (harder to inject build steps)
- No shell (distroless)
- Fewer CVEs in the final image
Separating build and runtime stages
- Multi-stage: use multiple FROM lines in one Dockerfile and name stages with AS. Compile in the first stage; in the second, use a slim base (alpine, distroless, etc.) and COPY —from=build_stage to bring in the binary and required
.sofiles only. - The final image then excludes compilers, headers, and build tools—smaller size and smaller attack surface.
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; debian:bookworm-slim receives only myapp via COPY —from=builder …. Installing libstdc++6 at runtime satisfies dynamic linking for typical C++ binaries.
# 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 other
.sofiles in the runtime stage. Often better for image size and maintenance. - Static: flags like
-static-libstdc++can reduce runtime packages but check licensing and ABI policy.
# Static linking example (CMake)
# In CMakeLists.txt:
# set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static-libstdc++")
vcpkg + multi-stage full example
# ========== 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]
4. Deployment image optimization
Slim bases and tidy layers
- Base: alpine is small but uses musl, not glibc, so many C++ binaries won’t match—prefer debian-slim, ubuntu minimal, or distroless for glibc binaries.
- Trim fat: use
--no-install-recommendswithapt-get, thenrm -rf /var/lib/apt/lists/to shrink layers. - Single RUN: combining steps can reduce layer count and size slightly.
Base image comparison
| Base | Size | glibc | C++ OK | Notes |
|---|---|---|---|---|
| ubuntu:22.04 | ~77MB | ✅ | ✅ | Safe default |
| debian:bookworm-slim | ~80MB | ✅ | ✅ | Often recommended |
| alpine:3.19 | ~7MB | ❌ (musl) | ⚠️ | glibc binaries won’t run |
| gcr.io/distroless/cc-debian12 | ~50MB | ✅ | ✅ | No shell, hardened |
Distroless example
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 you may need docker run --entrypoint overrides.
5. Common errors and fixes
Error 1: “cannot find -lstdc++” / “undefined reference”
Cause: libstdc++ missing in the runtime stage.
Fix:
RUN apt-get update && apt-get install -y --no-install-recommends \
libstdc++6 libgcc-s1 \
&& rm -rf /var/lib/apt/lists/*
Error 2: “error while loading shared libraries: libxxx.so.X”
Cause: dynamically linked library not present in the runtime image.
Fix:
# In the build stage, inspect binary dependencies
ldd build/myapp
Install missing .so packages in runtime or COPY —from=builder.
COPY --from=builder /usr/lib/x86_64-linux-gnu/libfoo.so.1 /usr/lib/x86_64-linux-gnu/
Error 3: Alpine — “FATAL: kernel too old”
Cause: Alpine uses musl; glibc-linked binaries won’t run there.
Fix: prefer debian-slim or distroless for typical glibc C++ deploys. To use Alpine, build on Alpine.
# Build on Alpine
FROM alpine:3.19 AS builder
RUN apk add --no-cache g++ cmake make
# ...
Error 4: “no such file or directory” when running the binary
Cause: missing dynamic linker or libs, or 32/64-bit mismatch.
Fix:
file build/myapp
ldd build/myapp
Error 5: vcpkg “CMAKE_TOOLCHAIN_FILE” not found
Cause: wrong vcpkg path or COPY order—vcpkg not present yet.
Fix:
ENV VCPKG_ROOT=/opt/vcpkg
RUN git clone https://github.com/Microsoft/vcpkg.git ${VCPKG_ROOT} \
&& ${VCPKG_ROOT}/bootstrap-vcpkg.sh -disableMetrics
COPY vcpkg.json .
RUN ${VCPKG_ROOT}/vcpkg install
# Then copy sources
COPY . .
RUN cmake -B build -DCMAKE_TOOLCHAIN_FILE=${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake
Error 6: Docker build cache feels stale
Cause: layers cached after dependency changes.
Fix:
docker build --no-cache -t myapp .
Usually prefer COPY order: dependencies → install → source → build so source-only edits rebuild only the last layers.
Error 7: “exec user process caused: exec format error”
Cause: architecture mismatch (e.g., x86_64 image on ARM Mac without emulation).
Fix:
docker build --platform linux/amd64 -t myapp .
Error 8: “vcpkg install” takes forever
Cause: vcpkg rebuilds packages from scratch each time.
Fix: BuildKit cache mounts:
# syntax=docker/dockerfile:1.4
RUN --mount=type=cache,target=/root/.cache/vcpkg \
${VCPKG_ROOT}/vcpkg install
Or maintain a prebuilt builder image:
FROM my-registry/cpp-builder:22.04 AS builder
# vcpkg already installed and cached
Error 9: “Permission denied” running the binary
Cause: switched to USER nobody but the binary isn’t executable.
Fix:
COPY --from=builder /src/build/myapp /usr/local/bin/
RUN chmod +x /usr/local/bin/myapp
Error 10: “failed to solve: process did not complete successfully”
Cause: a RUN step failed (missing apt package, network error, etc.).
Fix: narrow down with verbose logs:
docker build --progress=plain --no-cache -t myapp . 2>&1 | tee build.log
6. Optimization tips
1. Layer cache discipline
# ❌ Bad: source change reinstalls dependencies
COPY . .
RUN apt-get install ....&& vcpkg install && cmake --build build
# ✅ Good: dependencies before source
COPY vcpkg.json .
RUN vcpkg install
COPY . .
RUN cmake --build build
2. Use .dockerignore
# .dockerignore
.git
.gitignore
build/
.vscode/
*.md
Dockerfile
.dockerignore
Excluding junk shrinks build context and improves cache hits.
3. Multi-arch builds
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest --push .
4. BuildKit
# syntax=docker/dockerfile:1.4
RUN --mount=type=cache,target=/root/.cache/vcpkg \
vcpkg install
Set DOCKER_BUILDKIT=1 to enable cache mounts and other features.
5. Image size ballpark
| Setup | Approximate size |
|---|---|
| ubuntu + build tools + source | 1.5–2GB |
| debian-slim + binary only | 80–150MB |
| distroless + binary only | 50–80MB |
6. Faster builds
RUN cmake --build build -j$(nproc)
7. Fewer layers
# ❌ Bad: many RUNs → many layers
RUN apt-get update
RUN apt-get install -y g++
RUN rm -rf /var/lib/apt/lists/*
# ✅ Good: one chained RUN
RUN apt-get update && apt-get install -y --no-install-recommends g++ \
&& rm -rf /var/lib/apt/lists/*
8. Build args for flexibility
ARG CMAKE_BUILD_TYPE=Release
RUN cmake -B build -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} \
&& cmake --build build
docker build --build-arg CMAKE_BUILD_TYPE=Debug -t myapp:debug .
7. Production patterns
Pattern 1: version tags
docker build -t myapp:1.2.3 -t myapp:latest .
Semantic versions simplify rollback and traceability.
Pattern 2: non-root user
FROM debian:bookworm-slim
RUN groupadd -r app && useradd -r -g app app
COPY --from=builder /src/build/myapp /usr/local/bin/
USER app
CMD [myapp]
Pattern 3: health checks
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD /usr/local/bin/myapp --health || exit 1
Implement --health in the app for container orchestrators.
Pattern 4: secrets and env
docker run -e DATABASE_URL=....myapp
Prefer Docker Secrets or Kubernetes Secrets for sensitive values.
Pattern 5: CI/CD integration
# GitHub Actions sketch
- name: Build Docker image
run: docker build -t myapp:${{ github.sha }} .
- name: Push to registry
run: docker push myregistry/myapp:${{ github.sha }}
Pattern 6: tag intermediate stages
# Push builder image separately for CI cache warm-up
FROM ubuntu:22.04 AS builder
# ...
Prebuild and push builder so frequent source-only changes reuse it.
Pattern 7: logging and monitoring
Prefer stdout for logs—easy for Docker/Kubernetes to collect:
spdlog::info("Request received, id={}", request_id);
spdlog::error("Connection failed: {}", err.message());
JSON patterns integrate well with ELK, Loki, etc.:
spdlog::set_pattern(R"({"time":"%Y-%m-%d %H:%M:%S","level":"%l","msg":"%v"})");
Pattern 8: graceful shutdown
#include <csignal>
#include <atomic>
std::atomic<bool> g_running{true};
void signal_handler(int) { g_running = false; }
int main() {
std::signal(SIGTERM, signal_handler);
std::signal(SIGINT, signal_handler);
while (g_running) {
// work
}
return 0;
}
docker stop and kubectl delete pod send SIGTERM.
Pattern 9: resource limits
docker run -m 512m --cpus=0.5 myapp
In Kubernetes, use resources.limits.
Build pipeline sequence
sequenceDiagram
participant Dev as Developer
participant Docker as Docker
participant Registry as Registry
participant K8s as Kubernetes
Dev->>Docker: docker build
Docker->>Docker: builder stage (compile)
Docker->>Docker: runtime stage (copy)
Docker->>Docker: image produced
Dev->>Registry: docker push
Registry->>K8s: deploy
K8s->>K8s: Pod restart (new image)
8. Local development, Compose, Dev Container
CI-only docker build can slow the edit → build → test loop. Keep sources on the host and pin only the toolchain in a container to align team standards with local productivity.
8.1 Host source + container build (volume mount)
Assume a dev image already has compiler, CMake, and vcpkg.
# From project root: source on host, build inside container or on a volume
docker run --rm -it \
-v "$(pwd):/src" -w /src \
my-cpp-dev:22.04 \
bash -lc "cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug && cmake --build build -j\$(nproc)"
-v "$(pwd):/src": edit on the host (VS Code/Cursor), compile in the container.-u $(id -u):$(id -g)(Linux): align file ownership forbuild/output.- On Windows/macOS Docker Desktop, UID mapping differs—adjust per team policy.
8.2 docker compose for repeatable dev commands
A compose.yaml that builds the dev image and opens a shell simplifies onboarding.
# compose.yaml (example)
services:
dev:
build:
context: .
dockerfile: Dockerfile
target: dev # dev stage in Dockerfile below
volumes:
- .:/src
working_dir: /src
stdin_open: true
tty: true
command: bash
docker compose run --rm dev bash -lc "cmake -B build -G Ninja && cmake --build build"
Add a dev target to a multi-stage Dockerfile:
# Same packages as builder + debugging tools
FROM ubuntu:22.04 AS dev
RUN apt-get update && apt-get install -y --no-install-recommends \
g++ cmake ninja-build git gdb lldb valgrind ca-certificates \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /src
# Add vcpkg etc. per team standard
Use target: dev to separate deployment runtime from developer images in one Dockerfile.
8.3 Dev Container (VS Code / Cursor)
Place .devcontainer/devcontainer.json in the repo for “open repo → same container”. Pairs well with clangd + CMake Tools from the C++ environment guide.
// Example
{
"name": "cpp-vcpkg",
"build": {
"dockerfile": "../Dockerfile",
"context": "..",
"target": "dev"
},
"workspaceFolder": "/src",
"workspaceMount": "source=${localWorkspaceFolder},target=/src,type=bind,consistency=cached",
"customizations": {
"vscode": {
"extensions": [
"ms-vscode.cmake-tools",
"llvm-vs-code-extensions.vscode-clangd"
],
"settings": {
"cmake.configureOnOpen": true,
"C_Cpp.intelliSenseEngine": "disabled"
}
}
}
}
- If
devcontainer.jsonlives under.devcontainer/, pointing at the repo rootDockerfileoften uses"dockerfile": "../Dockerfile"and"context": "..". If the Dockerfile sits next to the json,"dockerfile": "Dockerfile"alone is enough. - If there is no
devstage yet, addAS devin the Dockerfile or droptarget. - Enable
CMAKE_EXPORT_COMPILE_COMMANDS=ONso CMake presets and clangd seecompile_commands.json.
8.4 Ninja + ccache to speed Docker builds
Ninja often scales better than Make on large graphs. ccache with BuildKit cache mounts survives layer invalidation better than plain layers.
# syntax=docker/dockerfile:1.4
FROM ubuntu:22.04 AS builder
RUN apt-get update && apt-get install -y --no-install-recommends \
g++ cmake ninja-build ccache \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /src
COPY . .
ENV CCACHE_DIR=/root/.cache/ccache
RUN --mount=type=cache,target=/root/.cache/ccache \
cmake -B build -G Ninja -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
&& cmake --build build -j$(nproc)
For local dev, a named volume like -v my_ccache:/root/.cache/ccache keeps cache across container deletes.
8.5 GDB inside containers
Default security profiles may restrict ptrace, breaking GDB.
docker run --rm -it \
--cap-add=SYS_PTRACE \
-v "$(pwd):/src" -w /src \
my-cpp-dev:22.04 \
gdb --args ./build/myapp
Some setups need --security-opt seccomp=unconfined—use only for local debugging because it weakens isolation.
Distroless runtimes lack shell and GDB—debug with the dev image; ship only the runtime stage to production.
8.6 Optional Sanitizer / Debug stage
For ASan/UBSan jobs in CI, keep a separate stage from Release runtime (replace builder with your actual stage name):
FROM builder AS sanitize
ENV CXXFLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -g"
ENV LDFLAGS="-fsanitize=address,undefined"
# If using vcpkg/Conan, pass the same -DCMAKE_TOOLCHAIN_FILE=... as builder.
RUN cmake -B build-san -DCMAKE_BUILD_TYPE=Debug -G Ninja \
&& cmake --build build-san
# Then ctest or custom scripts
Don’t ship Sanitizer binaries to production. See the Sanitizer section in the environment guide for flags.
9. Complete example project
Layout
cpp-docker-demo/
├── CMakeLists.txt
├── vcpkg.json
├── Dockerfile
├── .dockerignore
└── src/
└── main.cpp
main.cpp
#include <spdlog/spdlog.h>
#include <iostream>
int main() {
spdlog::info("C++ Docker app starting.");
std::cout << "Hello from Docker!\n";
return 0;
}
vcpkg.json
{
"name": "cpp-docker-demo",
"version": "1.0.0",
"dependencies": ["spdlog"]
}
CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(cpp-docker-demo LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
add_executable(myapp src/main.cpp)
find_package(spdlog CONFIG REQUIRED)
target_link_libraries(myapp PRIVATE spdlog::spdlog)
Dockerfile (full)
# syntax=docker/dockerfile:1.4
# ========== Build stage ==========
FROM ubuntu:22.04 AS builder
ARG DEBIAN_FRONTEND=noninteractive
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
# ========== Runtime stage ==========
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/*
RUN groupadd -r app && useradd -r -g app app
COPY --from=builder /src/build/myapp /usr/local/bin/
USER app
CMD [myapp]
.dockerignore
.git
.gitignore
build/
.vscode/
*.md
*.log
Build and run
docker build -t cpp-docker-demo:latest .
docker run --rm cpp-docker-demo:latest
Deployment checklist
- Pin base image version (
ubuntu:22.04, etc.) - Declare dependencies in
vcpkg.json/conanfile.txt - COPY order: dependencies → install → source → build
- Multi-stage build to exclude build tools from runtime
- Install required
.so(e.g.libstdc++6) in runtime -
.dockerignoreto exclude noise - Run as non-root
- Version your image tags (
myapp:1.2.3) - Prefer
debian-slimover Alpine for glibc binaries - Use
--no-install-recommends
Debugging tips
docker run -it --rm --entrypoint /bin/bash myapp:build
docker run -it --rm --entrypoint /bin/bash debian:bookworm-slim
docker run --rm --entrypoint /usr/bin/ldd myapp:latest /usr/local/bin/myapp
10. Summary
| Topic | Takeaway |
|---|---|
| Build image | Pin compiler, CMake, dependencies—same dev and CI |
| Multi-stage | Build in stage 1 → copy only the binary to runtime |
| Deploy optimization | Slim bases (debian-slim, distroless), trim packages, fewer layers |
| Caching | COPY order: dependencies → install → source → build |
| Local & onboarding | Volume mounts, compose, dev target, Dev Container |
| Speed | Ninja, ccache + BuildKit cache mounts |
| Alpine | musl—not for typical glibc binaries |
| Production | non-root, version tags, health checks |
Part 40 closes package managers (vcpkg/Conan) → CI/CD (GitHub Actions) → containers (Docker)—modernizing build and deploy.
Related reading (internal links)
- C++ package management: vcpkg & Conan (#40-1)
- C++ development environment guide (OS, hardware, clangd, Sanitizers, CI alignment)
- C++ CI/CD: multi-OS builds with GitHub Actions
Practical checklist
Before you code
- Is this the right fix for the problem?
- Can the team maintain it?
- Does it meet performance needs?
While coding
- Warnings addressed?
- Edge cases considered?
- Error handling appropriate?
At review
- Intent clear?
- Tests sufficient?
- Documented where needed?
Use this checklist to reduce mistakes and improve quality.
Keywords (search)
Search for Docker C++, container build, multi-stage build, C++ Docker image optimization to find related material.
FAQ
Q. When do I use this at work?
A. Whenever you need a pinned compiler and dependencies in Docker, and you want small deployment images via multi-stage builds and minimal bases. Apply the examples and decision guides above.
Q. Should I avoid Alpine for C++?
A. Alpine uses musl. Binaries linked against glibc won’t run there. Build on Alpine if you must use Alpine, and watch library compatibility. debian-slim is usually safer for glibc C++.
Q. How do I shrink images further?
A. (1) distroless base, (2) consider -static-libstdc++ with license/ABI review, (3) remove unused dependencies, (4) --no-install-recommends.
Q. What should I read first?
A. Follow previous post links at the bottom of each article, or open the C++ series index for order.
Q. Dev Container vs plain docker build?
A. docker build focuses on images and deploy artifacts; Dev Containers standardize the editor-attached dev shell with the repo. They can share the same Dockerfile.
Q. Where to go deeper?
A. Docker docs, vcpkg, Dev Containers spec, cppreference.
One-line summary: Docker and multi-stage builds unify build environments and shrink deployment images. Next, consider Clang-Tidy & Cppcheck (#41-1).
Previous post: DevOps for C++ #40-2: CI/CD with GitHub Actions
Next post: Stability #41-1: static analysis with Clang-Tidy and Cppcheck
See also
- C++23 features:
std::expected,mdspan - C++ package management: vcpkg & Conan (#40-1)
- C++ CI/CD with GitHub Actions
- C++26 features
- Cache-friendly C++: data-oriented design
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ CI/CD 파이프라인: GitHub Actions를 이용한 멀티 OS 자동 빌드·테스트 가이드
- C++ 패키지 관리 실무: vcpkg와 Conan으로 외부 라이브러리 의존성 지옥 탈출 [#40-1]
- C++ 개발 환경 완벽 가이드 | OS·하드웨어·도구·설정 추천
- C++ 패키지 매니저 | vcpkg·Conan으로 ‘라이브러리 설치 지옥’ 탈출하기
이 글에서 다루는 키워드 (관련 검색어)
C++, Docker, container, multi-stage build, Dev Container, docker-compose 등으로 검색하시면 이 글이 도움이 됩니다.