Introducción: ¿por qué contenerizar aplicaciones C++?
El desarrollo en C++ tradicionalmente se asocia con el problema de "funciona en mi máquina". Diferentes versiones de compiladores, bibliotecas y dependencias del sistema convierten el despliegue en un dolor de cabeza. Docker resuelve este problema: empaqueta la aplicación con todo su entorno en un contenedor ligero que se ejecuta de la misma manera en cualquier sistema.
En este artículo, analizaremos el proceso completo de contenerización de una aplicación C++, desde un Dockerfile simple hasta una compilación multi-etapa con optimización del tamaño de la imagen.
1. Fundamentos: Dockerfile para una aplicación C++
Comencemos con el ejemplo más simple. Supongamos que tenemos un programa en C++ que imprime "Hello, Docker!".
Código fuente (hello.cpp)
#include <iostream>
int main() { std::cout << "Hello, Docker!" << std::endl; return 0;}Primer Dockerfile
# Usamos la imagen con GCCFROM gcc:13.2
# Establecemos el directorio de trabajoWORKDIR /app
# Copiamos los fuentesCOPY hello.cpp .
# CompilamosRUN g++ -o hello hello.cpp
# EjecutamosCMD ["./hello"]Construimos y ejecutamos:
docker build -t cpp-hello .docker run cpp-helloLa desventaja de este enfoque es que la imagen pesa alrededor de 1.2 GB debido al conjunto completo de herramientas GCC. Para producción, esto es inaceptable.
2. Compilación multi-etapa (Multi-stage build)
La mejor práctica es separar la compilación y la ejecución. En la primera etapa, compilamos la aplicación con el conjunto completo de herramientas; en la segunda, copiamos solo el binario resultante a una imagen mínima y limpia.
Dockerfile optimizado
# Etapa 1: compilaciónFROM gcc:13.2 AS builder
WORKDIR /app
COPY hello.cpp .
RUN g++ -static -o hello hello.cpp
# Etapa 2: ejecuciónFROM alpine:3.19
WORKDIR /app
# Copiamos el binario compilado estáticamenteCOPY --from=builder /app/hello .
CMD ["./hello"]El punto clave es la bandera -static. Esta incluye todas las dependencias en el binario, permitiendo ejecutarlo en Alpine puro sin bibliotecas adicionales. ¡El tamaño de la imagen se reduce de 1.2 GB a ~6 MB!
3. Proyecto real: CMake + dependencias
Consideremos un caso más complejo: una aplicación que utiliza la biblioteca Boost y se compila con CMake.
Estructura del proyecto
myapp/├── CMakeLists.txt├── src/│ └── main.cpp└── DockerfileCMakeLists.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 funciona en el contenedor!" << std::endl; return 0;}Dockerfile con dependencias
# Etapa 1: compilaciónFROM ubuntu:22.04 AS builder
# Instalamos dependencias de compilaciónRUN apt-get update && apt-get install -y \ build-essential \ cmake \ libboost-system-dev \ && rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Copiamos los fuentesCOPY CMakeLists.txt .COPY src/ src/
# CompilamosRUN mkdir build && cd build && \ cmake .. -DCMAKE_BUILD_TYPE=Release && \ make -j$(nproc)
# Etapa 2: ejecuciónFROM ubuntu:22.04
# Instalamos solo dependencias de runtimeRUN 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"]Observe: en la segunda etapa, instalamos solo la versión de runtime de Boost (libboost-system1.74.0).