Ваше 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
# Запускаем