user2138149
user2138149

Reputation: 16486

How to build a Docker container to build a C++ application using vcpkg?

Most of the information contained in this question can be obtained here:


A series of events have caused me to want to try and build a C++ application inside a Docker container, while using vcpkg.

Here are a series of statements which should help provide context:

With that said, I cannot see a straightforward way to create a Docker container to do this.

I have one solution, which works, but it is not a good solution. The steps followed are described below.

First install vcpkg on the host

git clone https://github.com/microsoft/vcpkg.git
cd vcpkg && ./bootstrap-vcpkg.sh -disableMetrics

export VCPKG_ROOT=/path/to/vcpkg
export PATH=$VCPKG_ROOT:$PATH

Second, initialize a new C++ project with vcpkg

mkdir myproject && cd myproject
vcpkg new --application
# created `vcpkg-configuration.json` and `vcpkg.json`

# example: add dependencies using vcpkg
vcpkg add port fmt

Third, create CMakeLists.txt

There are other build systems available. My understanding is that vcpkg is compatiable with a range of build systems. I am using cmake by default, not for any particular reason.

# CMakeLists.txt
cmake_minimum_required(VERSION 3.10)

project(myproject)

find_package(fmt CONFIG REQUIRED)

add_executable(myproject main.cpp)

target_link_libraries(myproject PRIVATE fmt::fmt)

Fourth step: Create main.cpp

main.cpp can contain a minimal "hello world" example.

Fifth step: Create CMakePresets.json and CMakeUserPresets.json, and build

# CMakePresets.json
{
  "version": 2,
  "configurePresets": [
    {
      "name": "vcpkg",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build",
      "cacheVariables": {
        "CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
      }
    }
  ]
}
# CMakeUserPresets.json
{
    "version": 2,
    "configurePresets": [
      {
        "name": "default",
        "inherits": "vcpkg"
      }
    ]
  }

Note that this file (CMakeUserPresets.json) differs from the example file provided in the documentation, shown below:

# CMakeUserPresets.json (example from documentation)
{
    "version": 2,
    "configurePresets": [
      {
        "name": "default",
        "inherits": "vcpkg",
        "environment": {
          "VCPKG_ROOT": "<path to vcpkg>"
        }
      }
    ]
  }

I tried changing

"VCPKG_ROOT": "<path to vcpkg>"

to

"VCPKG_ROOT": "$env{VCPKG_ROOT}"

however, this resulted in an error when trying to run

$ cmake --preset=default
CMake Error: Could not read presets from /home/user/myproject: Invalid macro expansion

Therefore I removed "environment", and found that it seems to work.

The next error I encountered was the following:

CMake was unable to find a build program corresponding to "Ninja".

Therefore I changed

"generator": "Ninja"

to

"generator": "Unix Makefiles"

in CMakePresets.json.

Then the command ran ok: cmake --preset=default.

Finally, to build:

cmake --build build

Note that if we were to repeat these steps inside a Docker container, the first step would be required, but none of the remaining steps are required.

In the first step, we installed vcpkg, which would need to be done inside a Docker container, because it is unlikely we find an existing image which contains everything we want.

However, the remaining steps do not need to be repeated. We simply copy the required files into the container image.

There are two problem with this approach:

To explain the second point in more detial: The build of the Docker container image depends on some of the vcpkg commands having been run before the container is built. This is because the container depends on the files vcpkg-configuration.json and vcpkg.json. These are generated by running some vcpkg commands. To generate them, the host system requires an installation of vcpkg.

It feels a bit "chicken and egg".

It would be possible to run these commands inside the container as part of the build process instead, however then vcpkg-configuration.json and vcpkg.json would be generated files rather than files stored under source control.

Here's a copy of the Dockerfile.

FROM gcc:latest

WORKDIR /root

RUN apt update && apt install curl zip unzip tar cmake -y

RUN git clone https://github.com/microsoft/vcpkg.git
RUN cd vcpkg && ./bootstrap-vcpkg.sh -disableMetrics

