TileServer GL Self-Hosted Map Tile Server on Ubuntu 24.04 on Azure User Guide
Overview
TileServer GL is a self-hosted map tile server: it serves MBTiles/PMTiles vector and raster tile sources with a built-in web map preview, styled with MapLibre GL styles, and can server-side render raster PNG tiles from vector data - so you get your own maps without a Google Maps or Mapbox API key. The cloudimg image installs the pinned official TileServer GL 5.6.0 (Node.js 22) running under systemd with the headless server-side-rendering stack (Xvfb + the native MapLibre GL renderer) already configured, ships a small sample map (OpenMapTiles' Zurich, Switzerland demo extract) on a dedicated data disk so it shows a real map immediately, and locks it down for a marketplace appliance: TileServer GL itself is bound to loopback only, and nginx does all customer-facing work on port 80 - the preview UI, TileJSON and tile-render endpoints are gated behind a per-VM HTTP Basic Auth credential generated on first boot (TileServer GL has no accounts of its own). Backed by 24/7 cloudimg support.
What is included:
- TileServer GL 5.6.0 installed via npm (Node.js 22 LTS), running as the
tileserver-glsystemd service, headless server-side rendering via Xvfb - The built-in web preview UI, TileJSON endpoints and raster tile render endpoints on
:80, fronted by nginx with TileServer GL bound to loopback only - Per-VM HTTP Basic Auth (user
admin) protecting everything, with a unique password generated on first boot - A sample map dataset (OpenMapTiles' Zurich, Switzerland extract, (c) OpenMapTiles (c) OpenStreetMap contributors) on a dedicated Azure data disk, so the preview shows a real map out of the box
tileserver-gl.service+nginx.serviceas systemd units, enabled and active- An unauthenticated
/healthzendpoint on:80for 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 a comfortable starting point for a small tileset and light preview traffic; size up the VM for larger tilesets or heavier concurrent tile-render load. NSG inbound: allow 22/tcp from your management network, 80/tcp for the preview UI and tiles, and 443/tcp if you add TLS. TileServer GL serves plain HTTP; for production use, terminate TLS in front of it with your own domain.
Step 1 - Deploy from the Azure Marketplace
Sign in to the Azure Portal, choose Create a resource, search the Marketplace for TileServer GL 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 tileserver-gl \
--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 tileserver-gl --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 tileserver-gl.service nginx.service
Both report active. TileServer GL serves the preview UI, TileJSON and tile-render endpoints on the loopback address 127.0.0.1:8080 only; nginx fronts port 80 with the per-VM HTTP Basic Auth gate. The sample map dataset lives on a dedicated 20 GiB data disk mounted at /var/lib/tileserver-gl.

Step 5 - Retrieve your preview UI password
nginx protects the preview UI and every tile/TileJSON route 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/tileserver-gl-credentials.txt
This file contains TILESERVER_USERNAME, TILESERVER_PASSWORD, the TILESERVER_URL to open in a browser, the discovered TILESERVER_STYLE_ID (the bundled MapLibre GL style TileServer GL registered for the sample tileset), and a ready-to-use TILESERVER_SAMPLE_TILE_URL. The password is stored on disk only as a bcrypt hash in /etc/nginx/.tileserver-gl.htpasswd, so no plaintext password ships in the image. Store the password somewhere safe.

Step 6 - Confirm the health endpoint
nginx serves an unauthenticated health endpoint for load balancers and probes:
curl -s http://localhost/healthz
It returns ok. This endpoint never requires authentication, so it is safe for an Azure Load Balancer health probe.
Step 7 - Confirm authentication on the preview/tile path
Because a password is set on first boot, an unauthenticated request returns HTTP 401, so nobody reaches the map without the password. 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 the correct password authenticates:
PW=$(sudo grep '^TILESERVER_PASSWORD=' /root/tileserver-gl-credentials.txt | cut -d= -f2-)
echo "unauth : $(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1/)"
echo "wrongpw : $(curl -s -o /dev/null -w '%{http_code}' -u admin:wrong-pw http://127.0.0.1/)"
echo "authed : $(curl -s -o /dev/null -w '%{http_code}' -u admin:$PW http://127.0.0.1/)"
It prints unauth : 401, wrongpw : 401, authed : 200. Only the per-VM password reaches the preview UI, TileJSON and tile-render endpoints, because TileServer GL itself is bound to loopback and nginx port 80 is the only way in.

Step 8 - Fetch a rendered map tile
TileServer GL server-side renders raster PNG tiles from the sample vector tileset on demand. The credentials file already has a ready-to-use URL for a populated tile over Zurich; the following fetches it and confirms it is a real PNG image:
PW=$(sudo grep '^TILESERVER_PASSWORD=' /root/tileserver-gl-credentials.txt | cut -d= -f2-)
STYLE=$(grep '^TILESERVER_STYLE_ID=' /etc/tileserver-gl/tileserver-gl.env | cut -d= -f2-)
curl -s -o /tmp/sample-tile.png -w 'HTTP %{http_code}, %{size_download} bytes\n' \
-u admin:$PW "http://127.0.0.1/styles/${STYLE}/256/10/536/359.png"
file /tmp/sample-tile.png
It prints HTTP 200, 71806 bytes and PNG image data, 256 x 256, 8-bit/color RGBA, non-interlaced - a real rendered 256x256 map tile over Zurich, Switzerland, decoded from the vector sample data.

Step 9 - Sign in and view the sample map
Browse to http://<vm-public-ip>/. Your browser prompts for a username and password: enter admin and the password from Step 5. TileServer GL opens on its built-in preview page, listing the registered data source (the Zurich sample) and every bundled MapLibre GL style available for it, each with a live interactive map preview.

Step 10 - Explore the interactive map
Click into a style to open its full-page interactive MapLibre GL JS map. Pan and zoom around Zurich, Switzerland to see the vector-rendered roads, buildings, water and land-use layers redraw live at every zoom level.

Step 11 - Browse the data/style listing
The preview home page (Step 9) doubles as a directory of everything TileServer GL is currently serving: the zurich_switzerland data source and every style registered against it, each linking to its own TileJSON and inspection page. This is the same listing you will see grow once you add your own MBTiles/PMTiles tilesets (see Maintenance below).

Step 12 - Inspect the TileJSON / API info page
Every data source exposes a standard TileJSON document describing its tile URL template, bounds, min/max zoom and attribution - useful for wiring the tileset into MapLibre GL JS, Leaflet, OpenLayers or any TileJSON-aware client:
PW=$(sudo grep '^TILESERVER_PASSWORD=' /root/tileserver-gl-credentials.txt | cut -d= -f2-)
curl -s -u admin:$PW "http://127.0.0.1/data/v3.json" | python3 -m json.tool

Maintenance
- Password: the preview/tile password is set on first boot and stored as a bcrypt entry in
/etc/nginx/.tileserver-gl.htpasswd. To change it, runsudo htpasswd -B /etc/nginx/.tileserver-gl.htpasswd adminand thensudo systemctl reload nginx. - Adding your own tilesets: copy your MBTiles or PMTiles file into
/var/lib/tileserver-gl/, then either point the service at it directly (edit/etc/systemd/system/tileserver-gl.service'sExecStartto add--file /var/lib/tileserver-gl/<your-file>.mbtiles, or switch to aconfig.jsonreferencing multiple sources - see the TileServer GL documentation) andsudo systemctl daemon-reload && sudo systemctl restart tileserver-gl. No third-party maps API key is required for your own data. - Sample data attribution: the bundled demo tileset is OpenMapTiles' Zurich, Switzerland extract: (c) OpenMapTiles (c) OpenStreetMap contributors. Keep this attribution visible if you keep the sample tileset in a public-facing deployment.
- Restrict access: TileServer GL serves plain HTTP on port 80. For production, restrict access to trusted IP ranges in your Network Security Group, and front it with TLS (for example certbot with your own domain) terminating on
:443. - Loopback binding: TileServer GL is bound to
127.0.0.1:8080(--bind 127.0.0.1in/etc/systemd/system/tileserver-gl.service), so nginx is the only path in. Keep it that way - do not change the bind address to a public interface. - Server-side rendering: the raster tile-render endpoints run under
xvfb-run(an off-screen X server) so MapLibre GL Native's renderer works with no GPU. This is invisible in normal operation; if you see rendering errors after adding a very large or unusual style, checkjournalctl -u tileserver-gl. - Data disk: the sample tileset and any tilesets you add live on the dedicated data disk at
/var/lib/tileserver-gl. Resize the disk in the Azure Portal (Disks tab) as your own datasets grow. - 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.