The repository with an explanation is available here, and it has already received more than 100 stars. However, I want to explain it in more detail in this post. You can find a more technical explanation on the GitHub page as well, so I recommend reading that too.

Everything starts with the hardware. Most of the time, your target hardware is different from your host environment. So how can you develop and compile your application in this case? This post explains the process using Qt and OpenCV as examples, but the same approach can be applied to many other scenarios—not only cross-compilation.

Before Docker (This does not have to be docker but I was quite lazy before, I am still lazy but,, This can be done with namespaces as well in linux) I used Virtual Machines to carry whole development environment

However, in this case, a VM is huge—after all, it’s a complete operating system. Why do I need a full OS just to cross-compile Qt?

Aside from my laziness, there was no ready-to-use tool to efficiently emulate and run cross-compiled applications. Because of this, I built Qt for cross-compilation for my target inside a VM. The problem was that whenever a new Qt version was released or something went wrong with the VM, I had to recreate everything from scratch—again and again.

To cross-compile your application, you need toolchains for your target hardware. If you don’t know where to download them from, or if you don’t trust the available binaries, you have to build them yourself. Most of the time, this is a real pain.

If you know the target operating system, a precompiled toolchain usually exists. However, you often need to use the same OS as the target, which introduces yet another complication.

To cross-compile your application, you need toolchains for your target hardware. If you don’t know where to obtain them, or if you don’t trust the available binaries, you must build them yourself. Most of the time, this is a real pain.

If you know the target operating system, a precompiled toolchain usually exists. However, you often need to use the same operating system as the target, which introduces yet another complication.

Normally you can copy sysroot from your target directly via ftp/sftp but this is not healthy way. So docker gives you ability to have sysroot on the host directly. This means that you do not need even target ! ? to cross compile your application. We will have one dockerfile to create sysroot when this dockerfile is run and image is created from it we can copy the sysroot from container. looks easy.

This is beginning of the dockerfile for target hardware:

# arm64 Trixie base 2025-10-01-raspios-trixie-arm64
# 32 bit os will fail for this case
FROM arm64v8/debian:trixie

ENV DEBIAN_FRONTEND=noninteractive

First we need to choose os type. This is for raspberry pi for sure. It has to match with your target hardware os. Otherwise you may have wrong sysroot. Also becareful that you need to choose correct version for 32 or 64 bit.

Then dependencies


