Applications Azure

go-pmtiles on Ubuntu 24.04 on Azure User Guide

| Product: go-pmtiles on Ubuntu 24.04 LTS on Azure

Overview

go-pmtiles is Protomaps' open source, single binary map tile server. pmtiles serve reads a directory of PMTiles archives and serves standard Z/X/Y vector map tiles over HTTP at /<archive>/{z}/{x}/{y}.mvt, together with TileJSON at /<archive>.json. A PMTiles archive is a single file that holds an entire tile pyramid, so a whole basemap is one file with no tile database, no tile cache and no external services to run.

The server is a single self contained native Go binary. It runs behind nginx as a reverse proxy on port 80. The tile server listens on 127.0.0.1:8080 and binds the loopback interface only, with its admin port disabled, so the only public surface is nginx.

go-pmtiles is a public by design, read only service: it exposes only tile and TileJSON GET endpoints. There is no login, no administrator account and no mutating, upload or admin endpoint, so no credentials ship in the image or are generated at first boot. On the first boot of every deployed VM a one shot service resolves the VM public address, writes an instance info file to /root/go-pmtiles-info.txt, records the bootstrap ready marker, and starts the tile server and the front door. The tile server unit is gated on that marker, so it never serves before first boot has completed.

To make the image useful out of the box, cloudimg bundles a fully self contained MapLibre GL JS web map viewer, served locally by nginx, that renders the shipped basemap in the browser with no external CDN.

The bundled MapLibre web viewer rendering the central London OpenStreetMap basemap served by go-pmtiles

What is included:

  • go-pmtiles 1.31.2 server (single native Go binary) at /var/lib/pmtiles/pmtiles
  • nginx reverse proxy on :80 in front of the tile server on loopback :8080, with version disclosure suppressed and permissive CORS on the read only tile endpoints
  • A self contained MapLibre GL JS web map viewer served at / (MapLibre GL JS, the Protomaps light theme, Noto Sans glyphs and Protomaps sprites, all bundled locally)
  • A ready to explore sample basemap baked into the image: a central London PMTiles extract (zoom 0 to 15) cut from the Protomaps global OpenStreetMap basemap (© OpenStreetMap contributors, Open Database License ODbL), so a map renders on first boot
  • pmtiles.service and nginx.service as systemd units, enabled and active, plus a go-pmtiles-firstboot.service one shot that runs once per VM
  • The tile server runs as a dedicated non root pmtiles service account with no shell and no sudo
  • A fully patched Ubuntu 24.04 LTS security baseline at capture time, with unattended security updates enabled
  • 24/7 cloudimg support

Key facts: platform Ubuntu 24.04 LTS on Azure, default SSH user azureuser, application home /var/lib/pmtiles, web viewer and tile API on port 80, tile server loopback on 8080.

Prerequisites

An active Azure subscription, an SSH key pair, and a VNet plus subnet in the target region. Standard_B2s (2 vCPU / 4 GiB RAM) is a good starting point: go-pmtiles range reads tiles from the archive with a small in memory cache, so serving needs little memory even for large archives. Size the OS disk to the archives you host. NSG inbound: allow 22/tcp from your management network and 80/tcp (plus 443/tcp once you place your own TLS terminator in front) from the networks that will consume your tiles.

Step 1 — Deploy from the Azure Marketplace

Sign in to the Azure Portal, choose Create a resource, search the Marketplace for go-pmtiles 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). Then Review + create and Create. First boot initialisation takes a few seconds after the VM starts.

Step 2 — Deploy from the Azure CLI

az vm create \
  --resource-group <your-rg> \
  --name go-pmtiles \
  --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 go-pmtiles --port 80 --priority 1010

Step 3 — Connect to your VM

ssh azureuser@<vm-public-ip>

Look up the public IP at any time with az vm show -d -g <your-rg> -n go-pmtiles --query publicIps -o tsv.

Step 4 — Verify the go-pmtiles stack

Two systemd services make up the stack. Confirm both are active and that the tile server binds loopback only while nginx serves the public port:

systemctl is-active pmtiles nginx
ss -tln | grep -E ':80 |:8080 '

