Welcome to the next pikoTutorial !
In one of the recent articles, I showed how to use CMake for setting up a Python project. Today, we will see how to further extend it by adding building of Docker images to the CMakeLists.txt files. Today’s project structure:
project/
├── app1/
│ ├── CMakeLists.txt
│ ├── Dockerfile
│ ├── main.py
│ ├── requirements.txt
├── app2/
│ ├── CMakeLists.txt
│ ├── Dockerfile
│ ├── main.py
│ ├── requirements.txt
└── build/
├── CMakeLists.txtTop CMakeLists.txt file
Nothing special here, just assuring Python availability and adding sub-directories to the build:
# specify minimum CMake version
cmake_minimum_required(VERSION 3.28)
# specify project name
project(CMakeWithDocker)
# find Python
find_package(Python3 REQUIRED COMPONENTS Interpreter)
# include all subdirectoies into the build
add_subdirectory(app1)
add_subdirectory(app2)Dockerfiles
Both app1/Dockerfile and app2/Dockerfile look the same:
FROM python:3.9-slim
WORKDIR /app
COPY . /app
RUN pip install --no-cache-dir -r requirements.txt
CMD ["python", "main.py"]CMake files
Both app1/CMakeLists.txt and app2/CMakeLists.txt look similar with the only difference being the application name:
set(IMAGE_TARGET image_app_1)
# add custom target to build image for app1
add_custom_target(${IMAGE_TARGET} ALL
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/app1
COMMAND docker build -t image_app_1 .
COMMENT "Building image for app1"
)Pay attention to ALL placed after the target’s name. CMake, by default, doesn’t build custom targets, so you must make them explicitly dependent on all target (the default CMake target executed when calling cmake without --target flag).
Building the project
Now when everything’s ready, you can build the project by calling:
cd build
cmake ..
cmake --build . -jNote for beginners: because both of our applications are independent from each other, I’m adding
-jto the build command to parallelize build and speed up the whole process.
After the build is done, you can run:
docker imagesto check that 2 new images appeared:
REPOSITORY TAG IMAGE ID CREATED SIZE
image_app_1 latest 150d015d8915 3 minutes ago 150MB
image_app_2 latest d16b1453dbcc 3 minutes ago 147MBYou can run them calling, e.g.:
docker run --rm --name container_app_1 image_app_1Adding a dependent target
It’s often a good idea to initialize the project, build, run tests etc. before building the final image. Here, as an example, I’ll add initialization of the virtual environment of each Python application as a mandatory step for building the images:
set(VENV_TARGET venv_app_1)
set(IMAGE_TARGET image_app_1)
# add custom target to create virtual environment
add_custom_target(${VENV_TARGET} ALL
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/app1
COMMAND ${Python3_EXECUTABLE} -m venv venv
COMMENT "Creating virtual environment for app1"
)
# add custom target to build image for app1
add_custom_target(${IMAGE_TARGET} ALL
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/app1
COMMAND docker build -t image_app_1 .
COMMENT "Building image for app1"
DEPENDS ${VENV_TARGET}
)Notice that here I had to add DEPENDS argument to image_app_1 custom target definition. It assures that venv_app_1 target will be completed before image starts to build.

