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.txt
Top 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 . -j
Note for beginners: because both of our applications are independent from each other, I’m adding
-j
to the build command to parallelize build and speed up the whole process.
After the build is done, you can run:
docker images
to 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 147MB
You can run them calling, e.g.:
docker run --rm --name container_app_1 image_app_1
Adding 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.
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_1
line, 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_1
line – in such approach, the user runningcontainer_app_1
target is responsible for assuring thatimage_app_1
image 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_1
I 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_1