QGIS Server on Ubuntu 24.04 on Azure User Guide
Overview
QGIS Server is the map server of the QGIS project. It reads a QGIS project file and publishes the layers inside it as standards compliant OGC web services - WMS 1.3.0 and 1.1.1 for rendered map images, WFS for vector features, WCS for coverages and WMTS for cached tiles. Because it shares the rendering engine with QGIS on the desktop, the symbology, labelling and layer order a cartographer designed are exactly what clients receive over the web, with no second styling step.
QGIS Server is a FastCGI application, and it has no authentication and no user accounts of its own. Published as-is on a public IP, every layer of every project is readable by anyone. The cloudimg image closes that by design: QGIS Server runs as two persistent FastCGI worker processes that listen only on local unix sockets, and nginx on port 80 is the sole network listener and the security boundary. Every OGC request, the bundled map viewer and its assets sit behind HTTP Basic Auth (user admin) whose password is generated uniquely on the first boot of your VM. QGIS Server is installed from the official QGIS Long Term Release apt repository, so the runtime stays patchable with apt upgrade rather than being a source build you can never update. A working sample project and dataset - synthetic geometry authored by cloudimg, so its licensing is unambiguous - is published on a dedicated Azure data disk at /srv/qgis. Backed by 24/7 cloudimg support.
What is included:
- QGIS Server 3.44 LTR installed from the official QGIS apt repository (
qgis.org/ubuntu-ltr), verified at build time to have come from that repository - Two persistent FastCGI workers (
qgis-server@1,qgis-server@2) supervised byspawn-fcgi, sized one per vCPU for the recommended Standard_B2s - OGC services - WMS, WFS, WCS and WMTS - on
/ogc/, fronted by nginx with the workers reachable only over local unix sockets - Per-VM HTTP Basic Auth (user
admin) protecting the OGC endpoint and the viewer, with a unique password generated on first boot - A bundled Leaflet map viewer at
/with a live WMS legend and click-to-identify GetFeatureInfo, served locally with no CDN dependency - A working sample QGIS project and GeoPackage, generated by cloudimg, on a dedicated Azure data disk mounted at
/srv/qgis /usr/local/sbin/qgis-server-selftest.sh, an end-to-end OGC check you can re-run at any time- An unauthenticated
/healthzendpoint for Azure Load Balancer health probes - 24/7 cloudimg support
Prerequisites
An active Azure subscription, an SSH key pair, and a VNet + subnet in the target region. Standard_B2s (2 vCPU / 4 GiB RAM) is the recommended size and is what the two rendering workers are tuned for; scale up if you publish large raster projects or expect heavy concurrent tile traffic. NSG inbound: allow 22/tcp from your management network, 80/tcp for the OGC endpoint and viewer, and 443/tcp if you add TLS. The appliance serves plain HTTP on port 80; for production, terminate TLS in front of it with your own domain and restrict access to trusted IP ranges (see Maintenance).
Step 1 - Deploy from the Azure Marketplace
Sign in to the Azure Portal, choose Create a resource, search the Marketplace for QGIS Server by cloudimg, and select Create. On Basics pick your subscription, resource group, region and size; under Administrator account choose SSH public key and paste your key; under Inbound port rules allow SSH (22) and HTTP (80). Review the dedicated data disk on the Disks tab, then Review + create -> Create.
Step 2 - Deploy from the Azure CLI
az vm create \
--resource-group <your-rg> \
--name qgis-server \
--image <marketplace-image-urn> \
--size Standard_B2s \
--admin-username azureuser \
--ssh-key-values ~/.ssh/id_ed25519.pub \
--vnet-name <your-vnet> --subnet <your-subnet> \
--public-ip-sku Standard
az vm open-port --resource-group <your-rg> --name qgis-server --port 80 --priority 1010
Step 3 - Connect to your VM
ssh azureuser@<vm-public-ip>
Step 4 - Confirm the services are running
systemctl is-active qgis-server@1.service qgis-server@2.service nginx.service
ls -l /run/qgis-server/
findmnt -no SOURCE,TARGET,FSTYPE,SIZE /srv/qgis
All three services report active. The two FastCGI workers listen on the unix sockets qgis-1.sock and qgis-2.sock, owned qgis:www-data with mode 0660 - so only the nginx worker process can reach them. There is no TCP port for QGIS Server itself, which is why nginx cannot be bypassed. Your published projects and their data live on the dedicated Azure data disk mounted at /srv/qgis.

Step 5 - Confirm the QGIS version and the sample project
cat /etc/cloudimg-qgis-server-version
apt-cache policy qgis-server | head -3
ls -l /srv/qgis/projects /srv/qgis/data
ogrinfo -so /srv/qgis/data/cloudimg-sample.gpkg cloudimg_regions | head -8
The version file records the exact QGIS Server build in this image, and apt-cache policy shows it was installed from qgis.org/ubuntu-ltr - the project's own Long Term Release repository. That matters: QGIS Server here is an ordinary apt package, so security updates arrive through apt upgrade like everything else on the system. The sample project cloudimg-sample.qgs and its GeoPackage sit on the data disk, and ogrinfo reports the cloudimg_regions layer with its nine features.

