WarpBuild LogoWarpBuild Docs

C++ with CMake

Best practices for Dockerfile for C++ with CMake

C++ with CMake

This Dockerfile is designed for C++ projects using CMake as the build system. It uses a multi-stage build to create a lightweight runtime image.

# Stage 1: Build environment
FROM ubuntu:24.04 AS builder
 
# Prevent interactive prompts during package installation
ARG DEBIAN_FRONTEND=noninteractive
 
# Install build dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
    cmake \
    ninja-build \
    git \
    ca-certificates \
    ccache \
    && rm -rf /var/lib/apt/lists/*
 
# Set working directory
WORKDIR /src
 
# Copy all source files first (both CMakeLists.txt and source code)
COPY . .
 
# Create build directory
RUN mkdir -p build
 
# Configure CMake with Ninja generator and enable cache
WORKDIR /src/build
RUN --mount=type=cache,target=/root/.ccache \
    cmake .. -G Ninja \
    -DCMAKE_BUILD_TYPE=Release \
    -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
    -DCMAKE_INSTALL_PREFIX=/install
 
# Build and install the application
RUN --mount=type=cache,target=/root/.ccache \
    ninja install && \
    # Create lib directory if it doesn't exist
    mkdir -p /install/lib
 
# Stage 2: Runtime image
FROM alpine:3.19 AS runtime
 
# Install only the required runtime library
RUN apk add --no-cache libstdc++
 
# Create a non-root user
RUN addgroup -S appuser && adduser -S -g appuser appuser
 
# Create application directory
WORKDIR /app
 
# Copy only the built artifacts from the builder stage
COPY --from=builder /install/bin/ /app/bin/
COPY --from=builder /install/lib/ /app/lib/
 
# Set ownership and permissions
RUN chown -R appuser:appuser /app && \
    chmod +x /app/bin/*
 
# Switch to non-root user
USER appuser
 
# Expose port 8080
EXPOSE 8080
 
# Set the entrypoint to your application
ENTRYPOINT ["/app/bin/myapp"]

Key Features

  1. Multi-stage build: Separates build environment from runtime image
  2. Cache optimization: Uses ccache with buildkit cache mounts to speed up rebuilds
  3. Minimal runtime image: Uses Alpine for a small runtime footprint
  4. Security: Runs as a non-root user
  5. Build tool integration: Uses Ninja for faster builds

Customization

  • Adjust the cmake options based on your project requirements
  • Update the ENTRYPOINT to match your application's executable name

🔍 Why these are best practices:

✅ Multi-stage builds

  • Dramatically reduces final image size by separating build and runtime environments.
  • Eliminates build tools and source code from the runtime image.
  • Improves security by minimizing the attack surface.

✅ Compiler caching with ccache

  • Speeds up incremental builds by caching compiled objects.
  • Uses Docker's mount feature to preserve the cache between builds.
  • Significantly improves build times in CI/CD pipelines.

✅ Ninja build system

  • Faster build speed compared to traditional Make.
  • Better parallelism and dependency handling.
  • Improved build performance for large C++ projects.

✅ Minimal runtime dependencies

  • Installs only libraries required to run the application.
  • Reduces image size and potential vulnerabilities.
  • Improves container startup time and resource efficiency.

✅ Security best practices

  • Runs the application as a non-root user.
  • Sets appropriate file permissions.
  • Minimizes installed packages to reduce attack surface.

🚀 Additional Dockerfile best practices you can adopt:

Enable compiler optimizations

Optimize builds for production use:

RUN cmake .. -G Ninja \
    -DCMAKE_BUILD_TYPE=Release \
    -DCMAKE_CXX_FLAGS="-O3 -march=x86-64-v3 -flto" \
    -DCMAKE_INSTALL_PREFIX=/install

Add health checks

Monitor the application health:

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:8080/health || exit 1

Use .dockerignore

Exclude unnecessary files from the Docker build context:

build/
.git/
.github/
.vscode/
.idea/
*.md
docs/
tests/

Static linking for portable binaries

Create fully static binaries to eliminate runtime dependencies:

# In CMake configuration
RUN cmake .. -G Ninja \
    -DCMAKE_BUILD_TYPE=Release \
    -DCMAKE_CXX_FLAGS="-static" \
    -DBUILD_SHARED_LIBS=OFF \
    -DCMAKE_INSTALL_PREFIX=/install
 
# Then use a minimal runtime image
FROM scratch
COPY --from=builder /install/bin/myapp /myapp
ENTRYPOINT ["/myapp"]

Build for multiple architectures

Support various hardware platforms:

# Use buildx with platform-specific arguments
FROM --platform=$BUILDPLATFORM ubuntu:22.04 AS builder
ARG TARGETPLATFORM
ARG BUILDPLATFORM
 
# Set appropriate compiler flags based on target
RUN case "$TARGETPLATFORM" in \
      "linux/amd64") CMAKE_ARCH_FLAGS="-march=x86-64" ;; \
      "linux/arm64") CMAKE_ARCH_FLAGS="-march=armv8-a" ;; \
      *) CMAKE_ARCH_FLAGS="" ;; \
    esac && \
    cmake ... -DCMAKE_CXX_FLAGS="$CMAKE_ARCH_FLAGS"

Configure for different build types

Use build arguments to control build configuration:

ARG BUILD_TYPE=Release
RUN cmake .. -G Ninja \
    -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \
    -DCMAKE_INSTALL_PREFIX=/install

Add runtime configuration

Include configuration files in your image:

# In the runtime stage
COPY --from=builder /src/config/ /app/config/
ENV CONFIG_PATH=/app/config/config.json

By following these practices, you'll create Docker images for your C++ applications that are secure, efficient, and optimized for both development and production environments. These techniques help minimize build times, reduce image sizes, and ensure consistent behavior across different deployment environments, which is particularly important for C++ applications.

Last updated on