Tg
Application Infrastructure Azure

TileServer GL Self-Hosted Map Tile Server on Ubuntu 24.04 on Azure User Guide

| Product: TileServer GL on Ubuntu 24.04 LTS on Azure

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-gl systemd 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.service as systemd units, enabled and active
  • An unauthenticated /healthz endpoint on :80 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 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.

The tileserver-gl and nginx services active, TileServer GL listening on loopback 127.0.0.1:8080 only, and the dedicated 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.

The per-VM credentials file with the generated admin password, the preview URL, the discovered style id and a ready-to-use sample tile URL

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.

The HTTP Basic Auth round-trip on port 80 returning 401 unauthenticated, 401 for a wrong password, and 200 with the per-VM password serving the TileServer GL preview UI

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.

Fetching a server-side-rendered raster tile from the sample Zurich dataset and confirming it is a valid 256x256 PNG image

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.

The TileServer GL preview UI home page listing the Zurich sample data source and the bundled MapLibre GL styles registered against it, behind the HTTP Basic Auth prompt

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.

An interactive MapLibre GL JS map view zoomed into central Zurich, showing rendered roads, buildings and water from the sample vector tileset

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).

The TileServer GL data and styles listing page enumerating the registered Zurich data source and its associated MapLibre GL styles

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

The TileJSON document for the sample data source, showing its tile URL template, bounds, zoom range and OpenMapTiles/OpenStreetMap attribution, viewed in the browser

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, run sudo htpasswd -B /etc/nginx/.tileserver-gl.htpasswd admin and then sudo 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's ExecStart to add --file /var/lib/tileserver-gl/<your-file>.mbtiles, or switch to a config.json referencing multiple sources - see the TileServer GL documentation) and sudo 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.1 in /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, check journalctl -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.