C++ Deployment & Best Practices: CMake, Docker, CI/CD, RAII & Modern Guidelines (2025)
⚡ Quick Answer: C++ Deployment & Best Practices
- Build system: CMake — cross-platform, target-based, generates Make/Ninja/VS
- Dependencies: vcpkg or Conan — version-pinned, reproducible
- Containerization: multi-stage Docker — tiny production image, no compiler overhead
- CI/CD: GitHub Actions — build, test, sanitize, deploy on every push
- Linking: static for portability, dynamic for shared libraries
- Code quality: RAII + smart pointers + const correctness + noexcept
- Production flags:
-O2 -DNDEBUG -Wall -Werror
Writing correct C++ code is only half the job. Getting it reliably to production — and keeping it maintainable as the codebase grows — requires a disciplined toolchain. This guide covers the complete modern C++ production workflow: from CMake build files through multi-stage Docker containers, GitHub Actions pipelines, and the code-level practices that separate robust production code from fragile prototypes.
Deploy Steps
Full pipeline overview
CMake
Production CMakeLists.txt
Dependencies
vcpkg & Conan
Docker
Multi-stage build
CI/CD
GitHub Actions
Code Practices
RAII, const, noexcept
20-Point Checklist
Production readiness
FAQ
Common questions
C++ Deployment Pipeline — Full Overview
Code
Write & test locally
CMake Build
cmake -S . -B build
Tests
ctest + ASan/UBSan
Docker
Multi-stage image
CI/CD
GitHub Actions
Deploy
Server / Cloud
Monitor
Logs & metrics
| Aspect | Static linking | Dynamic linking |
|---|---|---|
| Library inclusion | Embedded in binary at link time | Loaded from OS at runtime |
| Binary size | Larger | Smaller |
| Portability | Self-contained — no runtime deps | Requires correct .so/.dll version |
| Security patches | Rebuild required | Update library, no rebuild |
| Best for | Docker, embedded, CLI tools | Large apps with shared libraries |
| Flag | -static or link .a files | Default (link .so / .dll) |
CMake — Production CMakeLists.txt Explained
cmake_minimum_required(VERSION 3.20) # minimum version lock
project(MyApp
VERSION 1.2.0
LANGUAGES CXX
)
# ── C++ standard ───────────────────────────────────────────────
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) # error if 17 not available
set(CMAKE_CXX_EXTENSIONS OFF) # no GNU extensions (-std=c++17 not -std=gnu++17)
# ── Build type defaults ─────────────────────────────────────────
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
endif()
# ── Find dependencies ───────────────────────────────────────────
find_package(Threads REQUIRED) # pthreads
# find_package(Boost 1.80 REQUIRED COMPONENTS system filesystem)
# ── Targets ─────────────────────────────────────────────────────
add_executable(MyApp
src/main.cpp
src/processor.cpp
)
# Target-based includes (PRIVATE = only MyApp, not its dependants)
target_include_directories(MyApp PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
)
target_link_libraries(MyApp PRIVATE
Threads::Threads
)
# ── Compiler warnings (per-target, not global) ──────────────────
target_compile_options(MyApp PRIVATE
$<$<CXX_COMPILER_ID:GNU,Clang>:-Wall;-Wextra;-Wpedantic>
$<$<CONFIG:Debug>:-fsanitize=address,undefined;-g;-O0>
$<$<CONFIG:Release>:-O2;-DNDEBUG>
)
target_link_options(MyApp PRIVATE
$<$<CONFIG:Debug>:-fsanitize=address,undefined>
)
# ── Embed version at compile time ───────────────────────────────
configure_file(
${CMAKE_CURRENT_SOURCE_DIR}/include/version.h.in
${CMAKE_CURRENT_BINARY_DIR}/include/version.h
)
# ── Tests ───────────────────────────────────────────────────────
enable_testing()
add_subdirectory(tests)
# ── Install rules ───────────────────────────────────────────────
install(TARGETS MyApp DESTINATION bin)
install(DIRECTORY include/ DESTINATION include)
# Debug build (with ASan + UBSan)
cmake -S . -B build/debug -DCMAKE_BUILD_TYPE=Debug
cmake --build build/debug --parallel $(nproc)
ctest --test-dir build/debug --output-on-failure
# Release build (optimized, no sanitizers)
cmake -S . -B build/release -DCMAKE_BUILD_TYPE=Release
cmake --build build/release --parallel $(nproc)
# Install to /usr/local
sudo cmake --install build/release --prefix /usr/local
# Cross-compile for ARM (with toolchain file)
cmake -S . -B build/arm -DCMAKE_TOOLCHAIN_FILE=arm-linux.cmake
include_directories()
and add_compile_options() affect every target in the project. Prefer
target_include_directories(MyLib PRIVATE ...) — it gives you precise control
over what propagates. Use PUBLIC only when downstream targets genuinely need
the include path.
Dependency Management: vcpkg vs Conan
# 1. Install vcpkg (one-time)
git clone https://github.com/microsoft/vcpkg
./vcpkg/bootstrap-vcpkg.sh
# 2. Install packages
./vcpkg/vcpkg install boost-filesystem sqlite3 openssl nlohmann-json
# 3. Create vcpkg.json (manifest mode — version-pinned)
{
"name": "myapp",
"version": "1.0.0",
"dependencies": [
"boost-filesystem",
"sqlite3",
{ "name": "openssl", "version>=": "3.0.0" },
"nlohmann-json"
]
}
cmake -S . -B build \
-DCMAKE_TOOLCHAIN_FILE=/path/to/vcpkg/scripts/buildsystems/vcpkg.cmake
# Then in CMakeLists.txt:
# find_package(nlohmann_json REQUIRED)
# target_link_libraries(MyApp PRIVATE nlohmann_json::nlohmann_json)
# conanfile.txt
[requires]
boost/1.83.0
sqlite3/3.43.2
openssl/3.1.3
[generators]
CMakeDeps
CMakeToolchain
# Install dependencies
pip install conan
conan install . --build=missing --output-folder=build
# Build with generated toolchain
cmake -S . -B build \
-DCMAKE_TOOLCHAIN_FILE=build/conan_toolchain.cmake \
-DCMAKE_BUILD_TYPE=Release
cmake --build build
include(FetchContent)
# Google Test
FetchContent_Declare(googletest
URL https://github.com/google/googletest/archive/v1.14.0.tar.gz
URL_HASH SHA256=8ad598c73ad796e0d8280b082cebd82a630d73e73cd3c70057938a6501bba5d7
)
FetchContent_MakeAvailable(googletest)
# nlohmann/json (header-only)
FetchContent_Declare(json
URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz
)
FetchContent_MakeAvailable(json)
# Use in targets
target_link_libraries(MyApp PRIVATE nlohmann_json::nlohmann_json)
target_link_libraries(tests PRIVATE GTest::gtest_main)
URL_HASH to verify integrity. For large projects with many dependencies, prefer vcpkg or Conan — they cache compiled packages across builds.
Docker — Multi-Stage Production Build
# ── Stage 1: Build ──────────────────────────────────────────────
FROM gcc:13 AS builder
WORKDIR /src
# Install CMake
RUN apt-get update && apt-get install -y cmake && rm -rf /var/lib/apt/lists/*
# Copy source (layers cached separately — CMakeLists.txt first)
COPY CMakeLists.txt .
COPY src/ src/
COPY include/ include/
# Configure and build release
RUN cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && \
cmake --build build --parallel $(nproc)
# ── Stage 2: Runtime ────────────────────────────────────────────
FROM ubuntu:22.04
# Only install runtime deps (not the compiler!)
RUN apt-get update && \
apt-get install -y libstdc++6 ca-certificates && \
rm -rf /var/lib/apt/lists/*
# Copy ONLY the compiled binary from the builder stage
COPY --from=builder /src/build/MyApp /usr/local/bin/MyApp
# Non-root user for security
RUN useradd --system --no-create-home appuser
USER appuser
ENTRYPOINT ["/usr/local/bin/MyApp"]
# Build the image
docker build -t myapp:1.2.0 -t myapp:latest .
# Check final image size
docker images myapp
# builder stage: ~1.8 GB | runtime stage: ~180 MB
# Run locally
docker run --rm myapp:1.2.0
# Run with environment variables and port mapping
docker run -d \
--name myapp-prod \
-e APP_PORT=8080 \
-p 8080:8080 \
myapp:1.2.0
# Push to registry (Docker Hub or ghcr.io)
docker push myapp:1.2.0
docker push myapp:latest
GitHub Actions CI/CD Pipeline
name: C++ CI/CD
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
build-and-test:
strategy:
matrix:
os: [ubuntu-22.04, macos-14, windows-2022]
build_type: [Debug, Release]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Install CMake
uses: lukka/get-cmake@latest
- name: Configure
run: |
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=${{ matrix.build_type }}
- name: Build
run: cmake --build build --parallel 4
- name: Run Tests (with sanitizers in Debug)
run: ctest --test-dir build --output-on-failure
- name: Upload binary artifact
if: matrix.build_type == 'Release' && matrix.os == 'ubuntu-22.04'
uses: actions/upload-artifact@v4
with:
name: MyApp-linux
path: build/MyApp
docker-build:
needs: build-and-test
runs-on: ubuntu-22.04
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
push: true
tags: ghcr.io/${{ github.repository }}:latest
Code Best Practices: RAII, const, noexcept & Error Handling
#include <iostream>
#include <memory>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
using namespace std;
namespace MyApp {
// RAII wrapper — resource tied to object lifetime
class DataProcessor {
unique_ptr<int[]> buffer; // RAII: freed in destructor automatically
int size;
public:
// Validate in constructor — throw on invalid state
explicit DataProcessor(int n)
: size(n), buffer(make_unique<int[]>(n))
{
if (n <= 0)
throw invalid_argument("Size must be positive");
}
// const + noexcept: read-only, optimizer-friendly
int getSize() const noexcept { return size; }
// string_view: zero-copy, no allocation
void process(string_view name) const {
cout << "Processing " << name << ": size=" << size << "\n";
}
// std::optional — no null pointers, no sentinel values
optional<int> findFirst(int target) const noexcept {
for (int i = 0; i < size; i++)
if (buffer[i] == target) return i;
return nullopt;
}
};
// Guard clause pattern — flat, readable
void run(string_view input) {
if (input.empty()) { cerr << "Error: empty input\n"; return; }
if (input.size() > 100) { cerr << "Error: input too long\n"; return; }
try {
DataProcessor p(42); // RAII: auto-cleanup
p.process(input);
auto idx = p.findFirst(7);
if (idx)
cout << "Found at index: " << *idx << "\n";
else
cout << "Not found\n";
}
catch (const exception& e) {
cerr << "Exception: " << e.what() << "\n";
}
}
} // namespace MyApp
int main(int argc, char* argv[]) {
if (argc != 2) {
cerr << "Usage: " << argv[0] << " <name>\n";
return 1;
}
MyApp::run(argv[1]);
return 0;
}
Processing MyProject: size=42
Not found20-Point Production Readiness Checklist
CMake with target_* commands
No global include_directories or add_compile_options.
C++ standard pinned
CMAKE_CXX_STANDARD_REQUIRED ON, EXTENSIONS OFF.
-Wall -Wextra -Werror in CI
All warnings are errors in CI — no silent issues.
Debug builds with -fsanitize=address,undefined
Catches memory errors and UB before release.
Release builds with -O2 -DNDEBUG
Production binaries are optimized, asserts disabled.
No raw new/delete
Use unique_ptr, shared_ptr, vector, string.
RAII for all resources
Files, sockets, mutexes — all wrapped in RAII types.
const everywhere it can be
Variables, parameters, member functions.
noexcept on move ops and destructors
Enables vector move optimization and correct semantics.
Dependencies version-pinned
vcpkg.json or conanfile.txt with exact versions.
Multi-stage Docker build
Runtime image contains only binary, no compiler.
Non-root Docker user
useradd + USER appuser — principle of least privilege.
CI matrix: Linux + Windows + macOS
Catches platform-specific issues before merge.
Tests with Google Test / Catch2
ctest integrated; tests run in CI on every push.
Git version tags embedded in binary
./app --version prints commit or tag.
Debug symbols separated
strip + objcopy for smaller production binary.
Error handling at boundaries
Exceptions or optional — never silent failure.
Input validation
Validate at system boundaries; reject invalid data early.
Logging to stderr / log file
std::clog or a logging library — no silent crashes.
README with build & deploy instructions
Any new developer can build in <5 minutes from README.
FAQ / Interview Questions
std::vector<int> data; gets a correct deep-copy constructor, correct copy assignment, correct move constructor, correct move assignment, and correct destructor — all for free. Compare this to managing int* data; which requires manually defining all five correctly. Prefer the Rule of Zero..github/workflows/build.yml. Key steps: (1) actions/checkout@v4 to get the code. (2) Install CMake with lukka/get-cmake@latest. (3) Configure: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release. (4) Build: cmake --build build --parallel 4. (5) Test: ctest --test-dir build. Use a matrix to test on ubuntu-22.04, macos-14, and windows-2022 simultaneously. Run Debug builds with sanitizers and Release builds for artifacts. Gate Docker publish behind github.ref == 'refs/heads/main'.std::optional<T> when: the absence of a value is a normal, expected outcome (searching for an element that might not exist, optional configuration settings). Use exceptions when: a failure is unexpected and represents an error the caller should handle at a higher level (file not found, invalid argument, network failure). Rules: never use exceptions for normal control flow (e.g., "item not found" is not exceptional). Never silently swallow exceptions — always catch and log or re-throw. Use noexcept on functions that genuinely cannot throw.Ship production-ready C++ — from code to deployment
Structured lessons, 200+ exercises, completion certificate. Join 50,000+ students.