본문으로 건너뛰기
Previous
Next
C++ DevContainer & Docker Guide — Standardize Builds,

C++ DevContainer & Docker Guide — Standardize Builds,

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:

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

  1. Problem scenario: environment mismatch hell
  2. Build environment image
  3. Multi-stage builds
  4. Deployment image optimization
  5. Common errors and fixes
  6. Optimization tips
  7. Production patterns
  8. Local development, Compose, Dev Container
  9. Complete example project
  10. 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

  1. Compiler and library version drift: different gcc/clang and glibc per developer
  2. Platform mismatch: binaries built on macOS won’t run on Linux servers
  3. Hidden dependencies: headers/libs only installed on one machine
  4. 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:, or docker run for 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 .so files 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 .so files 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-recommends with apt-get, then rm -rf /var/lib/apt/lists/ to shrink layers.
  • Single RUN: combining steps can reduce layer count and size slightly.

Base image comparison

BaseSizeglibcC++ OKNotes
ubuntu:22.04~77MBSafe default
debian:bookworm-slim~80MBOften recommended
alpine:3.19~7MB❌ (musl)⚠️glibc binaries won’t run
gcr.io/distroless/cc-debian12~50MBNo 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

SetupApproximate size
ubuntu + build tools + source1.5–2GB
debian-slim + binary only80–150MB
distroless + binary only50–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 for build/ 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.json lives under .devcontainer/, pointing at the repo root Dockerfile often uses "dockerfile": "../Dockerfile" and "context": "..". If the Dockerfile sits next to the json, "dockerfile": "Dockerfile" alone is enough.
  • If there is no dev stage yet, add AS dev in the Dockerfile or drop target.
  • Enable CMAKE_EXPORT_COMPILE_COMMANDS=ON so CMake presets and clangd see compile_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
  • .dockerignore to exclude noise
  • Run as non-root
  • Version your image tags (myapp:1.2.3)
  • Prefer debian-slim over 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

TopicTakeaway
Build imagePin compiler, CMake, dependencies—same dev and CI
Multi-stageBuild in stage 1 → copy only the binary to runtime
Deploy optimizationSlim bases (debian-slim, distroless), trim packages, fewer layers
CachingCOPY order: dependencies → install → source → build
Local & onboardingVolume mounts, compose, dev target, Dev Container
SpeedNinja, ccache + BuildKit cache mounts
Alpinemusl—not for typical glibc binaries
Productionnon-root, version tags, health checks

Part 40 closes package managers (vcpkg/Conan)CI/CD (GitHub Actions)containers (Docker)—modernizing build and deploy.


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.


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++, Docker, container, multi-stage build, Dev Container, docker-compose 등으로 검색하시면 이 글이 도움이 됩니다.