Skip to content

Building from source

Every build is fully declarative — the flake pins nixpkgs and every derived package, so a rebuild on a different machine produces a byte-identical image.

Prerequisites

  • Nix with flakes enabled.
  • Docker (to load the resulting image).

With Nix

git clone https://github.com/kartoza/qgis-desktop-docker.git
cd qgis-desktop-docker
nix run .#build-docker

Under the hood this runs:

nix build .#docker -o result
nix store cat "$(nix build .#docker --print-out-paths)" | docker load

The image is loaded as kartoza:qgis-desktop-ltr, and tagged with its exact QGIS version (kartoza:qgis-desktop-3.44.9) as well. :qgis-desktop-ltr is the tag every nix run .#run-* target and every compose file expects. Run it:

docker run --rm -p 8443:8443 --cap-add=NET_ADMIN kartoza:qgis-desktop-ltr

Choosing the QGIS channel

Two images are built from the same source; the QGIS package is the only difference between them.

Target QGIS Image tag
nix run .#build-docker (default) Long-term release kartoza:qgis-desktop-ltr, also kartoza:qgis-desktop-3.44.9
nix run .#build-docker-latest Current release kartoza:qgis-desktop-latest, also kartoza:qgis-desktop-4.0.1

Both channel tags move as QGIS ships. The version tag never does, so that is what a deployment pins when it must not move underneath you.

Locally built images spell out what they hold, because the repository half of the name is just kartoza. Published images do not need to — the GHCR repository is already qgis-desktop-docker, so those are plain :ltr and :latest.

Why LTR is the default. The LTR line only takes bug fixes, so a project that opens today opens the same way next month. That is what you want in front of users.

Why the other one exists. QGIS's current release becomes the next LTR. The :latest image lets you open your real projects, plugins and data against it now — while a regression can still be reported and fixed upstream, rather than on the day the LTR ships.

nix run .#build-docker-latest
docker run --rm -p 8443:8443 --cap-add=NET_ADMIN kartoza:qgis-desktop-latest

Both images accept exactly the same environment variables, so a compose file can be pointed at either by changing the tag alone.

Which QGIS is in a given image, without starting it:

docker image inspect kartoza:qgis-desktop-ltr \
  --format '{{index .Config.Labels "com.kartoza.qgis.channel"}} {{index .Config.Labels "com.kartoza.qgis.version"}}'

A running container prints the same on its first log line, and exports QGIS_DESKTOP_QGIS_CHANNEL and QGIS_DESKTOP_QGIS_VERSION into the session.

To build only the QGIS package for a channel — useful when checking whether a plugin's dependencies resolve — use nix build .#qgis-ltr or nix build .#qgis-latest.

With Make

Convenience wrappers around the same commands:

make build-docker    # Build the image
make run             # Run in foreground
make run-detached    # Run in background
make run-persistent  # Run with a persistent home volume
make stop            # Stop the container
make summary         # Regenerate build-summary.md
make compose-up      # Start via docker-compose
make compose-down    # Stop docker-compose

Build artefacts

Every build produces two artefacts alongside the image, published as 7-day PR artifacts and permanently attached to releases:

  • sbom.spdx.json — SPDX 2.3 Software Bill of Materials generated by anchore/sbom-action. Lists every package in the image with version and licence.
  • cve-scan.json — Grype vulnerability scan of the image with severity ratings.

nix run .#summary (or make summary) also produces a build-summary.md that renders the SBOM and CVE data as Markdown tables suitable for the PR comment or release notes.

Reproducibility check

Running nix build .#docker on two machines with the same flake should produce identical store paths. If they differ, something in the inputs is not pinned — file an issue.