Step 6 - Retrieve your password
nginx protects the OGC endpoint and the viewer with HTTP Basic Auth. The username is admin and a unique password is generated on the first boot of your VM and written to a root-only file:
sudo cat /root/qgis-server-credentials.txt
This file contains QGIS_SERVER_USERNAME, QGIS_SERVER_PASSWORD, the QGIS_SERVER_URL to open in a browser and the QGIS_SERVER_OGC_URL to give to OGC clients. The password is stored on disk only as a bcrypt hash in /etc/nginx/.qgis-server.htpasswd, so no plaintext password ships in the image and no two VMs share a login. Store the password somewhere safe.
Step 7 - Confirm the health endpoint
curl -s http://localhost/healthz
It returns ok. This endpoint is deliberately the only unauthenticated path on the VM, so it is safe to use for an Azure Load Balancer health probe without exposing any map data.
Step 8 - Confirm the authentication gate
Because QGIS Server has no access control of its own, the nginx gate is what stands between your data and the internet. The following reads the per-VM password from the credentials file and proves the round-trip - unauthenticated is rejected, a wrong password is rejected, and only the correct password reaches the service:
PW=$(sudo grep '^QGIS_SERVER_PASSWORD=' /root/qgis-server-credentials.txt | cut -d= -f2-)
CAPQ='SERVICE=WMS&VERSION=1.3.0&REQUEST=GetCapabilities'
echo "unauth : $(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1/ogc/?$CAPQ")"
echo "wrongpw : $(curl -s -o /dev/null -w '%{http_code}' -u admin:wrong-pw "http://127.0.0.1/ogc/?$CAPQ")"
echo "authed : $(curl -s -o /dev/null -w '%{http_code}' -u admin:$PW "http://127.0.0.1/ogc/?$CAPQ")"
It prints unauth : 401, wrongpw : 401 and authed : 200. The same gate covers GetMap, GetFeatureInfo, WFS and the viewer - not just GetCapabilities - so there is no unauthenticated path to any layer.

Step 9 - Confirm a real OGC round-trip
A map server that returns HTTP 200 is not necessarily a map server that draws anything: a blank tile is still a perfectly valid PNG. The image ships a self-test that checks the content, not just the status code. It asserts that GetCapabilities advertises both sample layers by name, that a GetMap request returns a PNG that is genuinely not blank, that GetFeatureInfo identifies a feature, that WFS returns features, and that unauthenticated and wrong-password requests are refused:
sudo /usr/local/sbin/qgis-server-selftest.sh
It prints OK. If anything is wrong it prints BROKEN-<reason> instead and exits non-zero, so it is safe to wire into your own monitoring. You can also see the evidence by hand:
PW=$(sudo grep '^QGIS_SERVER_PASSWORD=' /root/qgis-server-credentials.txt | cut -d= -f2-)
Q='SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&LAYERS=cloudimg_regions,cloudimg_sites&STYLES=&CRS=EPSG:3857&WIDTH=800&HEIGHT=800&FORMAT=image/png'
curl -s -o /tmp/map.png -u admin:$PW "http://127.0.0.1/ogc/?$Q&BBOX=-334000,6106000,668000,7761000"
file /tmp/map.png
gdalinfo -stats /tmp/map.png | grep -E 'Size is|StdDev'
file reports an 800x800 PNG and gdalinfo reports a non-zero standard deviation on each band - that is the proof pixels actually vary, i.e. a map was drawn. Render the same request over an area where the project has no data and every band reports StdDev=0.000, which is what a blank tile looks like.

Step 10 - Open the map viewer
Browse to http://<vm-public-ip>/. Your browser prompts for a username and password: enter admin and the password from Step 6. The bundled viewer loads the sample project through WMS and draws it, with a legend generated live by QGIS Server from the project's own symbology using GetLegendGraphic - so the legend always matches whatever project you publish. Leaflet is served from the VM itself, not from a CDN, so the viewer works on locked-down networks with no outbound internet access.

Step 11 - Zoom in and watch the server re-render
Use the + control or the scroll wheel to zoom in. Every pan and zoom issues fresh WMS GetMap requests, and QGIS Server re-renders the layers at the new scale from the source data - this is server side rendering, not a pre-cut tile cache, so scale dependent styling and labelling behave exactly as they do in QGIS Desktop.

Step 12 - Identify a feature with GetFeatureInfo
Click anywhere on the map. The viewer issues a WMS GetFeatureInfo request - the standard OGC identify operation - and shows the attributes of the feature under your cursor. This is the same request QGIS Desktop, OpenLayers or any other OGC client makes, so if identify works here it works for your clients too.