# Use Trixie repositories
RUN { \
  set -e; \
  printf '%s\n' \
    'deb http://deb.debian.org/debian trixie main contrib non-free-firmware' \
    'deb http://deb.debian.org/debian trixie-updates main contrib non-free-firmware' \
    'deb http://security.debian.org/debian-security trixie-security main contrib non-free-firmware' \
    > /etc/apt/sources.list; \
  apt-get update; \
  apt-get -y full-upgrade; \
  apt-get install -y --no-install-recommends \
    # toolchain & basics (native arm64, no cross toolchains needed)
    build-essential ninja-build cmake git wget python3 pkg-config gfortran \
    # graphics / xcb / mesa / egl / gbm
    libx11-dev libx11-xcb .........

As you see we use trixie repositories. Then when all packages are installed on emulated image we can zip/tar them and get it out from container.

RUN tar czf rasp.tar.gz -C / lib usr/include usr/lib etc/alternatives

Take it out from container. This command runs on host.

docker cp temp-arm:/build/rasp.tar.gz ./rasp.tar.gz

Now we have rasp.tar.gz file which has all dependencies from target. These are needed when you cross build the Qt for X server, scree, touch screen, network, 3d and more.

Next step we need to create another docker file to run x86 based image which OS matches exactly with the target hardware as well to install toolchains. This is very important so you do not need to compile toolchain for crosscompilation.

# arm64 Trixie base 2025-10-01-raspios-trixie-arm64
# 32 bit os will fail for this case
FROM arm64v8/debian:trixie

As you see same os version is chosen. Then again dependencies for the host environment:

# Use Trixie repositories
RUN { \
  set -e; \
  printf '%s\n' \
    'deb http://deb.debian.org/debian trixie main contrib non-free-firmware' \
    'deb http://deb.debian.org/debian trixie-updates main contrib non-free-firmware' \
    'deb http://security.debian.org/debian-security trixie-security main contrib non-free-firmware' \
    > /etc/apt/sources.list; \
  apt-get update; \
  apt-get -y full-upgrade; \
  apt-get install -y --no-install-recommends \
    # toolchain & basics (native arm64, no cross toolchains needed)
    build-essential ninja-build cmake git wget python3 pkg-config gfortran \
    # graphics / xcb / mesa / egl / gbm
    libx11-dev libx11-xcb-dev libxext-dev libxfixes-dev libxi-dev libxrender-dev \
    libxrandr-dev libxcursor-dev libxcomposite-dev libxdamage-dev libxss-dev libxtst-dev \
    libxcb1-dev libxcb-glx0-dev libxcb-keysyms1-dev libxcb-image0-dev libxcb-shm0-dev \
    libxcb-icccm4-dev libxcb-sync-dev 

If you check raspberry pi, you will see that for this specific version gcc14 is used. So we can find part of the toolchain that we need directly becuase we use same os. This is the trick otherwise these should be built by hand. This can be done in docker image as well but process can be different according to toolchain version.

    gcc-14-aarch64-linux-gnu \
    g++-14-aarch64-linux-gnu \

Next export sysroot which is produced from previous raspberry pi image.

# Set the working directory to /build
WORKDIR /build

# Create sysroot directory
RUN mkdir sysroot sysroot/usr sysroot/opt

# Copy Raspberry Pi sysroot tarball (if available)
COPY rasp.tar.gz /build/rasp.tar.gz
RUN tar xvfz /build/rasp.tar.gz -C /build/sysroot

You can find complete dockerfile in the link also, so I do not explain here line by line.

Now you can build Qt. For latest Qt version, Qt should be build for host as well because of tooling. So image is going to build Qt for host.

RUN { \
    set -e && \
    mkdir -p qt6 qt6/host qt6/pi qt6/host-build qt6/pi-build qt6/src && \
    cd qt6/src && \
    wget https://download.qt.io/official_releases/qt/6.10/6.10.1/submodules/qtbase-everywhere-src-6.10.1.tar.xz && \
    wget https://download.qt.io/official_releases/qt/6.10/6.10.1/submodules/qtshadertools-everywhere-src-6.10.1.tar.xz && \
    wget https://download.qt.io/official_releases/qt/6.10/6.10.1/submodules/qtdeclarative-everywhere-src-6.10.1.tar.xz && \

Using wget image download the tar files. So I designed the system in a way that you need to add each Qt module by hand. There are 3 of them for now. You can build whole Qt instead of module by module as well. It is up to you.

    cd ../host-build && \
    tar xf ../src/qtbase-everywhere-src-6.10.1.tar.xz && \
    tar xf ../src/qtshadertools-everywhere-src-6.10.1.tar.xz && \
    tar xf ../src/qtdeclarative-everywhere-src-6.10.1.tar.xz && \
    echo "Compile qtbase for host" && \
    cd qtbase-everywhere-src-6.10.1 && \
    cmake -GNinja -DCMAKE_BUILD_TYPE=Release \
        -DQT_BUILD_EXAMPLES=OFF \
        -DQT_BUILD_TESTS=OFF \
        -DCMAKE_INSTALL_PREFIX=/build/qt6/host && \
    cmake --build . --parallel 4 && \
    cmake --install . && \
    echo "Compile shader for host" && \
    cd ../qtshadertools-everywhere-src-6.10.1 && \
    /build/qt6/host/bin/qt-configure-module . && \
    cmake --build . --parallel 4 && \
    cmake --install . && \

First it will build base module and steps are same for others.

When Qt is compiled for host, it is time to compile it for target crossly!

    echo "Compile qtbase for rasp" && \
    cd qtbase-everywhere-src-6.10.1 && \
    cmake -GNinja -DCMAKE_BUILD_TYPE=Release -DINPUT_opengl=es2 \
        -DQT_BUILD_EXAMPLES=OFF -DQT_BUILD_TESTS=OFF \
        -DQT_HOST_PATH=/build/qt6/host \
        -DCMAKE_STAGING_PREFIX=/build/qt6/pi \
        -DCMAKE_INSTALL_PREFIX=/usr/local/qt6 \
        -DCMAKE_TOOLCHAIN_FILE=/build/toolchain.cmake \
        -DQT_FEATURE_xcb=ON -DFEATURE_xcb_xlib=ON \
        -DFEATURE_sql_psql=ON \
        -DQT_FEATURE_xlib=ON && \
    cmake --build . --parallel 4 && \
    cmake --install . && \

Simple cmake configuration.

RUN tar -czvf qt-host-binaries.tar.gz -C /build/qt6/host .
RUN tar -czvf qt-pi-binaries.tar.gz -C /build/qt6/pi .

So at the end of the compilation image will have two tar files for Qt. One of them for host binaries and one of them for target binaries. You can extract them later if you want on the target.

Okay we compiled the Qt but we need to be sure that Hello World application is able to be compiled as well. That is why we cross compile in the image simple application.

# Set up project directory
RUN mkdir /build/project
COPY project /build/project

# Build the project using Qt for Raspberry Pi
RUN { \
    cd /build/project && \
    /build/qt6/pi/bin/qt-cmake . && \
    cmake --build .; \
} 2>&1 | tee -a /build.log

To be able to get the binaries, you need to run

docker build -t qtcrossbuild .
docker create --name tmpbuild qtcrossbuild
docker cp tmpbuild:/build/qt-pi-binaries.tar.gz ./qt-pi-binaries.tar.gz
docker cp tmpbuild:/build/project/HelloQt6 ./HelloQt6

After this you will have binaries and helloworld binary. You can check with the file command 🙂

Opencv Compilation

There is an option for opencv compilation as well. If you want you can enable it passing BUILD_OPENCV=ON

docker build --build-arg BUILD_OPENCV=ON -t qtcrossbuild .
This is the part in the dockerfile where you can see the configuration for opencv and the version. I set it to 4.9.0 if you want to change it you can see some issues or you need to change the configuration.
ARG BUILD_OPENCV

# Build Opencv
RUN if [ "$BUILD_OPENCV" = "ON" ]; then \
    set -e && \
    echo "Cross Compile Opencv from source" && \
    mkdir -p /build/opencvBuild && \
    git clone --branch 4.9.0 --depth=1 https://github.com/opencv/opencv.git && \
    git clone --branch 4.9.0 --depth=1 https://github.com/opencv/opencv_contrib.git && \
    mkdir -p /build/opencv/build && \
    cd /build/opencv/build && \
    cmake \
        -G "Unix Makefiles" \
        -DCMAKE_BUILD_TYPE=Release \
        -DCMAKE_INSTALL_PREFIX=/build/opencvBuild \
        -DCMAKE_TOOLCHAIN_FILE=/build/opencvToolchain.cmake \
        -DOPENCV_EXTRA_MODULES_PATH=../../opencv_contrib/modules \
        -DCMAKE_C_FLAGS="-march=armv8-a -mtune=cortex-a53 -O2 --sysroot=/build/sysroot" \
        -DCMAKE_CXX_FLAGS="-march=armv8-a -mtune=cortex-a53 -O2 --sysroot=/build/sysroot" \
        -DCMAKE_C_FLAGS_RELEASE="-march=armv8-a -mtune=cortex-a53 -O2 --sysroot=/build/sysroot" \
        -DCMAKE_CXX_FLAGS_RELEASE="-march=armv8-a -mtune=cortex-a53 -O2 --sysroot=/build/sysroot" \
        -DOPENCV_ENABLE_NONFREE=ON \
        -DWITH_GSTREAMER=ON \
        -DWITH_FFMPEG=ON \
        -DWITH_V4L=ON \
        -DWITH_OPENGL=ON \
        -DWITH_GTK=OFF \
        -DWITH_QT=OFF \
        -DWITH_X11=ON \
        -DBUILD_opencv_highgui=ON \
        -DBUILD_TESTS=OFF \
        -DBUILD_PERF_TESTS=OFF \
        -DBUILD_EXAMPLES=OFF \
        -DOPENCV_ENABLE_CPU_DISPATCH=OFF \
        -DCPU_BASELINE="NEON" \
        -DCPU_DISPATCH="" \
        -DENABLE_NEON=ON \
        -DENABLE_VFPV3=OFF \
        -DENABLE_FP16=OFF \
        -DENABLE_BF16=OFF \
        -DENABLE_SVE=OFF \
        -DENABLE_SVE2=OFF \
        .. && \
    make -j4 VERBOSE=1 && \
    cmake --install . && \
    echo "Cross Compile Opencv completed" && \
    cd /build && \
    tar -czvf opencv-binaries.tar.gz -C /build/opencvBuild . \
; fi 2>&1 | tee -a /build.log

This is the video for raspberry pi compiling opencv with Qt https://www.youtube.com/watch?v=1dr1e2saxpo

Leave a Comment

Your email address will not be published. Required fields are marked *