Введение: зачем контейнеризировать 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