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

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

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

Начать курс

Ваше C++ приложение достойно лучшей упаковки: погружаемся в Docker

Вы написали крутой парсер на C++, который молниеносно обрабатывает гигабайты логов. Или, может быть, создали высоконагруженный микросервис для работы с сетевыми сокетами. Код отлажен, всё летает. Но приходит момент истины — деплой. И тут начинается: «А у тебя какая версия libcurl?», «Почему упало на продакшене с ошибкой GLIBC?», «Зачем ты ставишь весь GCC на сервер?».

Знакомо? Добро пожаловать в мир контейнеризации C++ приложений с Docker. Если вы до сих пор думали, что Docker — это только про JavaScript и Python, вы многое упускаете. На самом деле, именно для C++ контейнеризация даёт суперсилу: вы получаете неизменяемое окружение, в котором бинарник собран один раз и работает везде — от вашего ноутбука до продакшен-кластера.

В этой статье мы не просто разберём теорию. Мы пошагово создадим C++ приложение, упакуем его в Docker, оптимизируем образ до минимально возможного размера и заставим контейнер работать как часы. Поехали!

Почему Docker — лучший друг C++ разработчика?

Многие считают, что C++ приложения легко распространять в виде бинарников. Технически — да. Практически — это путь в ад. Разные дистрибутивы Linux, версии glibc, отсутствие библиотек, битая ABI-совместимость — вот лишь часть проблем.

Docker решает три главные боли C++ разработчика:

  • Воспроизводимость сборки: вы точно знаете, какая версия компилятора, CMake, Boost и других зависимостей используется. Никаких «на моей машине работает».
  • Изоляция: контейнер не засоряет хост-систему. Не нужно ставить GCC на продакшн-сервер — достаточно минимального рантайма.
  • Масштабирование: готовый образ легко поднять в Kubernetes или Docker Swarm. C++ отлично справляется с нагрузкой, а Docker помогает эту нагрузку распределить.

И главное — контейнеризация C++ приложений не требует магии. Всё, что вам нужно, — это Docker и немного терпения.

Создаём простое C++ приложение для контейнеризации

Для примера возьмём не «Hello, World!», а что-то более живое. Допустим, мы пишем небольшой HTTP-сервер на C++ с использованием Boost.Beast. Но чтобы не перегружать статью, сделаем упрощённую версию — сокетный эхо-сервер на чистом POSIX API. Это покажет, как работать с системными зависимостями.

Файл server.cpp:

#include <iostream>
#include <string>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>

int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);
    char buffer[1024] = {0};

    // Создаём сокет
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // Привязываем к порту 8080
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(8080);

    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    if (listen(server_fd, 3) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    std::cout << "Server listening on port 8080" << std::endl;

    while (true) {
        if ((new_socket = accept(server_fd, (struct sockaddr *)&address,
                                 (socklen_t*)&addrlen)) < 0) {
            perror("accept");
            exit(EXIT_FAILURE);
        }

        read(new_socket, buffer, 1024);
        std::cout << "Received: " << buffer << std::endl;
        send(new_socket, buffer, strlen(buffer), 0);
        close(new_socket);
    }

    return 0;
}

Собирать будем через CMake, так как это стандарт для C++ проектов. Файл CMakeLists.txt:

cmake_minimum_required(VERSION 3.16)
project(echo_server)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_executable(echo_server server.cpp)

Отлично, код готов. Теперь самое интересное — пишем Dockerfile.

Первый подход: наивный Dockerfile (который не стоит использовать)

Новички часто делают так: берут полный образ Ubuntu, ставят GCC, CMake, собирают проект и оставляют всё это в финальном образе. Получается монстр на 1.5 ГБ. Мы так делать не будем, но для понимания покажу:

# ПЛОХОЙ ПРИМЕР! Не копируйте!
FROM ubuntu:22.04

RUN apt-get update && apt-get install -y g++ cmake make
WORKDIR /app
COPY . .
RUN cmake -B build && cmake --build build

CMD ["./build/echo_server"]

Почему это плохо? В образе остаются компилятор, исходники, временные файлы. Размер — около 1.2 ГБ. Для продакшена это катастрофа. Нам нужно легковесное C++ приложение в Docker.

Профессиональный подход: мультистейдж сборка

Секрет минимального Docker-образа для C++ — многоэтапная (multistage) сборка. Идея проста: в первом этапе (builder) мы ставим всё для компиляции, собираем бинарник. Во втором этапе (runtime) копируем только готовый бинарник и минимальные рантайм-зависимости.

Вот как выглядит правильный Dockerfile для C++:

# Этап 1: Сборка
FROM alpine:3.18 AS builder

# Устанавливаем только то, что нужно для компиляции
RUN apk add --no-cache g++ cmake make

WORKDIR /app
COPY . .

# Сборка в статику, чтобы не тащить libstdc++ динамически
RUN cmake -B build -DCMAKE_BUILD_TYPE=Release && \
    cmake --build build --target echo_server

# Этап 2: Финальный образ
FROM alpine:3.18

# Копируем только бинарник
COPY --from=builder /app/build/echo_server /usr/local/bin/echo_server

# Открываем порт
EXPOSE 8080

# Запускаем                

Блоги

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