Docker DevContainer로 C++ 빌드 환경 통일하기
들어가며: 개발·CI·배포를 한 이미지로
40-1과 40-2에서 의존성 관리와 CI를 다뤘다면, 이번에는 그 환경 자체를 컨테이너 이미지로 고정합니다. C++은 같은 소스라도 컴파일러 버전, 표준 라이브러리, glibc 버전에 따라 빌드 결과와 실행 가능 여부가 달라집니다. 팀원은 Ubuntu 22.04와 24.04를 섞어 쓰고, CI는 또 다른 이미지에서 돌고, 배포 서버의 glibc가 빌드 머신보다 오래되어 GLIBC_2.34 not found로 실행이 안 되는 식의 문제가 여기서 나옵니다.
Docker로 빌드용 이미지(컴파일러, CMake, vcpkg/Conan)와 실행용 이미지(런타임 라이브러리만)를 나누면, 개발자 로컬과 CI와 배포가 같은 툴체인을 씁니다. 이 글에서는 빌드 이미지의 캐시 레이어 구성, 멀티 스테이지 빌드, 볼륨 마운트·docker compose·Dev Container를 이용한 로컬 개발, ccache와 BuildKit 캐시 마운트, 컨테이너 안 GDB, 그리고 자주 나는 에러를 다룹니다.
빌드 환경 이미지
레이어 캐시를 고려한 순서
Dockerfile의 각 명령은 레이어가 되고, Docker는 명령과 입력(COPY라면 파일 내용)이 이전과 같으면 그 레이어를 캐시에서 재사용합니다. 어떤 레이어가 바뀌면 그 아래 레이어는 모두 다시 실행됩니다. 그래서 자주 바뀌지 않는 것(시스템 패키지, 의존성 목록)을 위에, 자주 바뀌는 것(소스)을 아래에 둡니다. 소스 한 줄만 고쳤는데 vcpkg 의존성이 처음부터 다시 빌드된다면, 대개 COPY . .이 의존성 설치보다 위에 있는 경우입니다.
vcpkg (Manifest 모드)
my-cpp-app/
├── CMakeLists.txt
├── vcpkg.json
├── Dockerfile
└── src/
└── main.cpp
{
"name": "my-cpp-app",
"version": "1.0.0",
"dependencies": ["spdlog"]
}
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)
FROM debian:bookworm AS builder
RUN apt-get update && apt-get install -y --no-install-recommends \
g++ cmake ninja-build git ca-certificates curl zip unzip tar pkg-config \
&& rm -rf /var/lib/apt/lists/*
# vcpkg 설치 (자주 바뀌지 않으므로 위쪽 레이어)
ENV VCPKG_ROOT=/opt/vcpkg
RUN git clone https://github.com/microsoft/vcpkg.git ${VCPKG_ROOT} \
&& ${VCPKG_ROOT}/bootstrap-vcpkg.sh -disableMetrics
WORKDIR /src
# 의존성 목록만 먼저 복사 → vcpkg.json이 같으면 이 레이어는 캐시됨
COPY vcpkg.json .
RUN ${VCPKG_ROOT}/vcpkg install --x-install-root=/src/vcpkg_installed
# 소스 복사 후 빌드
COPY CMakeLists.txt .
COPY src/ src/
RUN cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_TOOLCHAIN_FILE=${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake \
-DVCPKG_INSTALLED_DIR=/src/vcpkg_installed \
&& cmake --build build
몇 가지가 빠지기 쉬운 부분입니다. vcpkg의 부트스트랩과 포트 빌드에는 curl, zip, unzip, tar, pkg-config가 필요하므로 최소 이미지에는 직접 설치해야 합니다. 또 vcpkg CMake 툴체인은 기본으로 ${CMAKE_BINARY_DIR}/vcpkg_installed에 패키지를 설치합니다. 앞 레이어에서 vcpkg install로 미리 설치한 위치와 이 경로가 다르면, CMake 설정 단계에서 모든 의존성을 다시 빌드해 캐시 레이어를 둔 의미가 사라집니다. 위 예에서 --x-install-root와 VCPKG_INSTALLED_DIR을 같은 경로로 맞춘 이유입니다.
git clone한 vcpkg는 클론 시점의 포트 버전을 씁니다. 재현성을 높이려면 vcpkg를 특정 커밋으로 체크아웃하거나, vcpkg.json에 builtin-baseline을 지정해 포트 버전을 고정합니다.
Conan 2
[requires]
spdlog/1.12.0
[generators]
CMakeDeps
CMakeToolchain
FROM debian:bookworm AS builder
RUN apt-get update && apt-get install -y --no-install-recommends \
g++ cmake ninja-build git python3 python3-venv \
&& rm -rf /var/lib/apt/lists/*
RUN python3 -m venv /opt/conan && /opt/conan/bin/pip install conan
ENV PATH=/opt/conan/bin:$PATH
RUN conan profile detect
WORKDIR /src
COPY conanfile.txt .
RUN conan install . --output-folder=build --build=missing -s build_type=Release
COPY CMakeLists.txt .
COPY src/ src/
RUN cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_TOOLCHAIN_FILE=build/conan_toolchain.cmake \
&& cmake --build build
Debian 12와 Ubuntu 23.04 이후는 시스템 Python에 pip install을 막아 두었습니다(PEP 668). --break-system-packages로 우회할 수도 있지만, 이 옵션은 pip 23.0.1 이후에만 있어 Ubuntu 22.04의 pip에서는 오류가 납니다. 가상 환경에 설치하면 배포판에 상관없이 동작합니다. Conan 2는 처음 실행할 때 conan profile detect로 기본 프로필을 만들어야 하고, 프로필의 build_type과 CMake의 CMAKE_BUILD_TYPE이 일치해야 find_package가 패키지를 찾습니다.
CI에서 이미지 쓰기
GitHub Actions에서는 Docker로 빌드한 뒤 결과 바이너리만 꺼내거나, 빌드 이미지를 container:로 지정해 Job 전체를 그 안에서 실행할 수 있습니다.
- name: Build with Docker
run: |
docker build --target builder -t myapp:build .
docker create --name extract myapp:build
docker cp extract:/src/build/myapp ./myapp
docker rm extract
jobs:
build:
runs-on: ubuntu-latest
container:
image: ghcr.io/my-org/cpp-builder:bookworm # 미리 만들어 둔 빌드 이미지
steps:
- uses: actions/checkout@v4
- run: cmake -B build -G Ninja && cmake --build build
멀티 스테이지 빌드
하나의 Dockerfile에 FROM을 여러 번 쓰고 AS로 이름을 붙이면, 각 스테이지가 독립된 이미지가 됩니다. 뒤 스테이지는 COPY --from=<스테이지>로 앞 스테이지의 파일 중 필요한 것만 가져옵니다. docker build가 최종적으로 태그하는 이미지는 마지막 스테이지(또는 --target으로 지정한 스테이지)이므로, 빌드 스테이지에 설치한 컴파일러·헤더·소스는 최종 이미지에 포함되지 않습니다. BuildKit은 서로 의존하지 않는 스테이지를 병렬로 빌드하고, 최종 스테이지에 필요 없는 스테이지는 아예 건너뜁니다.
flowchart TB
subgraph stage1["스테이지 1: builder"]
S1[debian:bookworm] --> S2[g++, cmake 설치]
S2 --> S3[소스 복사]
S3 --> S4[빌드]
S4 --> S5[myapp 바이너리]
end
subgraph stage2["스테이지 2: runtime"]
R1[debian:bookworm-slim] --> R2[런타임 라이브러리만]
R2 --> R3[COPY --from=builder]
end
S5 -.->|COPY --from| R3
# 스테이지 1: 빌드 (위 vcpkg 예시와 동일)
FROM debian:bookworm AS builder
# ... 의존성 설치와 빌드 ...
# 스테이지 2: 런타임
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/myapp
USER app
CMD ["myapp"]
빌드 스테이지와 런타임 스테이지의 배포판 버전을 맞춘 데는 이유가 있습니다. 바이너리는 빌드할 때 링크한 glibc와 libstdc++의 심볼 버전을 요구하므로, 새 배포판에서 빌드해 오래된 배포판에서 실행하면 version 'GLIBC_2.34' not found나 GLIBCXX_3.4.30 not found로 실행되지 않습니다. 반대 방향(오래된 곳에서 빌드, 새 곳에서 실행)은 대체로 동작하지만, 같은 배포판·같은 버전으로 맞추는 편이 가장 확실합니다.
CMD와 ENTRYPOINT는 ["myapp"]처럼 따옴표가 있는 JSON 배열로 써야 exec 형식이 됩니다. CMD [myapp]처럼 따옴표를 빼면 JSON으로 해석되지 않아 셸 형식(/bin/sh -c "[myapp]")으로 처리되고, 그런 명령은 없으므로 실행에 실패합니다. 셸이 없는 distroless 이미지에서는 셸 형식 자체가 동작하지 않습니다. exec 형식이어야 애플리케이션이 PID 1이 되어 docker stop의 SIGTERM을 직접 받는다는 점도 중요합니다.
동적 링크와 정적 링크
기본으로 C++ 바이너리는 libstdc++, libgcc_s, glibc를 동적으로 링크하므로, 런타임 이미지에 이 라이브러리가 있어야 합니다. -static-libstdc++ -static-libgcc로 C++ 런타임만 정적으로 넣으면 런타임 이미지에서 libstdc++를 설치하지 않아도 되고, 배포판 간 libstdc++ 버전 차이 문제도 피할 수 있습니다. glibc까지 정적으로 링크(-static)하는 것은 권장되지 않습니다. getaddrinfo 같은 함수가 실행 시점에 NSS 모듈을 동적으로 불러오려 하므로 경고가 나고 이름 해석이 기대와 다르게 동작할 수 있습니다. 완전 정적 바이너리가 필요하면 musl 기반(Alpine) 툴체인에서 빌드하는 쪽이 일반적입니다.
# C++ 런타임만 정적 링크
target_link_options(myapp PRIVATE -static-libstdc++ -static-libgcc)
베이스 이미지 선택
debian:bookworm-slim이나 ubuntu 같은 일반 배포판은 셸과 패키지 매니저가 있어 디버깅이 쉽습니다. gcr.io/distroless/cc-debian12는 glibc, libstdc++, CA 인증서 정도만 있고 셸과 패키지 매니저가 없어 이미지가 작고 취약점 스캔에서 걸리는 패키지 수가 크게 줄어듭니다. 대신 컨테이너 안에서 셸을 열 수 없으므로 디버깅은 개발 이미지에서 합니다. Alpine은 가장 작지만 musl libc를 쓰므로 glibc 환경에서 빌드한 바이너리는 실행되지 않고, Alpine에서 다시 빌드해야 합니다.
FROM debian:bookworm AS builder
# ... 빌드 ...
FROM gcr.io/distroless/cc-debian12:nonroot
COPY --from=builder /src/build/myapp /app/myapp
ENTRYPOINT ["/app/myapp"]
distroless의 :nonroot 태그는 기본 사용자가 비root(UID 65532)로 설정되어 있습니다. distroless의 cc 이미지는 Debian 12 기반이므로 빌드 스테이지도 Debian 12(bookworm)로 맞춥니다.
자주 나는 에러
error while loading shared libraries: libxxx.so.X
바이너리가 동적으로 링크한 라이브러리가 런타임 이미지에 없습니다. libstdc++가 없으면 libstdc++.so.6: cannot open shared object file로 나타납니다. 빌드 스테이지에서 ldd로 필요한 라이브러리를 확인하고, 런타임 스테이지에 해당 패키지를 설치하거나 파일을 복사합니다.
ldd build/myapp
COPY --from=builder /usr/lib/x86_64-linux-gnu/libfoo.so.1 /usr/lib/x86_64-linux-gnu/
vcpkg는 Linux에서 기본으로 정적 라이브러리(x64-linux 트리플렛)를 빌드하므로 vcpkg 의존성 때문에 이 에러가 나는 경우는 드물고, 대개 시스템 패키지로 설치한 라이브러리(OpenSSL, libpq 등)가 원인입니다.
파일이 있는데 no such file or directory
ls로 보면 바이너리가 분명히 있는데 실행하면 exec /usr/local/bin/myapp: no such file or directory가 나온다면, 없는 것은 바이너리가 아니라 ELF 헤더에 기록된 동적 로더(/lib64/ld-linux-x86-64.so.2)입니다. glibc로 빌드한 바이너리를 Alpine(musl)에서 실행하거나, 동적 링크된 바이너리를 scratch 이미지에 넣었을 때 생깁니다. file build/myapp으로 “dynamically linked, interpreter …”를 확인하고, glibc가 있는 베이스를 쓰거나 해당 환경에서 다시 빌드합니다.
version 'GLIBC_2.xx' not found
빌드 스테이지의 glibc가 런타임 스테이지보다 새것입니다. 두 스테이지의 배포판 버전을 맞춥니다.
exec format error
아키텍처가 맞지 않습니다. Apple Silicon Mac에서 만든 arm64 이미지를 x86_64 서버에서 실행하거나 그 반대인 경우입니다. 빌드할 때 --platform으로 대상 아키텍처를 지정합니다. 여러 아키텍처를 한 번에 지원하려면 docker buildx build --platform linux/amd64,linux/arm64 --push로 멀티 아키텍처 이미지를 만듭니다. 다른 아키텍처는 QEMU 에뮬레이션으로 빌드되므로 C++ 컴파일이 매우 느려질 수 있고, 규모가 크면 아키텍처별 네이티브 빌더를 쓰거나 크로스 컴파일을 고려합니다.
빌드 단계 실패 원인을 못 찾을 때
failed to solve: process ... did not complete successfully만 보이고 원인이 잘려 있다면, 전체 출력을 보도록 실행합니다.
docker build --progress=plain -t myapp . 2>&1 | tee build.log
캐시 때문에 의존성 변경이 반영되지 않는 것 같다면 --no-cache로 확인할 수 있지만, 근본적으로는 의존성 파일을 먼저 복사하는 순서가 맞는지 점검합니다.
빌드 시간 줄이기
.dockerignore
빌드 컨텍스트에 .git이나 로컬 build/ 디렉터리가 들어가면 전송 시간이 늘고, COPY . .의 캐시가 의미 없는 파일 변경으로 깨집니다. 특히 호스트에서 만든 build/의 CMakeCache.txt가 컨테이너로 복사되면 경로가 달라 CMake 설정이 실패할 수 있습니다.
# .dockerignore
.git
build/
.vscode/
*.log
BuildKit 캐시 마운트와 ccache
레이어 캐시는 레이어 입력이 조금이라도 바뀌면 전부 버려집니다. 소스 파일 하나를 고쳐도 빌드 레이어 전체가 다시 실행되어, 모든 번역 단위를 새로 컴파일합니다. BuildKit의 캐시 마운트는 레이어와 별개로 빌드 사이에 유지되는 디렉터리를 제공하므로, 여기에 ccache 저장소와 vcpkg 바이너리 캐시를 두면 바뀌지 않은 파일의 컴파일 결과를 재사용할 수 있습니다. Docker Engine 23 이후로는 BuildKit이 기본 빌더라 별도 설정이 필요 없습니다.
# syntax=docker/dockerfile:1
FROM debian:bookworm 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 \
--mount=type=cache,target=/root/.cache/vcpkg \
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
&& cmake --build build
/root/.cache/vcpkg는 Linux에서 vcpkg 바이너리 캐시의 기본 위치(~/.cache/vcpkg/archives)를 포함합니다. 캐시 마운트의 내용은 이미지에 들어가지 않으므로 이미지 크기에도 영향이 없습니다. 다만 캐시 마운트는 그 빌더 머신에만 남기 때문에, 매번 새 러너에서 시작하는 CI에서는 레지스트리 캐시(--cache-to type=registry)나 GitHub Actions 캐시 백엔드(type=gha)로 레이어 캐시를 따로 보존해야 효과가 있습니다.
Ninja는 기본으로 CPU 코어 수에 맞춰 병렬 빌드하므로 -j를 따로 줄 필요가 없습니다. Make 생성기를 쓴다면 cmake --build build -j"$(nproc)"처럼 지정합니다. 컨테이너에 메모리 제한이 있다면 템플릿이 많은 C++ 코드는 컴파일러 프로세스 하나가 수 GB를 쓸 수 있어, 병렬도를 낮춰야 OOM으로 빌드가 죽지 않습니다.
빌드 인자
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 .
ARG 값은 이미지 기록(docker history)에 남으므로 비밀 값을 넘기는 데 쓰면 안 됩니다. 빌드 중에 사설 저장소 토큰이 필요하면 RUN --mount=type=secret을 씁니다.
운영에서 챙길 것
이미지 태그는 myapp:1.2.3이나 커밋 SHA처럼 내용과 대응하는 값을 붙이고, latest만 쓰지 않습니다. 레지스트리에 푸시할 때는 레지스트리 경로가 포함된 이름으로 태그해야 합니다.
- name: Build and push
run: |
docker build -t ghcr.io/my-org/myapp:${{ github.sha }} .
docker push ghcr.io/my-org/myapp:${{ github.sha }}
컨테이너는 비root 사용자로 실행합니다. 앞의 예처럼 런타임 스테이지에서 사용자를 만들고 USER로 전환하거나, distroless의 :nonroot 태그를 씁니다. 실행 중 쓰기가 필요한 디렉터리가 있다면 USER 전환 전에 소유권을 맞춰 둡니다.
docker stop과 Kubernetes의 Pod 종료는 PID 1에 SIGTERM을 보내고, 일정 시간(Docker 기본 10초) 안에 끝나지 않으면 SIGKILL로 강제 종료합니다. PID 1은 커널이 기본 시그널 동작을 적용하지 않으므로, 애플리케이션이 SIGTERM을 처리하지 않으면 무시되고 매번 유예 시간을 다 기다린 뒤 강제 종료됩니다. 진행 중인 요청을 마무리하려면 SIGTERM을 받아 종료 절차를 시작해야 합니다.
#include <atomic>
#include <csignal>
std::atomic<bool> g_running{true}; // lock-free atomic은 시그널 핸들러에서 써도 안전
extern "C" void on_signal(int) { g_running = false; }
int main() {
std::signal(SIGTERM, on_signal);
std::signal(SIGINT, on_signal);
while (g_running) {
// 작업 수행
}
// 연결 정리, 버퍼 플러시
}
자식 프로세스를 띄우는 애플리케이션이라면 docker run --init으로 좀비 프로세스를 거둬 줄 init(tini)을 PID 1로 두는 것도 방법입니다.
로그는 파일이 아니라 표준 출력으로 내보내면 Docker와 Kubernetes가 수집합니다. spdlog의 패턴으로 JSON처럼 보이는 줄을 만들 수는 있지만, 메시지에 따옴표나 줄바꿈이 들어가면 깨진 JSON이 되므로 구조화 로그가 필요하면 값을 이스케이프하는 전용 포매터를 씁니다.
sequenceDiagram
participant Dev as 개발자/CI
participant Docker as BuildKit
participant Registry as 레지스트리
participant K8s as Kubernetes
Dev->>Docker: docker build
Docker->>Docker: builder 스테이지 빌드
Docker->>Docker: runtime 스테이지에 바이너리 복사
Dev->>Registry: docker push
K8s->>Registry: 새 태그 이미지 pull
K8s->>K8s: Pod 롤링 교체
로컬 개발: 볼륨 마운트·Compose·Dev Container
CI용 docker build만으로는 편집, 빌드, 테스트를 반복하는 사이클이 느립니다. 소스는 호스트에 두고 툴체인만 컨테이너로 고정하면 팀 표준과 로컬 생산성을 함께 얻을 수 있습니다.
볼륨 마운트
docker run --rm -it \
-u "$(id -u):$(id -g)" \
-v "$(pwd):/src" -w /src \
my-cpp-dev:bookworm \
bash -lc 'cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug && cmake --build build'
편집기는 호스트에서 쓰고 빌드는 컨테이너에서 합니다. Linux에서는 -u로 호스트 사용자와 같은 UID로 실행해야 build/ 안의 파일이 root 소유가 되지 않습니다. Docker Desktop(macOS, Windows)은 파일 공유 계층이 소유권을 변환해 주는 대신 바인드 마운트의 파일 I/O가 느리므로, 오브젝트 파일이 많이 생기는 빌드 디렉터리는 이름 있는 볼륨(-v build-cache:/src/build)에 두면 빌드가 빨라집니다.
docker compose
# compose.yaml
services:
dev:
build:
context: .
target: dev
volumes:
- .:/src
- ccache:/root/.cache/ccache
working_dir: /src
stdin_open: true
tty: true
command: bash
volumes:
ccache:
docker compose run --rm dev bash -lc "cmake -B build -G Ninja && cmake --build build"
같은 Dockerfile에 개발용 스테이지를 하나 더 두면 배포 이미지와 개발 이미지를 함께 관리할 수 있습니다.
FROM debian:bookworm AS dev
RUN apt-get update && apt-get install -y --no-install-recommends \
g++ cmake ninja-build git gdb valgrind ccache ca-certificates \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /src
Dev Container (VS Code / Cursor)
저장소에 .devcontainer/devcontainer.json을 두면, 저장소를 열 때 같은 컨테이너 안에서 편집기가 동작합니다. C++ 개발 환경 구축에서 다룬 clangd와 CMake Tools 구성이 그대로 들어갑니다.
{
"name": "cpp-dev",
"build": {
"dockerfile": "../Dockerfile",
"context": "..",
"target": "dev"
},
"workspaceFolder": "/src",
"workspaceMount": "source=${localWorkspaceFolder},target=/src,type=bind",
"runArgs": ["--cap-add=SYS_PTRACE"],
"customizations": {
"vscode": {
"extensions": [
"ms-vscode.cmake-tools",
"llvm-vs-code-extensions.vscode-clangd"
],
"settings": {
"cmake.configureOnOpen": true,
"C_Cpp.intelliSenseEngine": "disabled"
}
}
}
}
devcontainer.json이 .devcontainer/ 안에 있으므로 루트의 Dockerfile은 ../Dockerfile, 빌드 컨텍스트는 ..로 지정합니다. clangd가 빌드 설정을 정확히 알도록 CMake에서 CMAKE_EXPORT_COMPILE_COMMANDS=ON을 켜 compile_commands.json을 만들어 둡니다. Microsoft C/C++ 확장과 clangd를 함께 설치하면 두 엔진이 같은 진단을 이중으로 내므로, 위 설정처럼 C/C++ 확장의 IntelliSense를 끕니다.
컨테이너 안에서 GDB
Docker의 기본 seccomp 프로필은 최신 커널에서 ptrace를 허용하므로, 컨테이너 안에서 gdb ./build/myapp으로 프로그램을 시작하는 것은 대개 동작합니다. 다만 GDB가 주소 공간 무작위화를 끄려고 personality 시스템 호출을 쓰다 막혀 “Error disabling address space randomization: Operation not permitted” 경고가 나올 수 있고, 이 경고는 디버깅 자체를 막지는 않습니다. 이미 실행 중인 프로세스에 gdb -p로 붙으려면 CAP_SYS_PTRACE가 필요합니다.
docker run --rm -it --cap-add=SYS_PTRACE \
-v "$(pwd):/src" -w /src \
my-cpp-dev:bookworm gdb --args ./build/myapp
--security-opt seccomp=unconfined로 제한을 모두 푸는 방법도 있지만 격리를 크게 약화시키므로 로컬 디버깅에만 씁니다. distroless 런타임 이미지에는 셸도 GDB도 없으므로, 디버깅은 개발 이미지에서 하고 배포에는 런타임 스테이지만 푸시합니다.
Sanitizer 스테이지
CI에서 ASan/UBSan을 돌릴 때는 배포용 런타임과 분리된 스테이지를 둡니다. CMake는 처음 설정할 때 CXXFLAGS와 LDFLAGS 환경 변수로 컴파일·링크 플래그를 초기화합니다.
FROM builder AS sanitize
ENV CXXFLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -g"
ENV LDFLAGS="-fsanitize=address,undefined"
# vcpkg/Conan을 쓰는 builder라면 툴체인 옵션을 builder와 똑같이 넘깁니다.
RUN cmake -B build-san -G Ninja -DCMAKE_BUILD_TYPE=Debug \
&& cmake --build build-san \
&& ctest --test-dir build-san --output-on-failure
ASan은 기본 Docker 설정에서도 동작하지만, LeakSanitizer는 종료 시 ptrace를 쓰므로 일부 환경에서는 --cap-add=SYS_PTRACE 없이 “LeakSanitizer has encountered a fatal error”로 실패합니다. 운영 배포 이미지에는 sanitizer를 넣지 않습니다.
참고 자료
같이 보면 좋은 글
- C++ 패키지 관리 실무: vcpkg와 Conan으로 외부 라이브러리 의존성 지옥 탈출 [#40-1]
- C++ 개발 환경 구축 2026 | GCC 15·Clang 22·MSVC 14.51·C++23·C++26
- C++ CI/CD 파이프라인: GitHub Actions를 이용한 멀티 OS 자동 빌드·테스트 가이드
- C++23에서 실제로 쓸 기능
- C++ 캐시 효율적인 코드: 데이터 지향 설계 가이드
이전 글: DevOps for C++ #40-2: CI/CD GitHub Actions 다음 글: [안정성 확보 #41-1] 정적 분석 도구 통합: Clang-Tidy와 Cppcheck로 코드 퀄리티 강제하기