Skip to content

Commit d53bb2a

Browse files
authored
What’s under the hood of the official QGIS Server Docker image? (#48)
Follow up geotribu/website#1287 *Note* : I used an LLM tool to refine and correct some sentences.
2 parents 976031c + 46bdade commit d53bb2a

File tree

4 files changed

+354
-0
lines changed

4 files changed

+354
-0
lines changed

content/.authors.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,10 @@ authors:
4949
slug: thomas-szczurek-gayant
5050
url: https://geotribu.fr/team/thomas-szczurek-gayant/
5151
52+
pblottiere:
53+
avatar: https://cdn.geotribu.fr/img/internal/contributeurs/pblottiere.jpg
54+
description: Coding in the Wild West
55+
name: Paul BLOTTIERE
56+
slug: paul-blottiere
57+
url: https://geotribu.fr/team/paul-blottiere/
58+
Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
---
2+
title: "What’s under the hood of the official QGIS Server Docker image?"
3+
subtitle: (boo !)
4+
authors:
5+
- pblottiere
6+
categories:
7+
- article
8+
comments: true
9+
date: 2025-04-18
10+
description: "The Mysteries of the Official QGIS Server Docker Image"
11+
icon: material/docker
12+
image:
13+
license: default
14+
links:
15+
- Original version (French): https://geotribu.fr/articles/2025/2025-04-15_official-qgis-server-docker-image/
16+
tags:
17+
- Docker
18+
- QGIS
19+
- QGIS Server
20+
---
21+
22+
# What’s under the hood of the official QGIS Server Docker image?
23+
24+
## Introduction
25+
26+
![logo QGIS](https://cdn.geotribu.fr/img/logos-icones/logiciels_librairies/qgis.png){: .img-thumbnail-left }
27+
28+
Since the last Geotribu article about [deploying QGIS Server](https://geotribu.fr/articles/2010/2010-09-03_creer_diffuser_services_wms_avec_qgis/) dates back to 2010, the release of an official QGIS Server Docker image last year is a great opportunity to catch up!
29+
30+
As a quick reminder, QGIS Server is an open-source web mapping server solution—similar to [GeoServer](https://geoserver.org/) or [MapServer](https://mapserver.org/)—that allows you to serve maps and geospatial data on the web. It relies on OGC (Open Geospatial Consortium) standards to provide interoperable services. QGIS Desktop offers a graphical interface for users to create and edit their maps.
31+
32+
The official QGIS Server documentation explains in detail how to [install QGIS Server](https://docs.qgis.org/3.40/fr/docs/server_manual/getting_started.html) natively, i.e. directly from your platform or distribution’s package repositories. After the official QGIS Server image was released last year, Anita Graser shared a great [post](https://anitagraser.com/2024/04/20/qgis-server-docker-edition/) about its usage. However, comprehensive resources on containerized deployment remain limited. That’s why in this article, we’ll take a look at how and why using the official QGIS Server Docker image makes deployment much easier.
33+
34+
## Technical ecosystem
35+
36+
![logo Docker](https://cdn.geotribu.fr/img/logos-icones/logiciels_librairies/docker.svg){: .img-thumbnail-left }
37+
38+
Yes, there has been an official QGIS Server Docker image since 2024 :tada:... but what does that actually mean? Let’s start by first reviewing the underlying tech stack and a bit of vocabulary.
39+
40+
`Docker`
41+
42+
: Docker is a platform that allows you to create, deploy, and run applications in containers, ensuring their portability and isolation. These applications can be distributed as images through specialized platforms or rebuilt from source code.
43+
44+
`Dockerfile`
45+
46+
: A [Dockerfile](https://docs.docker.com/reference/dockerfile/) is a script that defines the steps to build a custom Docker image.
47+
48+
`Docker Hub`
49+
50+
: [Docker Hub](https://hub.docker.com/) is an online platform that allows you to store, share, and distribute Docker images, providing a centralized repository for applications and their components.
51+
52+
`docker compose`
53+
54+
: Composition – typically via the [Docker Compose](https://docs.docker.com/compose/) tool – allows you to define and manage multi-container applications using a configuration file that describes the services, networks, and volumes required for the application.
55+
56+
`Cluster cloud`
57+
58+
: Clustering – not covered in this article – refers to the management of multiple Docker instances distributed across several physical or virtual machines to increase the availability, resilience, and scalability of the application. It typically involves cloud environments, such as those based on [Kubernetes](https://kubernetes.io/).
59+
60+
Many QGIS Server Docker images are available online, each with its own specificities related to its use and configuration. However, since 2024, the image originally provided by [OPENGIS.ch](https://www.opengis.ch/) is now available as an official image on the [QGIS.org repository on Docker Hub](https://hub.docker.com/r/qgis/qgis-server). To get it, it’s as simple as:
61+
62+
```bash title="Downloading the official QGIS Server image"
63+
docker pull qgis/qgis-server:ltr
64+
```
65+
66+
## Study of the content
67+
68+
To deploy QGIS Server as an application, it is necessary to integrate third-party services into the container to ensure its proper functioning. As stated in the documentation, QGIS Server is an application:
69+
70+
- Requiring a graphical server.
71+
- Based on the FastCGI communication protocol to interact with a web server.
72+
- Depending on the web server used (Apache, NGINX, etc.), a specific utility may be required to launch the underlying FCGI process.
73+
74+
To meet these requirements, several technical solutions can be considered, which partly explains the diversity of QGIS Server Docker images available online. The image provided by OPENGIS.ch relies on:
75+
76+
- [Xvfb](https://www.x.org/archive/X11R7.7/doc/man/man1/Xvfb.1.xhtml) (X virtual framebuffer) as the graphical server.
77+
- [NGINX](https://nginx.org/) as the web sever.
78+
- [spawn-fcgi](https://linux.die.net/man/1/spawn-fcgi) as the utility to run the QGIS Server FastCGI application.
79+
80+
### QGIS Server and FastCGI
81+
82+
![logo X11](https://cdn.geotribu.fr/img/logos-icones/logiciels_librairies/x11.png){: .img-thumbnail-left }
83+
84+
It is possible to easily test the QGIS Server application from the command line as long as a graphical server is running. To do this, you need to simulate the passing of environment variables to the FCGI process, as a web server would do. For example, you can send a request to QGIS Server using the `REQUEST_URI` environment variable:
85+
86+
```bash title="Execution of a request by the QGIS Server FCGI process"
87+
# Starting a shell in a QGIS Server container
88+
$ docker run -it qgis/qgis-server:ltr /bin/bash
89+
90+
# Running the virtual Xvfb graphical server in the background and redirecting
91+
# standard output (file descriptor 1) and standard error (file descriptor 2)
92+
# to /dev/null to suppress any output
93+
$ /usr/bin/Xvfb :99 > /dev/null 2>&1 &
94+
95+
# Sending a request to QGIS Server and redirecting the logs to /dev/null
96+
$ REQUEST_URI="MAP=fake.qgs" /usr/lib/cgi-bin/qgis_mapserv.fcgi 2>/dev/null
97+
Content-Length: 195
98+
Content-Type: text/xml; charset=utf-8
99+
Server: QGIS FCGI server - QGIS version 3.43.0-Master
100+
Status: 500
101+
102+
<?xml version="1.0" encoding="UTF-8"?>
103+
<ServerException>Project file error. For OWS services: please provide a SERVICE and a MAP parameter pointing to a valid QGIS project file</ServerException>
104+
```
105+
106+
Here, we observe a `500` error code from QGIS Server indicating that the `fake.qgs` project specified via `REQUEST_URI="MAP=fake.qgs"` does not exist. The exception `<ServerException>Project file error.</ServerException>` is therefore returned by QGIS Server.
107+
108+
!!! note
109+
`Xvfb :99` starts a virtual X server with the display number `:99`, meaning that graphical applications running with this X server instance will not be displayed on a real monitor but will be rendered in memory. Once this virtual X server is started, the environment variable `ENV DISPLAY :99` is set in the [Dockerfile](https://github.com/qgis/qgis-docker/blob/main/server/Dockerfile) so that the QGIS Server application knows where to render in memory.
110+
111+
### Startup script
112+
113+
Previously, we started the QGIS Server container in interactive mode using the `-i` option and the `/bin/bash` command. Without these options, the QGIS Server application starts normally based on the `CMD` or `ENTRYPOINT` instruction specified in the Dockerfile.
114+
115+
The startup script used, located in the container's filesystem, can be found at `/usr/local/bin/start-xvfb-nginx.sh`, and its path can be obtained by inspecting the image.
116+
117+
```bash title="Inspecting the image to locate the startup script"
118+
# Retrieving the startup script path
119+
$ docker inspect -f '{{.Config.Cmd}}' qgis/qgis-server:ltr
120+
[/bin/sh -c /usr/local/bin/start-xvfb-nginx.sh]
121+
122+
# Displaying the contents of the startup script
123+
$ docker run qgis/qgis-server:ltr cat /usr/local/bin/start-xvfb-nginx.sh
124+
```
125+
126+
By examining the contents of this script, we can observe the startup sequence of the third-party utilities mentioned above:
127+
128+
- the graphical server `Xvfb`
129+
- `spawn-fcgi`, which launches QGIS Server while specifying TCP port `9993` for communication between the web server and the FCGI process
130+
- `NGINX`, if needed, depending on the `SKIP_NGINX` environment variable set at runtime
131+
132+
```bash title="Retrieving the path of the startup script"
133+
[...]
134+
/usr/bin/Xvfb :99 -ac -screen 0 1280x1024x16 +extension GLX +render -noreset >/dev/null &
135+
XVFB_PID=$(waitfor /usr/bin/Xvfb)
136+
137+
if [ -z "$SKIP_NGINX" ] || [ "$SKIP_NGINX" == "false" ] || [ "$SKIP_NGINX" == "0" ]; then
138+
nginx
139+
NGINX_PID=$(waitfor /usr/sbin/nginx)
140+
fi
141+
142+
spawn-fcgi -n -u ${QGIS_USER:-www-data} -g ${QGIS_USER:-www-data} -d ${HOME:-/var/lib/qgis} -P /run/qgis.pid -p 9993 -- /usr/lib/cgi-bin/qgis_mapserv.fcgi &
143+
[...]
144+
```
145+
146+
### NGINX configuration
147+
148+
![logo nginx](https://cdn.geotribu.fr/img/logos-icones/logiciels_librairies/nginx.svg){: .img-thumbnail-left }
149+
150+
The NGINX web server is configured by substituting its default configuration file `/etc/nginx/nginx.conf` with a customized one to ensure that the server behaves as desired.
151+
152+
```bash title="Displaying the contents of the NGINX configuration file"
153+
$ docker run -it qgis/qgis-server:ltr cat /etc/nginx/nginx.conf
154+
[...]
155+
location /ogc/ {
156+
rewrite ^/ogc/(.*)$ /qgis/qgis_mapserv.fcgi?map=/io/data/$1/$1.qgs;
157+
}
158+
# Direct access without map rewrite
159+
location /ows/ {
160+
rewrite ^/ows/$ /qgis/qgis_mapserv.fcgi;
161+
}
162+
location /wfs3/ {
163+
rewrite ^/wfs3/(.*)$ /qgis/qgis_mapserv.fcgi;
164+
}
165+
location /qgis/ {
166+
internal; # Used only by the OGC rewrite
167+
root /var/www/data;
168+
fastcgi_pass localhost:9993;
169+
[...]
170+
```
171+
172+
In this configuration, we distinguish three public entry points and one internal entry point:
173+
174+
- Access via `/ogc/my_project`, which specifically expects a QGIS project located at `/io/data/my_project/my_project.qgs`.
175+
- Access via `/ows/` and `/wfs3/`.
176+
- An internal access point via `/qgis`, used by the other endpoints, enabling communication with the FCGI process via the socket `localhost:9993`.
177+
178+
Entry point number 1 therefore requires mounting the `/io/data/` directory to work with a dedicated directory for each project.
179+
180+
## Container startup
181+
182+
Now that we've explored the features of our image, we can test the different launch configurations.
183+
184+
### Default configuration
185+
186+
It is possible to start the QGIS Server container with the default configuration using the parameters below:
187+
188+
- Redirecting local port `8080` to port `80` of the container's web server (`-p` option).
189+
- Mounting the QGIS project directory to `/io/data` inside the container (`-v` option).
190+
191+
```bash title="Starting a QGIS Server container"
192+
# Cloning the QGIS QGIS-Training-Data repository
193+
git clone https://github.com/qgis/QGIS-Training-Data
194+
195+
# Preparing the world/world.qgs project directory for use as a mount point
196+
# /io/data
197+
cp -r QGIS-Training-Data/exercise_data/qgis-server-tutorial-data/ \
198+
QGIS-Training-Data/exercise_data/world
199+
200+
# Starting a container by mounting the QGIS project directory from the tutorial
201+
docker run \
202+
-v ./QGIS-Training-Data/exercise_data/:/io/data \
203+
-p 8080:80 \
204+
qgis/qgis-server:ltr
205+
```
206+
207+
Once the container is deployed, it is possible to send requests to QGIS Server through the NGINX entry points described in the previous section.
208+
209+
```bash title="Entry points /ogc, /ows and /wfs3"
210+
# WMS request to /ogc on the NGINX web server of the container to access the project
211+
# /io/data/world/world.qgs
212+
curl "http://localhost:8080/ogc/world?SERVICE=WMS&REQUEST=GetCapabilities"
213+
214+
# WMS request to /ows, explicitly specifying the project path via the MAP parameter
215+
curl "http://localhost:8080/ows/?MAP=/io/data/qgis-server-tutorial-data/world.qgs&SERVICE=WMS&REQUEST=GetCapabilities"
216+
217+
# OGC API Features request to /wfs3, explicitly specifying the project path via the
218+
# MAP parameter
219+
curl "http://localhost:8080/wfs3/collections.json?MAP=/io/data/qgis-server-tutorial-data/world.qgs"
220+
```
221+
222+
The OGC API Features protocol, also known as WFS3, also provides HTML rendering, allowing direct access via your browser to a web page for exploring the underlying data.
223+
224+
```bash title="URL of an HTML rendering page for OGC API Features"
225+
http://localhost:8080/wfs3/collections/countries/items/65.html?MAP=/io/data/qgis-server-tutorial-data/world.qgs
226+
```
227+
228+
![OGC API Features Landing Page](https://cdn.geotribu.fr/img/articles-blog-rdp/articles/2025/qgis_server_docker/ogcapif.png "OGC API Feeatures Landing Page"){: .img-center loading=lazy }
229+
230+
### Composition with external NGINX
231+
232+
![logo docker compose](https://cdn.geotribu.fr/img/logos-icones/logiciels_librairies/docker-compose.png){: .img-thumbnail-left }
233+
234+
As mentioned earlier, an environment variable `SKIP_NGINX` allows using the QGIS Server container without the integrated web server. In this case, the QGIS Server container operates solely as a graphical rendering `backend`. It is then possible to use composition to create a multi-container application with:
235+
236+
- A QGIS Server container for graphical rendering.
237+
- An NGINX container as the web server that redirects requests to the FCGI process via socket `9993`.
238+
239+
First, the NGINX configuration file describes an access point and how to communicate with QGIS Server:
240+
241+
```Nginx title="NGINX configuration file nginx.conf"
242+
events {
243+
worker_connections 1024;
244+
}
245+
246+
http {
247+
upstream qgis-fcgi {
248+
server qgis-server:9993;
249+
}
250+
server {
251+
location /qgisserver/ {
252+
fastcgi_pass qgis-fcgi;
253+
fastcgi_param QUERY_STRING $query_string;
254+
include fastcgi_params;
255+
}
256+
}
257+
}
258+
```
259+
260+
Next, a configuration file for the `docker compose` tool must be written to describe our multi-container application:
261+
262+
```yml title="docker-compose.yml configuration file"
263+
services:
264+
nginx:
265+
image: "nginx"
266+
volumes:
267+
# mounting the NGINX configuration file into the container
268+
- ./nginx.conf:/etc/nginx/nginx.conf
269+
ports:
270+
- "8080:80"
271+
depends_on:
272+
- qgis-server
273+
qgis-server:
274+
image: "qgis/qgis-server:ltr"
275+
environment:
276+
# disabling the internal NGINX server
277+
SKIP_NGINX: "true"
278+
volumes:
279+
# mounting the project directory
280+
- ./QGIS-Training-Data/exercise_data/:/io/data
281+
```
282+
283+
!!! warning
284+
The configuration scripts above are intentionally simplified for the reader’s understanding and should not be used in a production environment :bomb:
285+
286+
Finally, we just need to run our containers using the `docker compose` command, which will automatically read the configuration file named `docker-compose.yml` located in the current directory:
287+
288+
```bash
289+
# Running the containers in detached mode
290+
$ docker compose up -d
291+
[+] Running 3/3
292+
✔ Network tmp_default Created
293+
✔ Container tmp-qgis-server-1 Started
294+
✔ Container tmp-nginx-1 Started
295+
296+
# Status of the running NGINX and QGIS Server containers
297+
$ docker ps
298+
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
299+
30dce3dd878f nginx "/docker-entrypoint.…" 3 seconds ago Up 2 seconds 0.0.0.0:8081->80/tcp, [::]:8081->80/tcp test-nginx-1
300+
8b73de48d754 qgis/qgis-server:ltr "/bin/sh -c /usr/loc…" 3 seconds ago Up 2 seconds 80/tcp, 9993/tcp test-qgis-server-1
301+
302+
# GET request to /qgisserver, explicitly specifying the project path using the MAP parameter
303+
$ curl "http://localhost:8080/qgisserver/?MAP=/io/data/qgis-server-tutorial-data/world.qgs&SERVICE=WMS&REQUEST=GetCapabilities"
304+
```
305+
306+
## What about the plugins?
307+
308+
![logo pyqgis](https://cdn.geotribu.fr/img/logos-icones/programmation/pyqgis.png){: .img-thumbnail-left }
309+
310+
Since the beginning of this article, we’ve had fun (for sure! :sparkles:!) exploring the official QGIS Server image through some reverse engineering. However, it is also possible to refer to the [documentation](https://github.com/qgis/qgis-docker/blob/main/server/README.md) or examine the [Dockerfile](https://github.com/qgis/qgis-docker/blob/main/server/Dockerfile) used to build this image.
311+
312+
Looking at this file more closely, we can see the presence of the instruction `ENV QGIS_PLUGINPATH /io/plugins`. This implies that QGIS Server expects to have Python plugins in the specified directory. To test this mechanism, the [wfsOutputExtension](https://plugins.qgis.org/plugins/wfsOutputExtension/) plugin from [3Liz](https://www.3liz.com/) can be deployed:
313+
314+
```bash title="Deployment of the wfsOutputExtension plugin"
315+
# Creating a dedicated directory for plugins
316+
mkdir plugins
317+
318+
# Cloning the wfsOutputExtension server plugin
319+
git clone https://github.com/3liz/qgis-wfsOutputExtension plugins
320+
321+
# Starting a container by mounting the QGIS project and plugin directories
322+
docker run \
323+
-v ./QGIS-Training-Data/exercise_data/:/io/data \
324+
-v ./plugins/:/io/plugins \
325+
-p 8080:80 \
326+
qgis/qgis-server:ltr
327+
```
328+
329+
Thanks to the `wfsOutputExtension` plugin, it is possible to specify various additional formats through the `OUTPUTFORMAT` parameter of the WFS `GetFeature` request. For example, we can specify the `csv` format, which is not natively supported by QGIS Server:
330+
331+
```bash title="Executing a WFS GetFeature request"
332+
$ curl "http://localhost:8080/ows/?MAP=/io/data/qgis-server-tutorial-data/world.qgs&SERVICE=WFS&REQUEST=GetFeature&TYPENAME=countries&FEATUREID=countries.1&OUTPUTFORMAT=csv"
333+
gml_id,id,name
334+
countries.1,1,Antigua and Barbuda
335+
```
336+
337+
## Conclusion
338+
339+
The official QGIS Server Docker image greatly simplifies the deployment of this map server solution, offering a ready-to-use configuration that is easily adaptable. With Docker, deploying QGIS Server becomes easy, portable, and isolated, without worrying about complex dependencies like graphic servers or web servers.
340+
341+
By integrating `NGINX`, `Xvfb`, and `FastCGI`, the image ensures smooth operation of QGIS Server in a containerized environment. It also provides the option to use an external web server, like NGINX, to separate functions and have better control over configurations
342+
343+
<!-- geotribu:authors-block -->

mkdocs-free.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ markdown_extensions:
113113
# Admonition - https://squidfunk.github.io/mkdocs-material/extensions/admonition/
114114
- admonition
115115
- attr_list
116+
# Définitions - https://squidfunk.github.io/mkdocs-material/setup/extensions/python-markdown/#definition-lists
117+
- def_list
116118
# Footnotes - https://squidfunk.github.io/mkdocs-material/reference/footnotes/
117119
- footnotes
118120
- md_in_html

mkdocs.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@ markdown_extensions:
147147
# Admonition - https://squidfunk.github.io/mkdocs-material/extensions/admonition/
148148
- admonition
149149
- attr_list
150+
# Définitions - https://squidfunk.github.io/mkdocs-material/setup/extensions/python-markdown/#definition-lists
151+
- def_list
150152
# Footnotes - https://squidfunk.github.io/mkdocs-material/reference/footnotes/
151153
- footnotes
152154
- md_in_html

0 commit comments

Comments
 (0)