8-bit Assembly was Fun Ramblings of a not-so-old Programmer

Running a C++ gRPC Server inside Docker

I recently became interested in using Kubernetes (aka k8s) to run production services. One of the challenges I set for myself was to create a relatively small Docker image for a C++ server of some sort. After some fiddling with the development environment and tools I was able to create a 15MiB image that contains both a server and a small client. This post describes how I got this to work.

The Plan

My first idea was to create a minimal program, link it statically, and then copy the program into the Docker image. In principle that should make the image fairly small, a minimal Alpine Linux image is only 4MiB, if the program is statically linked no other requirements are needed.

Unfortunately, most Linux distributions use glibc, which, for all practical purposes requires dynamic linking to support “Name Service Switch” (NSS). Furthermore, glibc is licensed under the GNU Lesser General Public License, and I do not want to concern myself with the terms under which the binaries that statically link glibc may or may not be redistributed. I am interested in writing code, not in becoming a lawyer.

Fortunately Alpine Linux is based on the musl library, which supports static linking without any of the glibc headaches.

To make the build easy to reproduce, we will first create a Docker image containing all the development tools. I expect that this image will be rather large, as the development tools, plus libraries, plus headers can take significant space. The trick is to use Docker multi-stage builds to first compile the server using this large image, and then copy only the server binary into a much smaller Docker image.

Setting up the Development Environment

Most of the libraries and tools I needed to compile my server were readily available as Alpine Linux packages. So I just installed them using:

apk update && apk add build-base gcc g++

To get the static version of the C library you need to install one more package:

apk update && apk add libc-dev

I prefer to use Boost instead of writing my own libraries, so I also installed the development version of Boost and the static version of these libraries:

apk update && apk add boost-dev boost-static

I also prefer CMake as a meta-build system, and Ninja as its backend:

apk update && apk add cmake ninja

Finally I will use vcpkg to install any dependencies that do not have suitable Alpine Linux packages, so add some additional tools:

apk update && apk add curl git perl unzip tar

Compiling Additional dependencies

Some of the dependencies, such as gRPC, do not have readily available packages, in this case I just use vcpkg to build them. First we need to download and compile vcpkg itself:

git clone https://github.com/Microsoft/vcpkg.git
cd vcpkg
./bootstrap-vcpkg.sh --useSystemBinaries

Note that vcpkg can download the binaries it needs, such as CMake, or Perl, but I decided to disable these downloads. Now we can compile the dependencies:

./vcpkg install --triplet x64-linux grpc

The triplet option is needed because vcpkg seems to default to a non-usable triplet under Alpine Linux.

Compiling the gRPC server

With all the development tools in place I created a small project with a gRPC echo service. This project is available from GitHub:

git clone https://github.com/coryan/docker-grpc-cpp.git
cd docker-grpc-cpp

I prefer CMake as by build tool for C++, in this case we need to provide a number of special options:

Option Description
-H. Set the source directory
-B.build Configure the binary output directory
-GNinja Use Ninja as the backend build system
-DCMKE_BUILD_TYPE=Release Compile optimized binaries
-DCMAKE_TOOLCHAIN_FILE=…/vcpkg.cmake Use vcpkg in find_package()
-DBoost_USE_STATIC_LIBS=ON Use the static libraries for Boost
-DCMAKE_EXE_LINKER_FLAGS=”-static” Create static binaries

Putting these options together:

cmake -H. -B.build \
    -GNinja \
    -DCMKE_BUILD_TYPE=Release \
    -DCMAKE_TOOLCHAIN_FILE=/l/vcpkg/scripts/buildsystems/vcpkg.cmake \
    -DBoost_USE_STATIC_LIBS=ON \
    -DCMAKE_EXE_LINKER_FLAGS="-static"

and then build the usual way:

cmake --build .build

Scripting the Build

That is a lot of steps to remember, fortunately they are easy to script. First I created a Dockerfile prepare an image with the development tools:

sudo docker build -t grpc-cpp-devtools:latest -f tools/Dockerfile.devtools tools

As I expected this is a rather large image:

REPOSITORY          TAG                 SIZE
grpc-cpp-devtools   latest              974MB

But as I planned all along we can use that image to create the server image:

$ sudo docker build -t grpc-cpp-echo:latest -f examples/echo/Dockerfile.server .

Which is much smaller:

REPOSITORY          TAG                 SIZE
grpc-cpp-echo       latest              11.8MB
grpc-cpp-devtools   latest              974MB

Running the server

Of course this would all be for naught if we cannot run and use the server:

ID=$(sudo docker run -d -P grpc-cpp-echo:latest /r/echo_server)
ADDRESS=$(sudo docker port "${ID}" 7000)
sudo docker run --network=host grpc-cpp-echo:latest /r/echo_client \
    --address "${ADDRESS}"
Response ping-0
Response ping-1
Response ping-2
Response ping-3
Response ping-4
Response ping-5
Response ping-6
Response ping-7
Response ping-8
Response ping-9

Further Thoughts

I hope you found these instructions useful. I hope I will have time to describe using an image such as the one created in this post to run a Kubernetes-based service.