Step 13 - Connect QGIS Desktop and other OGC clients
Any OGC client can consume this server. In QGIS Desktop choose Layer -> Add Layer -> Add WMS/WMTS Layer, create a new connection with the URL http://<vm-public-ip>/ogc/, set the authentication to Basic with username admin and your per-VM password, then connect and add the layers. The same URL works for Add WFS Layer. Browsing the capabilities document in a browser shows exactly what any client sees - the service metadata, the supported operations and formats, and the published layers:
PW=$(sudo grep '^QGIS_SERVER_PASSWORD=' /root/qgis-server-credentials.txt | cut -d= -f2-)
curl -s -u admin:$PW 'http://127.0.0.1/ogc/?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetCapabilities' | grep -oE '<Name>[^<]*</Name>'
curl -s -u admin:$PW 'http://127.0.0.1/ogc/?SERVICE=WFS&VERSION=1.1.0&REQUEST=GetFeature&TYPENAME=cloudimg_regions&MAXFEATURES=1&OUTPUTFORMAT=GeoJSON'
The first command lists the published layer names, the second returns a GeoJSON feature over WFS. Note that the capabilities document advertises itself at whatever address you used to reach it - the appliance derives that from the request, so there is no hostname baked into the project and nothing to reconfigure when you put the VM behind a domain or load balancer.

Step 14 - Publish your own project
The sample project is there so the server does something useful out of the box; replace it with your own. Copy your .qgs (or .qgz) project and all of its data onto the data disk, point the appliance at it, and reload:
# from your workstation
scp -r ./my-project azureuser@<vm-public-ip>:/tmp/
# on the VM
sudo mv /tmp/my-project /srv/qgis/projects/
sudo chown -R qgis:www-data /srv/qgis
sudo sed -i 's|fastcgi_param QGIS_PROJECT_FILE .*|fastcgi_param QGIS_PROJECT_FILE /srv/qgis/projects/my-project/my-project.qgs;|' \
/etc/nginx/sites-available/cloudimg-qgis
sudo nginx -t && sudo systemctl reload nginx
sudo systemctl restart qgis-server@1 qgis-server@2
Use absolute paths inside the project, or relative paths with the data stored alongside the .qgs file, so the workers can resolve every layer. In QGIS Desktop, Project -> Properties -> QGIS Server is where you set the WMS service title, the advertised extent, the CRS list and which layers are published over WFS and WCS - those settings are read straight out of the project file by QGIS Server. /usr/local/lib/cloudimg/make-sample-project.py shows the same settings applied programmatically with PyQGIS if you would rather generate projects than author them by hand.
Maintenance
- Password: the password is set on first boot and stored as a bcrypt entry in
/etc/nginx/.qgis-server.htpasswd. To change it, runsudo htpasswd -B /etc/nginx/.qgis-server.htpasswd adminand thensudo systemctl reload nginx. - Restrict access: the appliance serves plain HTTP on port 80. For production, restrict port 80 to trusted IP ranges in your Network Security Group and front it with TLS (for example certbot with your own domain) terminating on
:443. HTTP Basic Auth sends the password with every request, so TLS is strongly recommended on any network you do not fully control. - Scaling the workers: each worker holds the rendering stack plus the cached project in memory, so the image runs two of them - one per vCPU on Standard_B2s. On a larger VM, add instances (
sudo systemctl enable --now qgis-server@3) and add a matchingserver unix:/run/qgis-server/qgis-3.sock max_fails=3 fail_timeout=10s;line to theupstream qgisserverblock in/etc/nginx/sites-available/cloudimg-qgis, then reload nginx. - Tuning rendering:
QGIS_SERVER_MAX_THREADS,QGIS_SERVER_PARALLEL_RENDERINGandQGIS_SERVER_CACHE_SIZEare set in/etc/systemd/system/qgis-server@.service. They are deliberately conservative for 4 GiB of RAM; raise them only alongside more RAM, andsudo systemctl daemon-reloadthen restart the workers after any change. - Logs: QGIS Server logs to the journal at level 2 -
sudo journalctl -u 'qgis-server@*' -fwhile you issue requests is the fastest way to debug a project that will not load. nginx access and error logs are in/var/log/nginx/. - Self-test: re-run
sudo /usr/local/sbin/qgis-server-selftest.shafter any change to your project or nginx configuration; it exits non-zero with a reason if the OGC round-trip or the auth gate stops working. - Storage: every published project and its data live under
/srv/qgison the dedicated data disk; back up that volume to protect your work. - Upgrades: QGIS Server is an apt package from
qgis.org/ubuntu-ltr, sosudo apt update && sudo apt upgradekeeps it current within the Long Term Release series. Restart the workers afterwards. - Security patches: unattended-upgrades remains enabled so the OS continues to receive security updates automatically.
Support
cloudimg provides 24/7 expert support for this image. Contact support@cloudimg.co.uk.