go-pmtiles
Overview
go-pmtiles is the open source, single-binary map tile server from Protomaps. 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. There is no tile database, no tile cache and no external service to run. This SecureAnyCloud image adds nginx in front, a bundled web map viewer and a sample basemap, so a map renders on first boot. The tile server listens on 127.0.0.1:8080 behind nginx.
go-pmtiles is a public-by-design, read-only service. It exposes only tile and TileJSON GET endpoints, with no login, no administrator account and no mutating, upload or admin endpoint. No credentials ship in the image or are generated at first boot.
What you get:
- go-pmtiles 1.31.2 server, a single native Go binary, at /var/lib/pmtiles/pmtiles
- An nginx reverse proxy on
:80in 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 / by nginx, with no external CDN. MapLibre GL JS, the Protomaps light theme, Noto Sans glyphs and Protomaps sprites are all bundled locally
- A 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)
pmtiles.serviceandnginx.serviceas enabled and active systemd units, plusgo-pmtiles-firstboot.service, a one-shot unit that runs once per VM- A tile server that runs as a dedicated non-root
pmtilesservice 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 SecureAnyCloud support
On the first boot of every VM, the one-shot service resolves the VM's public address and writes an instance info file to /root/go-pmtiles-info.txt. It then records the bootstrap ready marker and starts the tile server and nginx. The tile server unit is gated on that marker, so it never serves before first boot completes.
Key facts: 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 on loopback port 8080.
Prerequisites
You need an active Azure subscription, an SSH key pair, and a VNet with a subnet in the target region.
Standard_B2s (2 vCPU / 4 GiB RAM) is a good starting size. 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.
Configure the network security group (NSG) to allow inbound 22/tcp from your management network. Allow 80/tcp from the networks that consume your tiles, plus 443/tcp once you place your own TLS terminator in front.
Step 1 - Deploy from the Azure Marketplace
Sign in to the Azure Portal, choose Create a resource, search the Marketplace for go-pmtiles by SecureAnyCloud, and select Create.
On the Basics tab, 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).
Select Review + create, then Create. First-boot initialisation takes a few seconds after the VM starts.
Step 2 - Deploy from the Azure CLI
# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:go-pmtiles-ubuntu-24-04:default:latest
az vm create \
--resource-group <your-rg> \
--name go-pmtiles \
--image sac1647283583153:go-pmtiles-ubuntu-24-04:default:latest \
--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 1010Step 3 - Connect to your VM
ssh azureuser@<vm-public-ip>To look up the public IP at any time, run 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 that both are active, that the tile server binds loopback only, and that nginx serves the public port:
systemctl is-active pmtiles nginx ss -tln | grep -E ':80 |:8080 'Both services report active. The socket list shows nginx on 0.0.0.0:80 and 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 [::]:*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 same tiles render anywhere in the sample extent.
Step 6 - Request a map tile over HTTP
The tiles the viewer draws are standard Z/X/Y vector tiles that you can fetch directly. Each tile is a gzip-compressed Mapbox Vector Tile (.mvt). Request one and decode it to confirm that 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 gzip-encoded protobuf vector tile 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: 119916Step 7 - Read TileJSON and the per-VM instance info
go-pmtiles publishes a TileJSON document. Map clients such as MapLibre, Leaflet and OpenLayers use it 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. It lists the VM's own reachable URLs and a note that the service is read-only and public by design:
sudo cat /root/go-pmtiles-info.txtThe 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.pmtilesA 206 Partial Content response confirms that range serving is enabled.
Step 8 - Review 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.txtThe 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 are bundled under their own permissive licenses (BSD-3-Clause and the SIL Open Font License). They are MapLibre GL JS, the Protomaps theme and sprites, and the Noto Sans fonts.
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 pmtilesYou 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
- go-pmtiles server (single Go binary), version 1.31.2:
/var/lib/pmtiles/pmtiles - nginx reverse proxy, distribution package: /etc/nginx/sites-available/sac-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
/: root filesystem/var/lib/pmtiles: go-pmtiles home, holding the 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/sac/data/go-pmtiles-firstboot.done: first-boot bootstrap ready marker
Managing the service
systemctl status pmtiles --no-pager journalctl -u pmtiles -n 50 --no-pagerRestart the tile server after you add or replace an archive with sudo systemctl restart pmtiles.
Security notes
go-pmtiles is deployed here as a public-by-design, read-only tile server. You can confirm the posture on your VM:
- Unprivileged service. The tile server runs as the non-root
pmtilesservice account with no shell and no sudo. - Loopback only. It binds
127.0.0.1:8080only, with its admin port disabled. The sole public surface is nginx on port 80, with version disclosure suppressed. - Read-only API. 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.
- Gated start. The tile server unit is gated on the first-boot ready marker, so it does 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/sac/data/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 with this image or your deployment, contact SecureAnyCloud support:
- Email: sales@secureanycloud.com
- Response time: 24/7, with a guaranteed 24-hour response SLA
- Website: https://secureanycloud.com
Deploy on Azure
Launch go-pmtiles on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.
View this image
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com