Контейнеризация C++ приложений с Docker: полное руководство

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: зачем контейнеризировать C++ приложения?

Разработка на C++ традиционно ассоциируется с проблемой «это работает на моей машине». Разные версии компиляторов, библиотек и системных зависимостей превращают развертывание в головную боль. Docker решает эту проблему: вы упаковываете приложение со всем окружением в легковесный контейнер, который запускается одинаково на любой системе.

В этой статье мы разберем полный процесс контейнеризации C++ приложения — от простого Dockerfile до многоступенчатой сборки с оптимизацией размера образа.

1. Основы: Dockerfile для C++ приложения

Начнем с простейшего примера. Допустим, у нас есть программа на C++, которая выводит «Hello, Docker!».

Исходный код (hello.cpp)

#include <iostream>

int main() {
    std::cout << "Hello, Docker!" << std::endl;
    return 0;
}

Первый Dockerfile

# Используем образ с GCC
FROM gcc:13.2

# Устанавливаем рабочую директорию
WORKDIR /app

# Копируем исходники
COPY hello.cpp .

# Компилируем
RUN g++ -o hello hello.cpp

# Запускаем
CMD ["./hello"]

Соберем и запустим:

docker build -t cpp-hello .
docker run cpp-hello

Минус такого подхода — образ весит около 1.2 ГБ из-за полного набора инструментов GCC. Для продакшена это неприемлемо.

2. Многоступенчатая сборка (Multi-stage build)

Лучшая практика — разделить сборку и выполнение. На первом этапе мы компилируем приложение с полным инструментарием, на втором — копируем только готовый бинарник в чистый минимальный образ.

Оптимизированный Dockerfile

# Этап 1: сборка
FROM gcc:13.2 AS builder

WORKDIR /app

COPY hello.cpp .

RUN g++ -static -o hello hello.cpp

# Этап 2: выполнение
FROM alpine:3.19

WORKDIR /app

# Копируем статически скомпилированный бинарник
COPY --from=builder /app/hello .

CMD ["./hello"]

Ключевой момент — флаг -static. Он включает все зависимости в бинарник, позволяя запускать его на чистом Alpine без дополнительных библиотек. Размер образа уменьшается с 1.2 ГБ до ~6 МБ!

3. Реальный проект: CMake + зависимости

Рассмотрим более сложный случай: приложение, использующее библиотеку Boost и собираемое через CMake.

Структура проекта

myapp/
├── CMakeLists.txt
├── src/
│   └── main.cpp
└── Dockerfile

CMakeLists.txt

cmake_minimum_required(VERSION 3.20)
project(MyApp LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(Boost REQUIRED COMPONENTS system)

add_executable(myapp src/main.cpp)
target_link_libraries(myapp Boost::system)

main.cpp

#include <iostream>
#include <boost/asio.hpp>

int main() {
    boost::asio::io_context io;
    std::cout << "Boost.Asio работает в контейнере!" << std::endl;
    return 0;
}

Dockerfile с зависимостями

# Этап 1: сборка
FROM ubuntu:22.04 AS builder

# Устанавливаем зависимости для сборки
RUN apt-get update && apt-get install -y \
    build-essential \
    cmake \
    libboost-system-dev \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# Копируем исходники
COPY CMakeLists.txt .
COPY src/ src/

# Собираем
RUN mkdir build && cd build && \
    cmake .. -DCMAKE_BUILD_TYPE=Release && \
    make -j$(nproc)

# Этап 2: выполнение
FROM ubuntu:22.04

# Устанавливаем только runtime-зависимости
RUN apt-get update && apt-get install -y \
    libboost-system1.74.0 \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

COPY --from=builder /app/build/myapp .

CMD ["./myapp"]

Обратите внимание: на втором этапе мы устанавливаем только runtime-версию Boost (libboost-system1.74.0), а не полный пакет для разработки. Это экономит сотни мегабайт.

4. Продвинутые техники и best practices

Кэширование слоев Docker

Docker кэширует каждый слой. Чтобы ускорить сборку, размещайте редко меняющиеся инструкции (установка пакетов, копирование CMakeLists.txt) до часто меняющихся (копирование исходников).

# Плохо: каждое изменение в src/ переустанавливает пакеты
COPY src/ src/
RUN apt-get install -y build-essential cmake

# Хорошо: сначала зависимости, потом код
RUN apt-get install -y build-essential cmake
COPY CMakeLists.txt .
COPY src/ src/

Использование .dockerignore

Создайте файл .dockerignore, чтобы не копировать лишние файлы в контекст сборки:

build/
.git/
*.md
.gitignore

Запуск с ограничениями ресурсов

При запуске контейнера с C++ приложением полезно ограничивать CPU и память:

docker run --cpus="1.5" --memory="512m" myapp

Передача переменных окружения

C++ приложение может читать конфигурацию из переменных окружения:

# В коде: const char* port = std::getenv("APP_PORT");
docker run -e APP_PORT=8080 myapp

Заключение

Контейнеризация C++ приложений с Docker — это не просто модный тренд, а практическая необходимость для современной разработки. Мы рассмотрели:

  • Базовый Dockerfile — для быстрого старта и отладки
  • Многоступенчатую сборку — для минимального размера образа (от 6 МБ)
  • Работу с CMake и внешними зависимостями — для реальных проектов
  • Best practices

Блоги

Рекомендаций по книгам