Overview #
In this tutorial I’m using the following setup based on Ubuntu 22.04 servers, GitLab is dockerized:
192.168.70.4 # GitLab
192.168.70.5 # GitLab Runner
192.168.70.6 # Deployment Server with Docker platform installed
I’m using the same setup for the deployment via SSH as described in my blog post:
GitLab CI Pipeline - Deploy website with Nginx Docker Container
The main requirements for the deployment via SSH key are the following:
-
Create a SSH key pair on the server where GitLab is deployed
-
Copy the public SSH key to the authorized keys file of the Deployment server
-
SSH into the deployment servers to add the fingerprint
-
Add the private SSH key as variable to the GitLab Repository CI/CD settings
-
On the deployment server, a user named “gitlab-deployment”, that is member of the Docker group, is used for the authentication of for the GitLab CI pipeline
GitLab Repository #
Overview #
In this tutorial I’m comparing different Dockerfiles for the Deployment of a C++ application. The container image sizes of the deployed containers are as follows:
-
Singlestage Dockerfile with Debian Slim:
586MB
-
Multistage Dockerfile with Debian Slim (Dynamic Linking):
336MB
-
Multistage Dockerfile with Debian Slim (Static Compilation):
78.3MB
-
Multistage Dockerfile with Alpine (Static Compilation):
11.3MB
File & Folder Structure #
The file and folder structure of the GitLab repository looks like this:
GitLab-Repository
├── Dockerfile
├── example-app
│ └── example-app.cpp
├── .gitlab-ci.yml
└── README.md
Dockerfile #
Singlestage: Debian Slim #
- Dockerfile
# Use Debian as the base image
FROM debian:bookworm-slim
# Set the working directory in the container
WORKDIR /app
# Install necessary dependencies in a single RUN command to reduce image layers
RUN apt update && apt install -y --no-install-recommends \
g++ \
libboost-system-dev \
libboost-thread-dev \
libboost-chrono-dev \
libboost-random-dev \
libssl-dev \
cmake \
&& apt clean && rm -rf /var/lib/apt/lists/*
# Copy the source code into the container
COPY example-app/example-app.cpp .
# Compile the C++ code
RUN g++ -o example-app example-app.cpp -lboost_system -lboost_thread -lssl -lcrypto -lpthread
# Expose the port on which the API will listen
EXPOSE 8080
# Run the compiled application when the container starts
ENTRYPOINT ["./example-app"]
Multistage Version: Debian Slim (Dynamic Linking) #
- This Dockerfile uses dynamic linking / dynamic compilation in the build stage. The binary does not include all the code from the libraries it depends on.
- Dockerfile
# Use Debian as the base image / Label image as "builder"
FROM debian:bookworm-slim AS builder
# Set the working directory in the container
WORKDIR /app
# Install necessary dependencies in a single RUN command to reduce image layers
RUN apt update && apt install -y --no-install-recommends \
g++ \
libboost-system-dev \
libboost-thread-dev \
libboost-chrono-dev \
libboost-random-dev \
libssl-dev \
cmake \
&& apt clean && rm -rf /var/lib/apt/lists/*
# Copy the source code into the container
COPY example-app/example-app.cpp .
# Compile the C++ code
RUN g++ -o example-app example-app.cpp -lboost_system -lboost_thread -lssl -lcrypto -lpthread
# Second stage: Use a minimal base image for the final runtime container
FROM debian:bookworm-slim
# Set the working directory in the container
WORKDIR /app
# Copy the compiled binary from the builder stage
COPY --from=builder /app/example-app .
# Install runtime dependencies (only what's needed to run the binary)
RUN apt update && apt install -y --no-install-recommends \
libboost-system-dev \
libboost-thread-dev \
libssl-dev \
&& apt clean && rm -rf /var/lib/apt/lists/*
# Expose the port on which the API will listen
EXPOSE 8080
# Run the compiled application when the container starts
ENTRYPOINT ["./example-app"]
Multistage Version: Debian Slim (Static Compilation) #
-
This Dockerfile uses static compilation in the build stage. The Compiled binary includes all necessary libraries within itself, making it independent of the runtime environment.
-
With a statically compiled binary, the second / runtime stage becomes very small. The image is about 78MB.
- Dockerfile
# Use Debian as the base image / Label image as "builder"
FROM debian:bookworm-slim AS builder
# Set the working directory in the container
WORKDIR /app
# Install necessary dependencies in a single RUN command to reduce image layers
RUN apt update && apt install -y --no-install-recommends \
g++ \
libboost-system-dev \
libboost-thread-dev \
libboost-chrono-dev \
libboost-random-dev \
libssl-dev \
cmake \
&& apt clean && rm -rf /var/lib/apt/lists/*
# Copy the source code into the container
COPY example-app/example-app.cpp .
# Compile the C++ code with static linking
RUN g++ -static -o example-app example-app.cpp -lboost_system -lboost_thread -lssl -lcrypto -lpthread
# Second stage: Use a minimal base image for the final runtime container
FROM debian:bookworm-slim
# Set the working directory in the container
WORKDIR /app
# Copy the compiled binary from the builder stage
COPY --from=builder /app/example-app .
# Expose the port on which the API will listen
EXPOSE 8080
# Run the compiled application when the container starts
ENTRYPOINT ["./example-app"]
Multistage Version: Alpine (Static Compilation) #
-
This Dockerfile uses static compilation in the build stage. The Compiled binary includes all necessary libraries within itself, making it independent of the runtime environment.
-
With a statically compiled binary, the second / runtime stage becomes very small. The image is about 11MB.
- Dockerfile
# Use Debian as the base image / Label image as "builder"
FROM debian:bookworm-slim AS builder
# Set the working directory in the container
WORKDIR /app
# Install necessary dependencies in a single RUN command to reduce image layers
RUN apt update && apt install -y --no-install-recommends \
g++ \
libboost-system-dev \
libboost-thread-dev \
libboost-chrono-dev \
libboost-random-dev \
libssl-dev \
cmake \
&& apt clean && rm -rf /var/lib/apt/lists/*
# Copy the source code into the container
COPY example-app/example-app.cpp .
# Compile the C++ code with static linking
RUN g++ -static -o example-app example-app.cpp -lboost_system -lboost_thread -lssl -lcrypto -lpthread
# Second stage: Use Alpine for the final runtime container
FROM alpine:latest AS runtime
# Set the working directory in the container
WORKDIR /app
# Copy the statically compiled binary from the builder stage
COPY --from=builder /app/example-app .
# Expose the port on which the application listens
EXPOSE 8080
# Run the compiled application when the container starts
ENTRYPOINT ["./example-app"]
CI Pipeline Manifest #
- .gitlab-ci.yml
### Variables
variables:
DEPLOY_IP: "192.168.70.6" # Deployment server IP
DEPLOY_USER: "gitlab-deployment" # Deployment server SSH user
DEPLOY_PORT: 8080 # Port for the container to expose
CONTAINER_NAME: "cpp-example-app" # Name of the deployed container
### Stages
stages:
- build
- deploy
### Build Container Image
build_image:
image: docker:stable
stage: build
services:
- docker:dind
variables:
DOCKER_TLS_CERTDIR: ""
before_script:
# Login to GitLab Container Registry using predefined CI/CD variables
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
script:
# Build the Docker image from the Dockerfile in the current directory
- docker build --pull -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG" .
# Push the built Docker image to the GitLab Container Registry
- docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG"
rules:
# Rule: Run this job only for main branch and if the Dockerfile exists
- if: $CI_COMMIT_BRANCH == "main"
exists:
- Dockerfile
### Deploy Container to Virtual Machine
deploy_container:
stage: deploy
image: alpine:latest
before_script:
# Update the package index, install the OpenSSH client for SSH connections
- apk update && apk add openssh-client
# If the private SSH key file ($ID_RSA) exists, set secure permissions (read/write for the owner only)
- if [ -f "$ID_RSA" ]; then chmod og= $ID_RSA; fi
script:
# SSH into the deployment server, log in to the GitLab Container registry
- ssh -i $ID_RSA -o StrictHostKeyChecking=no $DEPLOY_USER@$DEPLOY_IP "docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY"
# SSH into the deployment server, pull the image from the registry
- ssh -i $ID_RSA -o StrictHostKeyChecking=no $DEPLOY_USER@$DEPLOY_IP "docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG"
# SSH into the deployment server, remove the existing container (if it exists)
- ssh -i $ID_RSA -o StrictHostKeyChecking=no $DEPLOY_USER@$DEPLOY_IP "docker container rm -f $CONTAINER_NAME || true"
# SSH into the deployment server, run the new container
- ssh -i $ID_RSA -o StrictHostKeyChecking=no $DEPLOY_USER@$DEPLOY_IP "docker run -d -p $DEPLOY_PORT:$DEPLOY_PORT --restart=unless-stopped --name $CONTAINER_NAME $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG"
rules:
# Rule: Run this job only for main branch
- if: $CI_COMMIT_BRANCH == "main"
C++ Application #
- example-app/example-app.cpp
#include <boost/asio.hpp> // Include Boost.Asio for networking (TCP/IP handling)
#include <boost/beast.hpp> // Include Boost.Beast for HTTP and WebSocket protocols
#include <iostream> // Include the standard I/O library for printing to the console
namespace beast = boost::beast; // Create an alias for Boost.Beast to simplify usage
namespace http = beast::http; // Create an alias for Boost.Beast's HTTP module
namespace net = boost::asio; // Create an alias for Boost.Asio to simplify networking code
int main() {
try {
// The I/O context (required for all I/O in Boost.Asio)
net::io_context ioc;
// Create an endpoint to listen on port 8080 on all network interfaces
net::ip::tcp::acceptor acceptor(ioc, {net::ip::tcp::v4(), 8080});
std::cout << "Server is listening on http://0.0.0.0:8080\n";
while (true) {
// Create a socket for the next connection
net::ip::tcp::socket socket(ioc);
// Block until a connection is made
acceptor.accept(socket);
// Create an HTTP response
http::response<http::string_body> res{http::status::ok, 11};
res.set(http::field::server, "SimpleServer");
res.set(http::field::content_type, "text/plain");
res.body() = "Hello from C++ Example Application";
res.prepare_payload();
// Write the response to the socket
http::write(socket, res);
}
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
return 0;
}
Verify Container Deployment #
Basic Dockerfile Version: Debian Slim #
# List Docker containers
docker ps
# Shell output:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
68701f8eb75e gitlab-registry.jklug.work/root/cpp-project:main "./example-app" About a minute ago Up About a minute 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp cpp-example-app
# List docker images
docker images
# Shell output:
REPOSITORY TAG IMAGE ID CREATED SIZE
gitlab-registry.jklug.work/root/cpp-project main b28ac1535f9f 6 minutes ago 586MB
Multistage Dockerfile Version: Debian Slim (Dynamic Linking) #
# List Docker containers
docker ps
# Shell output:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
709481066c25 gitlab-registry.jklug.work/root/cpp-project:main "./example-app" 8 seconds ago Up 8 seconds 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp cpp-example-app
# List docker images
docker images
# Shell output:
REPOSITORY TAG IMAGE ID CREATED SIZE
gitlab-registry.jklug.work/root/cpp-project main dc935327a1ac 43 seconds ago 336MB
Multistage Dockerfile Version: Debian Slim (Static Compilation) #
# List Docker containers
docker ps
# Shell output:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f80f11e382d2 gitlab-registry.jklug.work/root/cpp-project:main "./example-app" 23 seconds ago Up 23 seconds 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp cpp-example-app
# List docker images
docker images
# Shell output:
REPOSITORY TAG IMAGE ID CREATED SIZE
gitlab-registry.jklug.work/root/cpp-project main 948c46af05bf About a minute ago 78.3MB
Multistage Dockerfile Version: Alpine (Static Compilation) #
# List Docker containers
docker ps
# Shell output:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
105ae1654ec6 gitlab-registry.jklug.work/root/cpp-project:main "tail -f /dev/null" About a minute ago Up About a minute 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp cpp-example-app
# List docker images
docker images
# Shell output:
REPOSITORY TAG IMAGE ID CREATED SIZE
gitlab-registry.jklug.work/root/cpp-project main e52e7245c868 2 minutes ago 11.3MB
Curl the Container #
# Curl the container
curl localhost:8080
# Shell output:
Hello from C++ Example Application