AI is powerful. Snippets are instant.
Stop prompting for the same patterns repeatedly. Get almost 100 free VS Code snippets for C++, Python, CMake and Bazel from piko::snippets GitHub repository.
Running Docker container with CMake
But why stop here? Let’s add the target for running the container from CMake level:
set(VENV_TARGET venv_app_1)
set(IMAGE_TARGET image_app_1)
# add custom target to create virtual environment
add_custom_target(${VENV_TARGET} ALL
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/app1
COMMAND ${Python3_EXECUTABLE} -m venv venv
COMMENT "Creating virtual environment for app1"
)
# add custom target to build image for app1
add_custom_target(${IMAGE_TARGET} ALL
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/app1
COMMAND docker build -t image_app_1 .
COMMENT "Building image for app1"
DEPENDS ${VENV_TARGET}
)
# add custom target to run container for app1
add_custom_target(container_app_1
COMMAND docker run --rm --name container_app_1 image_app_1
COMMENT "Running container for app1"
DEPENDS ${IMAGE_TARGET}
)When it comes to specifying dependencies for the container_app_1 target, you have 2 options:
- you can add
DEPENDS image_app_1line, as I did above – this will make sure that the container being started, always bases on the newest version of the image, so it can be potentially re-built every single time when running the container
[ 33%] Creating virtual environment for app1
[ 33%] Built target venv_app_1
[ 66%] Building image for app1
[+] Building 1.1s (9/9) FINISHED
=> [internal] load build definition from Dockerfile
=> => transferring dockerfile: 162B
=> [internal] load metadata for docker.io/library/python:3.9-slim
=> [internal] load .dockerignore
=> => transferring context: 2B
=> [1/4] FROM docker.io/library/python:3.9-slim
=> [internal] load build context
=> => transferring context: 125.50kB
=> CACHED [2/4] WORKDIR /app
=> CACHED [3/4] COPY . /app
=> CACHED [4/4] RUN pip install --no-cache-dir -r requirements.txt
=> exporting to image
=> => exporting layers=> => writing image
=> => naming to docker.io/library/image_app_1
[ 66%] Built target image_app_1
[100%] Running container for app1
Hello from Python app1
[100%] Built target container_app_1- you can omit
DEPENDS image_app_1line – in such approach, the user runningcontainer_app_1target is responsible for assuring thatimage_app_1image already exists, but it will allow you to just run the container basing on whatever image version has been recently built
[100%] Running container for app1
Hello from Python app1
[100%] Built target container_app_1I didn’t add ALL to container_app_1 custom target because most likely you want to run the container on demand, not during every project build. To run the container, call:
cmake --build . --target container_app_1Read also:
- A 40-line LLM-based bash command executor in Python
- GTest and short-circuit evaluation in C++
- AI is powerful. Snippets are instant.
- From AUTOSAR to S-Core: the first C++ pub/sub implementation
- How to write Arduino Uno code with Python?
- Combining Bazel with Docker
- Running commands with timeout on Linux
- Running Python unit tests with CMake
- Thirdparty dependencies with FetchContent
- Bug of the week #11
- Combining CMake with Docker
- How to search the internet from Linux terminal?
- Folding expressions in C++
- How to derive from an enum in Python?
- Bug of the week #10
- Trying ROS2: client/server within a single container
- Make C++ a better place #4: Go as an alternative
- How to convert hex to dec in Linux terminal?
- Setting up a Python project with CMake
- Separating builds for different configs with Bazel
- Trying ROS2: pub/sub within a single container
- Bug of the week #9
- UDP multicasting with Python
- Destruction order vs thread safety in C++
- Let’s review some code: C++ #2
- Make C++ a better place #3: D as an alternative
- Registering callback using std::function in C++
- Bug of the week #8
- TCP client/server with Python
- Simple menus in Bash scripts with select
- Calling member function on a nullptr in C++
- Bug of the week #7
- Python lru_cache explained
- How to dockerize a Python application?
- Make C++ a better place #2: CppFront as an alternative
- Parameters combinations in GoogleTest
- Data transfer with curl
- Python reduce explained
- Bug of the week #6
- Custom literals in C++
- Linux and hash command
- 5 Python good practices which make life easier
- Let’s review some code: Python #1
- Make C++ a better place #1: What does better mean
- Enums vs enum class in C++
- Bug of the week #5
- UDP client/server with Python
- Hard links in Linux
- Functions calling order in unit tests in C++
- Bug of the week #4
- Yield in Python – state machines, coroutines and more
- Copy files from another branch with Git
- Make C++ a better place #0: Introduction
- 5 misconceptions about std::move in C++
- How to use xargs on Linux?
- How to test method call order with unittest in Python?
- Bug of the week #3
- Build & run C++ unit tests with CMake
- Arrange text with sort on Linux
- Key derivation function with Python