Both lines read active, and the socket list shows nginx on 0.0.0.0:80 with the tile server bound to 127.0.0.1:8080 only:

active
active
LISTEN 0      511          0.0.0.0:80        0.0.0.0:*
LISTEN 0      4096       127.0.0.1:8080      0.0.0.0:*
LISTEN 0      511             [::]:80           [::]:*

The two systemd services active and the go-pmtiles server bound to loopback only

Step 5 — Open the web map viewer

Browse to http://<vm-public-ip>/. The bundled MapLibre viewer opens and renders the central London basemap served by go-pmtiles, with pan, zoom, a navigation control and a scale bar. Zoom in to street level to see roads, buildings, parks, water and place labels drawn from the vector tiles:

The web viewer zoomed to street level around Trafalgar Square

The same tiles render anywhere in the sample extent; here the map is centred on the Thames and the City of London:

The web viewer showing the Thames and the City of London

Step 6 — Request a real map tile over HTTP

The tiles the viewer draws are standard Z/X/Y vector tiles you can fetch directly. Each tile is a gzip compressed Mapbox Vector Tile (.mvt). Request one and decode it to confirm it carries real map geometry:

curl -s -D - -o tile.mvt http://127.0.0.1/tiles/basemap/14/8186/5448.mvt | grep -iE '^(HTTP|content-type|content-encoding)'
echo "wire bytes (gzip): $(stat -c%s tile.mvt)"
echo "decoded MVT bytes: $(gzip -dc tile.mvt | wc -c)"

The server returns a protobuf vector tile, gzip encoded, that decodes to over a hundred kilobytes of real geometry:

HTTP/1.1 200 OK
Content-Type: application/x-protobuf
Content-Encoding: gzip
wire bytes (gzip): 83272
decoded MVT bytes: 119916

A real z/x/y map tile served over HTTP, decoded to Mapbox Vector Tile bytes

Step 7 — TileJSON and the per VM instance info

go-pmtiles publishes a TileJSON document that map clients (MapLibre, Leaflet, OpenLayers) use to discover the tile URL template, zoom range and bounds:

curl -s http://127.0.0.1/tiles/basemap.json | jq '{tilejson,name,minzoom,maxzoom,bounds,tiles}'

The tiles URL is stamped with this VM's own public address, resolved at first boot:

{
  "tilejson": "3.0.0",
  "name": "Protomaps Basemap",
  "minzoom": 0,
  "maxzoom": 15,
  "bounds": [-0.18, 51.47, -0.05, 51.54],
  "tiles": ["http://<vm-public-ip>/tiles/basemap/{z}/{x}/{y}.mvt"]
}

Every VM writes an instance info file on first boot with its own reachable URLs and the read only, public by design note:

sudo cat /root/go-pmtiles-info.txt

TileJSON served by go-pmtiles and the per VM instance info file

The raw PMTiles archive is also exposed for clients that read it directly with the PMTiles HTTP range protocol (for example the pmtiles JavaScript library). It supports byte range requests, so a client fetches only the header and the tiles it needs:

curl -s -r 0-6 -o /dev/null -w 'range status: HTTP %{http_code}\n' http://127.0.0.1/data/basemap.pmtiles

A 206 Partial Content confirms range serving is enabled.

Step 8 — The sample basemap and its licenses

The image ships a small basemap so the viewer works out of the box. The attribution and licensing are recorded on the VM:

cat /var/lib/pmtiles/DATA-LICENSES.txt

The map data is an extract of OpenStreetMap, © OpenStreetMap contributors, licensed under the Open Database License (ODbL); keep this attribution whenever you serve or redisplay the tiles. The web viewer components (MapLibre GL JS, the Protomaps theme and sprites, and the Noto Sans fonts) are bundled under their own permissive licenses (BSD-3-Clause and the SIL Open Font License).

The data license and attribution notice shipped with the sample basemap

Step 9 — Serve your own area

To serve your own region, replace the sample archive with a PMTiles file of your area. The pmtiles binary can cut a bounded extract straight from the Protomaps global basemap using range requests, so you download only the region you need. This is a one time build step:

