C++ 컨테이너 기반 개발: Docker로 빌드 환경 표준화 및 배포 이미지 최적화 [#40-3]
이 글의 핵심
Docker로 C++ 빌드·배포 환경을 통일하고, 로컬 개발(볼륨·Compose)·Dev Container·ccache·컨테이너 디버깅까지 확장하는 방법을 다룹니다.
들어가며: “우리 환경이랑 똑같이 돌려보세요”
개발·CI·배포를 한 이미지로
40-1, 40-2에서 의존성 관리와 CI를 다뤘다면, 컨테이너(애플리케이션과 실행 환경을 함께 묶어 격리된 공간에서 실행하는 기술)는 그 환경을 이미지(컨테이너를 만들 때 쓰는 읽기 전용 템플릿)로 고정해 “어디서나 같은 환경”을 보장합니다.
Node·웹 스택과 패턴을 맞추려면 Docker Compose(프로덕션)·minikube·Nginx 리버스 프록시·Node.js GitHub Actions CI/CD·Node.js 배포를 함께 보고, 테스트 고정은 C++ Google Test로 이어지면 빌드→테스트→이미지 흐름이 한 줄로 이해됩니다. 컨테이너 안에서 띄운 DB·캐시에 앱을 붙이려면 Node.js DB 연동·C++ DB 연동·PostgreSQL vs MySQL·Redis 캐싱을, 호스트 용량은 Linux 디스크/inode와 함께 점검하세요. Docker(가장 널리 쓰이는 컨테이너 플랫폼)로 빌드 전용 이미지(컴파일러, CMake, vcpkg/Conan)와 실행 전용 이미지(런타임만 포함한 최소 이미지)를 나누면, 개발·CI·배포가 동일한 도구 체인을 사용하게 됩니다. 멀티 스테이지 빌드(한 Dockerfile 안에서 빌드용 단계와 실행용 단계를 나누어, 최종 이미지에는 실행에 필요한 것만 넣는 방식)로 한 Dockerfile 안에서 “빌드 스테이지”에서 컴파일하고 “런타임 스테이지”에는 바이너리와 필요한 라이브러리만 복사하면, 이미지 크기가 작아지고 보안도 좋아집니다.
이 글에서 다루는 것:
- 문제 시나리오: “로컬에서는 되는데”, “팀원마다 환경이 달라요”
- 빌드용 Docker 이미지: 베이스 이미지 선택·의존성 설치·캐시 레이어
- 멀티 스테이지 빌드: 빌드 스테이지 → 런타임 스테이지 복사
- 배포 이미지 최적화: 경량 베이스·불필요 패키지 제거
- 자주 발생하는 에러와 해결법
- 최적화 팁과 프로덕션 패턴
- 로컬 개발: 볼륨 마운트, docker compose, Dev Container
- 빌드 가속: Ninja, ccache + BuildKit 캐시 마운트
- 컨테이너 안에서 GDB로 디버깅할 때의 주의점
목차
- 문제 시나리오: 환경 불일치 지옥
- 빌드 환경 이미지
- 멀티 스테이지 빌드
- 배포 이미지 최적화
- 자주 발생하는 에러와 해결법
- 최적화 팁
- 프로덕션 패턴
- 로컬 개발·Compose·Dev Container
- 완전한 예제 프로젝트
- 정리
개념을 잡는 비유
빌드·검사·배포 파이프라인은 공장 검수 라인과 비슷합니다. 같은 입력이면 같은 산출물이 나오게 고정하고, Sanitizer·정적 분석은 출하 전 불량 검사 역할을 합니다.
1. 문제 시나리오: 환경 불일치 지옥
실제 겪는 상황
"로컬에서는 빌드되는데 CI에서만 실패해요."
"팀원 A는 Ubuntu 22.04, B는 24.04인데 결과가 달라요."
"배포 서버의 glibc 버전이 달라서 실행이 안 돼요."
"이미지가 2GB인데 빌드 도구까지 다 들어있어요."
"Alpine에서 빌드했는데 glibc 링크 에러가 나요."
원인 분석
- 컴파일러·라이브러리 버전 차이: 개발자마다 다른 gcc/clang, glibc 버전
- 플랫폼 차이: macOS에서 빌드한 바이너리를 Linux 서버에서 실행 불가
- 의존성 누락: 로컬에만 설치된 헤더·라이브러리에 의존
- 이미지 비대화: 빌드 도구·소스 코드까지 배포 이미지에 포함
해결 방향: Docker로 환경 고정
flowchart LR
subgraph before["Docker 없음 (Before)"]
B1["개발자 A / Ubuntu 22.04"] --> B2[다른 바이너리]
B3["개발자 B / Ubuntu 24.04"] --> B4[다른 바이너리]
B5["CI / 다른 환경"] --> B6[빌드 실패 가능]
end
subgraph after["Docker (After)"]
A1[동일한 베이스 이미지] --> A2[동일한 빌드]
A2 --> A3[개발자 A]
A2 --> A4[개발자 B]
A2 --> A5[CI]
end
Docker 이미지로 컴파일러·CMake·의존성을 고정하면, 누가 어디서 빌드해도 동일한 결과를 얻습니다.
추가 문제 시나리오
시나리오 5: 새 팀원 온보딩
"저도 빌드해보고 싶은데, gcc랑 CMake를 어떤 버전으로 설치해야 해요?"
"제가 설치한 vcpkg 경로가 다른데, 왜 제 환경에서는 빌드가 안 되죠?"
수동 환경에서는 새 팀원이 프로젝트를 빌드하는 데 몇 시간이 걸릴 수 있습니다. Docker + docker build 한 줄이면 동일한 환경에서 빌드됩니다.
시나리오 6: 멀티 플랫폼 배포
"로컬은 M1 Mac인데, 배포 서버는 x86_64 Linux예요."
"ARM 서버와 x86 서버 둘 다 지원해야 해요."
Docker Buildx로 --platform linux/amd64,linux/arm64를 지정하면 한 번에 멀티 아키텍처 이미지를 빌드할 수 있습니다.
시나리오 7: 보안 스캔 실패
"이미지에 CVE 취약점이 있다고 나와요."
"빌드 도구까지 들어가 있어서 공격면이 넓대요."
멀티 스테이지 빌드로 런타임 이미지에는 바이너리만 넣으면, 컴파일러·쉘·패키지 매니저가 없어 공격면이 최소화됩니다. distroless 베이스는 셸 자체가 없습니다.
2. 빌드 환경 이미지
컴파일러·CMake·의존성 한 번에
- 베이스: ubuntu:22.04, debian-slim 등에서 gcc, clang, cmake, git 등을 설치. 버전을 명시하면 재현 가능합니다.
- vcpkg/Conan을 이미지에 넣을 때는, 캐시 레이어를 활용해 의존성 목록(vcpkg.json, conanfile.txt)이 바뀌지 않으면 해당 레이어를 재사용하도록 Dockerfile을 작성합니다. COPY 순서를 “의존성 파일 먼저 → install → 소스 복사 → 빌드”로 두면 소스만 바뀌었을 때 의존성 단계는 캐시됩니다.
- CI에서는 이 이미지를 container: 로 사용하거나, docker run으로 빌드만 수행하고 아티팩트만 내보내도 됩니다.
최소 C++ 예제 (컨테이너 안에서 빌드)
// main.cpp - Docker 빌드 스테이지에서 컴파일됩니다.
#include <iostream>
int main() {
std::cout << "Docker 빌드 스테이지에서 컴파일됩니다.\n";
return 0;
}
기본 Dockerfile (캐시 레이어 활용)
COPY vcpkg.json . 만 먼저 하고 그 다음 RUN 에서 vcpkg install 을 하면, vcpkg.json 이 바뀌지 않는 한 이 레이어가 캐시되어 재빌드가 빨라집니다. rm -rf /var/lib/apt/lists/ 로 apt 캐시를 지우면 이미지 크기가 줄어듭니다.
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 (캐시됨)
COPY . .
RUN cmake -B build -DCMAKE_TOOLCHAIN_FILE=... && cmake --build build
주의사항: CMAKE_TOOLCHAIN_FILE 경로는 vcpkg 부트스트랩 위치와 맞춰야 하고, 멀티 스테이지에서는 빌드 스테이지에만 vcpkg를 두는 편이 일반적입니다.
vcpkg 연동 완전 예제
프로젝트 구조:
my-cpp-app/
├── CMakeLists.txt
├── vcpkg.json
├── Dockerfile
└── src/
└── main.cpp
vcpkg.json:
{
"dependencies": ["spdlog"]
}
CMakeLists.txt:
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 모드):
# 빌드 스테이지
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 설치 (캐시 레이어)
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 install 캐시
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
Conan 연동 예제
Conan을 사용하는 프로젝트의 Dockerfile 예시입니다.
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
CI에서 Docker 빌드 사용
GitHub Actions에서 Docker로 빌드하고 아티팩트를 추출하는 예시:
- 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
또는 container: job으로 동일한 이미지에서 빌드:
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. 멀티 스테이지 빌드
빌드 스테이지와 런타임 스테이지 분리
- 멀티 스테이지: 하나의 Dockerfile에 FROM을 여러 번 두고, AS로 스테이지에 이름을 붙입니다. 첫 번째 스테이지에서 컴파일하고, 두 번째 스테이지에서는 경량 베이스(alpine, distroless 등)만 두고 COPY —from=build_stage로 바이너리와 필요한 .so만 가져옵니다.
- 이렇게 하면 최종 이미지에는 컴파일러·헤더·빌드 도구가 들어가지 않아 크기·공격면이 줄어듭니다.
flowchart TB
subgraph stage1["스테이지 1: builder"]
S1[ubuntu:22.04] --> S2[g++, cmake 설치]
S2 --> S3[소스 복사]
S3 --> S4[빌드]
S4 --> S5[myapp 바이너리]
end
subgraph stage2["스테이지 2: runtime"]
R1[debian:bookworm-slim] --> R2[libstdc++6만 설치]
R2 --> R3[COPY --from=builder]
R3 --> R4[최종 이미지 ~50MB]
end
S5 -.->|COPY --from| R3
기본 멀티 스테이지 Dockerfile
builder 스테이지에서만 g++·cmake로 빌드하고, debian:bookworm-slim 스테이지에는 COPY —from=builder … 로 빌드된 myapp 바이너리만 가져옵니다. 런타임에 libstdc++6 만 설치하면 동적 링크된 C++ 런타임이 있어 실행 가능합니다.
# 스테이지 1: 빌드
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
# 스테이지 2: 런타임
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"]
동적 링크 vs 정적 링크
- 동적 링크: 런타임 스테이지에 libstdc++6 등 필요한 .so를 설치. 이미지 크기와 유지보수에 유리.
- 정적 링크:
-static-libstdc++등으로 링크하면 라이브러리 설치를 줄일 수 있으나, 라이선스·ABI 정책을 확인해야 합니다.
# 정적 링크 예시 (CMake)
# CMakeLists.txt에 추가:
# set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static-libstdc++")
vcpkg + 멀티 스테이지 완전 예제
# ========== 스테이지 1: 빌드 ==========
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
# ========== 스테이지 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/*
COPY --from=builder /src/build/myapp /usr/local/bin/
USER nobody
CMD ["myapp"]
4. 배포 이미지 최적화
경량 베이스와 레이어 정리
- 베이스: alpine은 작지만 glibc가 아니라 musl이라 C++ 바이너리가 호환되지 않을 수 있으므로, debian-slim, ubuntu minimal, 또는 distroless가 무난합니다.
- 불필요 제거: apt-get install 시 —no-install-recommends, 설치 후 rm -rf /var/lib/apt/lists/ 로 캐시를 지우면 레이어 크기가 줄어듭니다.
- 한 레이어에 정리: RUN을 하나로 묶어서 레이어 수를 줄이면 이미지가 조금 더 작아질 수 있습니다.
베이스 이미지 비교
| 베이스 | 크기 | glibc | C++ 호환 | 비고 |
|---|---|---|---|---|
| ubuntu:22.04 | ~77MB | ✅ | ✅ | 무난 |
| debian:bookworm-slim | ~80MB | ✅ | ✅ | 권장 |
| alpine:3.19 | ~7MB | ❌ (musl) | ⚠️ | glibc 바이너리 불가 |
| gcr.io/distroless/cc-debian12 | ~50MB | ✅ | ✅ | 셸 없음, 보안 강화 |
distroless 사용 예시
FROM debian:bookworm-slim AS builder
# ... 빌드 ...
FROM gcr.io/distroless/cc-debian12
COPY --from=builder /usr/local/bin/myapp /app/myapp
ENTRYPOINT ["/app/myapp"]
distroless는 셸·패키지 매니저가 없어 공격면이 최소화됩니다. 디버깅 시 docker run --entrypoint 로 바꿔야 합니다.
5. 자주 발생하는 에러와 해결법
에러 1: “cannot find -lstdc++” / “undefined reference”
원인: 런타임 스테이지에 libstdc++가 없음.
해결법:
RUN apt-get update && apt-get install -y --no-install-recommends \
libstdc++6 libgcc-s1 \
&& rm -rf /var/lib/apt/lists/*
에러 2: “error while loading shared libraries: libxxx.so.X”
원인: 동적 링크된 라이브러리가 런타임 이미지에 없음.
해결법:
# 빌드 스테이지에서 바이너리 의존성 확인
ldd build/myapp
누락된 .so를 런타임 스테이지에 설치하거나, COPY --from=builder 로 복사합니다.
COPY --from=builder /usr/lib/x86_64-linux-gnu/libfoo.so.1 /usr/lib/x86_64-linux-gnu/
에러 3: Alpine에서 “FATAL: kernel too old”
원인: Alpine은 musl libc를 사용. glibc로 빌드한 바이너리는 Alpine에서 실행 불가.
해결법: C++ 배포 시 debian-slim 또는 distroless 사용. Alpine을 쓰려면 Alpine 환경에서 빌드해야 합니다.
# Alpine에서 빌드하려면
FROM alpine:3.19 AS builder
RUN apk add --no-cache g++ cmake make
# ...
에러 4: “no such file or directory” (바이너리 실행 시)
원인: 바이너리가 동적 링크된 라이브러리를 찾지 못함. 또는 32/64비트 불일치.
해결법:
# 파일 형식 확인
file build/myapp
# 의존성 확인
ldd build/myapp
에러 5: vcpkg “CMAKE_TOOLCHAIN_FILE” not found
원인: vcpkg 경로가 잘못되었거나, COPY 순서로 vcpkg가 아직 없음.
해결법:
# vcpkg를 먼저 설치한 뒤 COPY
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
# 그 다음 소스 복사
COPY . .
RUN cmake -B build -DCMAKE_TOOLCHAIN_FILE=${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake
에러 6: Docker 빌드 캐시가 너무 오래됨
원인: 의존성 변경 후에도 이전 레이어가 캐시됨.
해결법:
# 캐시 없이 빌드
docker build --no-cache -t myapp .
일반적으로는 COPY 순서를 “의존성 파일 → install → 소스”로 두어, 소스만 바뀌면 마지막 레이어만 재빌드되도록 합니다.
에러 7: “exec user process caused: exec format error”
원인: 아키텍처 불일치 (예: ARM Mac에서 x86_64 이미지 실행).
해결법:
# 플랫폼 명시
docker build --platform linux/amd64 -t myapp .
에러 8: “vcpkg install” 시간이 너무 오래 걸림
원인: 매 빌드마다 vcpkg가 패키지를 처음부터 빌드함.
해결법: BuildKit 캐시 마운트 사용:
# syntax=docker/dockerfile:1.4
RUN --mount=type=cache,target=/root/.cache/vcpkg \
${VCPKG_ROOT}/vcpkg install
또는 builder 이미지를 미리 빌드해 두고 베이스로 사용:
FROM my-registry/cpp-builder:22.04 AS builder
# vcpkg가 이미 설치·캐시된 상태
에러 9: “Permission denied” (바이너리 실행 시)
원인: USER nobody 등으로 전환했을 때 바이너리에 실행 권한이 없음.
해결법:
COPY --from=builder /src/build/myapp /usr/local/bin/
RUN chmod +x /usr/local/bin/myapp
에러 10: “failed to solve: process did not complete successfully”
원인: RUN 단계에서 명령이 실패 (예: apt 패키지 없음, 네트워크 오류).
해결법: 단계별로 빌드해 실패 지점 확인:
docker build --progress=plain --no-cache -t myapp . 2>&1 | tee build.log
--progress=plain으로 출력을 자세히 확인합니다.
6. 최적화 팁
1. 레이어 캐시 활용
# ❌ 나쁜 예: 소스 변경 시 의존성까지 재설치
COPY . .
RUN apt-get install ... && vcpkg install && cmake --build build
# ✅ 좋은 예: 의존성 → 소스 순서
COPY vcpkg.json .
RUN vcpkg install
COPY . .
RUN cmake --build build
2. .dockerignore 사용
# .dockerignore
.git
.gitignore
build/
.vscode/
*.md
Dockerfile
.dockerignore
불필요한 파일을 제외하면 빌드 컨텍스트가 줄어들고 캐시 효율이 좋아집니다.
3. 멀티 아키텍처 빌드
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest --push .
4. BuildKit 활용
# syntax=docker/dockerfile:1.4
# BuildKit에서 캐시 마운트 사용
RUN --mount=type=cache,target=/root/.cache/vcpkg \
vcpkg install
DOCKER_BUILDKIT=1 환경 변수로 BuildKit을 활성화하면 캐시 마운트 등 고급 기능을 쓸 수 있습니다.
5. 이미지 크기 비교
| 구성 | 예상 크기 |
|---|---|
| ubuntu + 빌드 도구 + 소스 | 1.5~2GB |
| debian-slim + 바이너리만 | 80~150MB |
| distroless + 바이너리만 | 50~80MB |
6. 빌드 시간 단축
# 병렬 빌드: cmake --build에 -j 옵션
RUN cmake --build build -j$(nproc)
7. 레이어 수 최소화
# ❌ 나쁜 예: RUN이 여러 개면 레이어 증가
RUN apt-get update
RUN apt-get install -y g++
RUN rm -rf /var/lib/apt/lists/*
# ✅ 좋은 예: 하나로 묶기
RUN apt-get update && apt-get install -y --no-install-recommends g++ \
&& rm -rf /var/lib/apt/lists/*
8. 빌드 아규먼트로 유연성 확보
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. 프로덕션 패턴
패턴 1: 버전 태깅
docker build -t myapp:1.2.3 -t myapp:latest .
시맨틱 버전으로 태그를 붙여 롤백과 추적을 쉽게 합니다.
패턴 2: non-root 사용자
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"]
패턴 3: 헬스체크
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD /usr/local/bin/myapp --health || exit 1
애플리케이션에 --health 옵션을 구현해 컨테이너 상태를 확인합니다.
패턴 4: 시크릿·환경 변수
# 빌드 시 ARG, 실행 시 ENV
docker run -e DATABASE_URL=... myapp
시크릿은 Docker Secrets 또는 Kubernetes Secrets로 주입합니다.
패턴 5: CI/CD 파이프라인 연동
# GitHub Actions 예시
- name: Build Docker image
run: docker build -t myapp:${{ github.sha }} .
- name: Push to registry
run: docker push myregistry/myapp:${{ github.sha }}
패턴 6: 스테이지별 이미지 태깅
# builder 스테이지만 별도 이미지로 푸시 (CI 캐시용)
FROM ubuntu:22.04 AS builder
# ...
CI에서 builder 이미지를 미리 빌드해 두고, 소스만 변경 시 builder를 재사용할 수 있습니다.
패턴 7: 로깅·모니터링
표준 출력으로 로그를 내보내면 Docker/Kubernetes가 수집하기 쉽습니다:
// C++ 앱에서 구조화된 로그
spdlog::info("Request received, id={}", request_id);
spdlog::error("Connection failed: {}", err.message());
JSON 포맷으로 출력하면 ELK, Loki 등에 연동하기 좋습니다:
spdlog::set_pattern(R"({"time":"%Y-%m-%d %H:%M:%S","level":"%l","msg":"%v"})");
패턴 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) {
// 작업 수행
}
return 0;
}
docker stop 또는 kubectl delete pod 시 SIGTERM이 전달됩니다.
패턴 9: 리소스 제한
# 메모리·CPU 제한
docker run -m 512m --cpus=0.5 myapp
Kubernetes에서는 resources.limits로 제한합니다.
빌드 파이프라인 시퀀스
sequenceDiagram
participant Dev as 개발자
participant Docker as Docker
participant Registry as 레지스트리
participant K8s as Kubernetes
Dev->>Docker: docker build
Docker->>Docker: builder 스테이지 (빌드)
Docker->>Docker: runtime 스테이지 (복사)
Docker->>Docker: 이미지 생성
Dev->>Registry: docker push
Registry->>K8s: 배포
K8s->>K8s: Pod 재시작 (새 이미지)
8. 로컬 개발·Compose·Dev Container
CI용 docker build만으로는 편집 → 빌드 → 테스트 사이클이 느릴 수 있다. 소스는 호스트에 두고, 툴체인만 컨테이너로 고정하는 패턴을 쓰면 팀 표준과 로컬 생산성을 같이 잡을 수 있다.
8.1 볼륨 마운트로 “호스트 소스 + 컨테이너 빌드”
이미 빌드해 둔 개발용 이미지(컴파일러·CMake·vcpkg 설치됨)가 있다고 가정한다.
# 프로젝트 루트에서: 소스는 호스트, 빌드 산출물은 컨테이너 안 또는 볼륨
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": 편집기는 호스트 VS Code/Cursor, 빌드는 컨테이너에서 실행.-u $(id -u):$(id -g)(Linux): 생성되는build/파일 소유권을 호스트 사용자에 맞추면 권한 꼬임을 줄일 수 있다.- Windows/macOS Docker Desktop에서는 UID 매핑이 다를 수 있어, 팀 정책에 맞게 조정한다.
8.2 docker compose로 개발·실행 명령 고정
compose.yaml에 빌드 이미지와 같은 환경에서 쉘/테스트를 열어두면 신입 온보딩이 쉬워진다.
# compose.yaml (예시)
services:
dev:
build:
context: .
dockerfile: Dockerfile
target: dev # 아래 Dockerfile 예시의 dev 스테이지
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"
멀티 스테이지 Dockerfile에 dev 타깃 추가 예시:
# ... 기존 builder와 동일한 패키지 + 디버깅 도구
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
# vcpkg 등은 팀 표준에 맞게 추가
target: dev로 배포 이미지(runtime) 와 개발 이미지를 한 Dockerfile에서 나눈다.
8.3 Dev Container (VS Code / Cursor)
저장소에 .devcontainer/devcontainer.json을 두면 “저장소 열면 곧바로 동일 컨테이너”로 들어간다. 개발 환경 완벽 가이드에서 언급한 clangd + CMake Tools와도 잘 맞는다.
{
"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"
}
}
}
}
devcontainer.json을.devcontainer/아래에 두는 경우, 루트의Dockerfile을 가리키려면"dockerfile": "../Dockerfile"와"context": ".."조합이 흔하다. (Dockerfile이.devcontainer/안에 있으면"dockerfile": "Dockerfile"만으로 된다.)- 빌드 스테이지만 있고
dev가 없다면Dockerfile에AS dev스테이지를 추가하거나,target없이 빌드하도록 조정한다. - 원격 컨테이너 안에서 CMake preset·clangd가
compile_commands.json을 보도록CMAKE_EXPORT_COMPILE_COMMANDS=ON을 켜 두면 편하다.
8.4 Ninja + ccache로 Docker 빌드 시간 줄이기
대형 프로젝트에서는 Ninja가 Make보다 빌드 그래프 처리에 유리한 경우가 많다. ccache는 BuildKit 캐시 마운트와 함께 쓰면 레이어가 바뀌어도 컴파일 결과를 재사용하기 쉽다.
# 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)
로컬 개발에서 -v my_ccache:/root/.cache/ccache처럼 이름 붙은 볼륨을 붙이면, 컨테이너를 지워도 캐시가 남는다.
8.5 컨테이너 안에서 GDB로 디버깅
기본 보안 프로필에서는 ptrace 제한으로 GDB가 제대로 동작하지 않을 수 있다.
docker run --rm -it \
--cap-add=SYS_PTRACE \
-v "$(pwd):/src" -w /src \
my-cpp-dev:22.04 \
gdb --args ./build/myapp
일부 환경에서는 --security-opt seccomp=unconfined가 필요할 수 있다. 보안을 느슨하게 하므로 로컬 디버깅 한정으로만 쓴다.
distroless 런타임 이미지에는 셸·GDB가 없으므로, 디버깅은 dev 이미지에서 하고, 배포는 멀티 스테이지의 runtime만 푸시하는 흐름이 안전하다.
8.6 Sanitizer·Debug 빌드 전용 스테이지 (선택)
CI에서 ASan/UBSan job을 Docker로 돌릴 때, Release 런타임 이미지와 분리된 스테이지를 둔다. (아래 builder는 앞선 Dockerfile에서 소스·의존성까지 복사해 둔 스테이지 이름으로 바꾼다.)
FROM builder AS sanitize
ENV CXXFLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -g"
ENV LDFLAGS="-fsanitize=address,undefined"
# vcpkg/Conan 등을 쓰는 builder라면 여기서도 -DCMAKE_TOOLCHAIN_FILE=... 를 builder와 동일하게 넘긴다.
RUN cmake -B build-san -DCMAKE_BUILD_TYPE=Debug -G Ninja \
&& cmake --build build-san
# 이어서: ctest 또는 커스텀 테스트 스크립트
운영 배포 이미지에는 Sanitizer를 넣지 않는 것이 일반적이다. (자세한 플래그는 개발 환경 가이드의 Sanitizer 절 참고.)
9. 완전한 예제 프로젝트
프로젝트 구조
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 앱이 시작됩니다.");
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 (완전판)
# syntax=docker/dockerfile:1.4
# ========== 빌드 스테이지 ==========
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
# ========== 런타임 스테이지 ==========
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
빌드 및 실행
# 빌드
docker build -t cpp-docker-demo:latest .
# 실행
docker run --rm cpp-docker-demo:latest
배포 체크리스트
구현 전 확인할 항목:
- 베이스 이미지 버전 고정 (ubuntu:22.04 등)
- vcpkg.json / conanfile.txt로 의존성 선언
- COPY 순서: 의존성 파일 → install → 소스 → 빌드
- 멀티 스테이지로 빌드 도구 제외
- 런타임 스테이지에 libstdc++6 등 필수 .so 설치
- .dockerignore로 불필요 파일 제외
- non-root 사용자로 실행
- 이미지 태그에 버전 포함 (예: myapp:1.2.3)
- Alpine 대신 debian-slim (glibc 호환)
-
--no-install-recommends사용
디버깅 팁
# 빌드 스테이지에서 쉘로 들어가기
docker run -it --rm --entrypoint /bin/bash myapp:build
# 런타임 이미지에 임시로 쉘 추가 (distroless는 불가)
docker run -it --rm --entrypoint /bin/bash debian:bookworm-slim
# 그 다음 컨테이너 안에서 바이너리 수동 실행
# 바이너리 의존성 확인
docker run --rm --entrypoint /usr/bin/ldd myapp:latest /usr/local/bin/myapp
10. 정리
| 항목 | 요약 |
|---|---|
| 빌드 이미지 | 컴파일러·CMake·의존성 고정 — 개발·CI 동일 환경 |
| 멀티 스테이지 | 빌드 스테이지에서 컴파일 → 런타임 스테이지에 바이너리만 복사 |
| 배포 최적화 | 경량 베이스(debian-slim, distroless)·불필요 패키지 제거·레이어 최소화 |
| 캐시 활용 | COPY 순서: 의존성 파일 → install → 소스 → 빌드 |
| 로컬·온보딩 | 볼륨 마운트, compose, dev 타깃, Dev Container로 편집–빌드 통일 |
| 빌드 가속 | Ninja, ccache + BuildKit 캐시 마운트 |
| Alpine 주의 | musl libc — glibc 바이너리와 호환 안 됨 |
| 프로덕션 | non-root 사용자, 버전 태깅, 헬스체크 |
40번 시리즈는 패키지 관리(vcpkg/Conan) → CI/CD(GitHub Actions) → 컨테이너(Docker)로 “빌드와 배포의 현대화”를 마쳤습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 패키지 관리 실무: vcpkg와 Conan으로 외부 라이브러리 의존성 지옥 탈출 [#40-1]
- C++ 개발 환경 완벽 가이드 (OS·하드웨어·clangd·Sanitizer·CI 정렬)
- C++ CI/CD 파이프라인: GitHub Actions를 이용한 멀티 OS 자동 빌드·테스트 가이드
실전 체크리스트
실무에서 이 개념을 적용할 때 확인해야 할 사항입니다.
코드 작성 전
- 이 기법이 현재 문제를 해결하는 최선의 방법인가?
- 팀원들이 이 코드를 이해하고 유지보수할 수 있는가?
- 성능 요구사항을 만족하는가?
코드 작성 중
- 컴파일러 경고를 모두 해결했는가?
- 엣지 케이스를 고려했는가?
- 에러 처리가 적절한가?
코드 리뷰 시
- 코드의 의도가 명확한가?
- 테스트 케이스가 충분한가?
- 문서화가 되어 있는가?
이 체크리스트를 활용하여 실수를 줄이고 코드 품질을 높이세요.
이 글에서 다루는 키워드 (관련 검색어)
Docker C++, 컨테이너 빌드, 멀티 스테이지 빌드, C++ Docker 이미지 최적화 등으로 검색하시면 이 글이 도움이 됩니다.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Docker로 컴파일러·의존성이 고정된 빌드 환경을 만들고, 배포용 이미지를 멀티 스테이지·최소 베이스로 최적화하는 방법을 다룹니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. Alpine을 C++에 쓰면 안 되나요?
A. Alpine은 musl libc를 사용합니다. glibc로 빌드한 C++ 바이너리는 Alpine에서 실행되지 않습니다. Alpine을 쓰려면 Alpine 환경에서 빌드해야 하며, 일부 라이브러리는 musl에서 호환 문제가 있을 수 있습니다. debian-slim이 무난합니다.
Q. 이미지 크기를 더 줄이려면?
A. (1) distroless 베이스 사용, (2) 정적 링크(-static-libstdc++) 검토, (3) 불필요한 의존성 제거, (4) --no-install-recommends 사용.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. Dev Container와 그냥 Docker 빌드 차이는?
A. docker build는 이미지·배포 산출물에 초점이 맞고, Dev Container는 에디터가 붙은 개발 셸을 저장소와 함께 고정하는 데 가깝다. 둘 다 같은 Dockerfile을 공유할 수 있다.
Q. 더 깊이 공부하려면?
A. Docker 공식 문서, vcpkg 문서, Dev Containers 사양, cppreference를 참고하세요.
한 줄 요약: Docker·멀티스테이지 빌드로 빌드 환경을 통일하고 배포 이미지를 줄일 수 있습니다. 다음으로 Clang-Tidy·Cppcheck(#41-1)를 읽어보면 좋습니다.
이전 글: DevOps for C++ #40-2: CI/CD GitHub Actions
다음 글: [안정성 확보 #41-1] 정적 분석 도구 통합: Clang-Tidy와 Cppcheck로 코드 퀄리티 강제하기
관련 글
- C++23 핵심 기능 완벽 가이드 | std::expected·mdspan
- C++ 패키지 관리 실무: vcpkg와 Conan으로 외부 라이브러리 의존성 지옥 탈출 [#40-1]
- C++ CI/CD 파이프라인: GitHub Actions를 이용한 멀티 OS 자동 빌드·테스트 가이드
- C++26 핵심 기능 완벽 가이드 | 리플렉션 ^^· std::execution
- C++ 캐시 효율적인 코드: 데이터 지향 설계 가이드