ENV VCPKG_ROOT=/root/vcpkg
ENV PATH=$VCPKG_ROOT:$PATH

RUN mkdir /app
WORKDIR /app

COPY ./myproject ./myproject
WORKDIR /app/myproject

RUN cmake --preset=default
RUN cmake --build build

A .dockerignore file is also required to prevent copying of the build directory. Without this the build will error.

#.dockerignore
**/build

To run it

$ docker build -t myproject/myprojectbuild .
$ docker run --rm myproject/myprojectbuild

To summarize:

Upvotes: -5

Views: 106

Answers (1)

David Maze
David Maze

Reputation: 158598

I could install a second version of gcc...

cmake supports a CC environment variable to specify the location of the compiler. This is fairly common among non-hand-built build systems; GNU Automake definitely supports it, it is a standard variable in make, and so on.

Before doing anything else, I'd try just using the alternate compiler you have installed on the host

export CC=gcc-14 CXX=g++-14
cmake --build

If you really want to run this build in a container, remember that a container (usually) has its own installation of a base Linux distribution. "Don't break Debian" is good advice, but if you did happen to break it in a container, you can just delete the container and start over. Using the Debian alternatives system would only update the default compiler in the container, and not affect the host at all.

This, plus some mechanical difficulties, means you'd typically avoid version-manager tools in a container environment. Install the single toolchain you need to build the single application that the container runs.

For this example I'll use Ubuntu, which is closely-related to Debian, but of note its toolchain can be updated with a library of newer compilers. In a Dockerfile you can install the toolchain and build the application.

FROM ubuntu:24.04

# Make the ubuntu-toolchain-r PPA available
RUN apt-get update \
 && DEBIAN_FRONTEND=noninteractive \
    apt-get install --assume-yes software-properties-common \
 && add-apt-repository ppa:ubuntu-toolchain-r/ppa

# Install dependencies, including a newer gcc
RUN apt-get update \
 && DEBIAN_FRONTEND=noninteractive \
    apt-get install --assume-yes \
      build-essential \
      gcc-14
# If you need any libraries, also install their -dev packages here

# Make gcc-14 be the system default gcc (within the image)
RUN ln -sf gcc-14 /usr/bin/gcc

# Copy in the application and build it
WORKDIR /app
COPY ./ ./
RUN cmake --build

I'd remove the CMake default preset that forces vcpkg. You can rename it if you'd like, and then cmake --preset vcpkg --build would build with it.

You'd build this image like any other, but getting the binary out is a little tricky. You need to create a temporary container so that you can run docker cp.

docker build -t my-app .
docker create --name temp-app my-app
docker cp -r temp-app:/app/out ./out
docker rm temp-app

Note that, if your host is not a Linux host, this will produce a Linux binary, which could be problematic for you. The build environment will also have different C shared libraries installed. This frequently isn't a problem but it occasionally is, especially if there's a newer version of the GNU C library in the newer version of the Linux distribution.

(I generally disagree with the assertion that isolating build tools inside a container simplifies the build process.)


If your actual goal is to run the program in a container too then you should use a Docker multi-stage build for this. The idea is to build the application in an image, and then build a second copy of the image that doesn't include the (usually very large) toolchain. That turns out to be simpler to run, provided your application is amenable to being run in a container (it doesn't depend on local storage or interact on stdio, for example).

You'd update the Dockerfile:

FROM ubuntu:24.04 AS build
#                 ^^^^^^^^ add

# ...all of the toolchain setup and build from above...

RUN cmake --build
RUN cmake --install

FROM ubuntu:24.04

# Get the installed application from the previous stage
COPY --from=build /usr/local /usr/local

# Set the standard container metadata to run it
CMD ["myapp"]

It's often good practice to create a non-root user in the last stage and switch to it as one of the last steps. If your application needs C shared libraries from Debian packages, you'd also need to RUN apt-get update && apt-get install the non-development packages in the final stage.

Upvotes: 1

Related Questions