# Cut an extract of your area from the Protomaps global OpenStreetMap basemap
# (pick a recent daily build from https://maps.protomaps.com/builds/):
cd /var/lib/pmtiles/tiles
sudo -u pmtiles /var/lib/pmtiles/pmtiles extract \
  https://build.protomaps.com/<YYYYMMDD>.pmtiles your-area.pmtiles \
  --bbox=<min_lon>,<min_lat>,<max_lon>,<max_lat> --maxzoom=15

# The server serves every .pmtiles file in the directory as /<name>/{z}/{x}/{y}.mvt.
# Point the viewer at your archive, or drop in your own, then restart:
sudo systemctl restart pmtiles

You can also build PMTiles from your own data with pmtiles convert (from an MBTiles file) or with the Protomaps basemaps tooling. Each additional zoom level roughly doubles the archive size, so choose --maxzoom to match how far your users zoom in.

Server Components

Component Version Path
go-pmtiles server (single Go binary) 1.31.2 /var/lib/pmtiles/pmtiles
nginx reverse proxy distribution /etc/nginx/sites-available/cloudimg-go-pmtiles
Sample basemap (PMTiles archive) central London z0-15 /var/lib/pmtiles/tiles/basemap.pmtiles
Bundled MapLibre web viewer MapLibre GL JS 5.24.0 /var/lib/pmtiles/viewer/

Filesystem Layout

Path Description
/ Root filesystem
/var/lib/pmtiles go-pmtiles home: binary, licence, served archives and bundled web viewer
/var/lib/pmtiles/tiles Directory of PMTiles archives served as /<name>/{z}/{x}/{y}.mvt
/var/lib/pmtiles/viewer Self contained MapLibre web viewer and vendored assets
/var/lib/pmtiles/DATA-LICENSES.txt OpenStreetMap (ODbL) and component attribution
/etc/pmtiles/pmtiles.env Per VM PMTILES_PUBLIC_URL, written at first boot
/root/go-pmtiles-info.txt Per VM instance info written at first boot
/var/lib/cloudimg/go-pmtiles-firstboot.done First boot bootstrap ready marker

Managing the service

systemctl status pmtiles --no-pager
journalctl -u pmtiles -n 50 --no-pager

Restart the tile server after you add or replace an archive with sudo systemctl restart pmtiles.

Security

go-pmtiles is deployed here as a public by design, read only tile server. Confirm the posture on your VM:

  • The tile server runs as the non root pmtiles service account with no shell and no sudo.
  • It binds 127.0.0.1:8080 only, with its admin port disabled; the sole public surface is nginx on port 80, with version disclosure suppressed.
  • There is no login, administrator account, token or mutating endpoint; only tile and TileJSON GET requests are served, and a POST to a tile path is rejected.
  • The tile server unit is gated on the first boot bootstrap ready marker, so it will not serve before first boot completes.

CORS is permissive on the read only tile endpoints, which is conventional for a public tile server. Restrict inbound 80/tcp to the networks that should consume your tiles, and place your own TLS terminator in front for HTTPS. Keep 22/tcp limited to your management network.

Troubleshooting

The map or viewer does not load. Check both services with systemctl is-active pmtiles nginx. If pmtiles is not active, inspect journalctl -u pmtiles -n 100 --no-pager. The tile server only starts once the first boot marker exists at /var/lib/cloudimg/go-pmtiles-firstboot.done.

A tile request returns 404. The path is /tiles/<archive-name>/{z}/{x}/{y}.mvt, where <archive-name> is the archive filename without the .pmtiles extension (the sample is basemap). A tile outside the archive's bounds or above its max zoom (15 for the sample) will not exist.

Labels or icons are missing when you serve your own area. The bundled viewer ships glyphs for Latin place names; areas whose labels use other scripts need the matching Noto font ranges added under /var/lib/pmtiles/viewer/assets/fonts/.

Support

For assistance, contact cloudimg support:

  • Email: support@cloudimg.co.uk
  • Response Time: 24/7 with a guaranteed 24 hour response SLA
  • Website: https://cloudimg.co.uk