go-pmtiles 1.31.2 on AWS User Guide
Overview
This guide covers launching and using the go-pmtiles 1.31.2 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg Hardened EHRbase & GIS, Health and Research Platforms AMIs listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
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 at /<archive>/{z}/{x}/{y}.mvt, together with TileJSON at /<archive>.json. A PMTiles archive is one file that holds a whole tile pyramid, so a basemap is a single file with no tile database, no tile cache and no external services to run.
The cloudimg image adds:
- HTTPS from the first boot. nginx serves everything on port 443 with a certificate generated on your instance's first boot (never baked into the image). Port 80 redirects to HTTPS.
- A read only service. go-pmtiles has no login, no administrator account and no write endpoint. nginx accepts only
GET,HEADandOPTIONS(anything else gets405) and refuses WebSocket upgrades. - go-pmtiles on loopback only, as an unprivileged user. The tile server listens on
127.0.0.1:8080with its admin listener disabled, runs as thepmtilessystem user in a systemd sandbox, and serves only the local directory/var/lib/pmtiles/tiles. A client can name an archive in that directory but never a URL or another path. The only open ports are 22, 80 and 443. - CORS you control. Cross origin access to the tiles and archives is allowed only for your instance's own HTTPS address plus the origins you add with
go-pmtiles-allow-origin(none in the image). There is noAccess-Control-Allow-Origin: *. - Fixed host and real client addresses. Requests for a host name the instance does not serve are dropped, the TileJSON tile URLs always use your configured host name (never the one a request supplied), and nginx logs the real client address rather than forged forwarding headers.
- Metadata service blocked. go-pmtiles and nginx cannot reach the instance metadata service, and the AMI requires IMDSv2.
- Compiled from source with current Go. The upstream go-pmtiles 1.31.2 release binary was built with Go 1.26.1 and carries known Go and library advisories. cloudimg compiles the same 1.31.2 source with Go 1.27.3 and patched dependency versions, and checks the result with
govulncheckat build time. - A self contained web map viewer. MapLibre GL JS 6.12.0 with the Protomaps light theme, Noto Sans glyphs and Protomaps sprites, all served from the instance (no CDN), under a strict Content Security Policy.
- A sample basemap so a map renders on first boot: a central London extract (zoom 0 to 15) of the Protomaps OpenStreetMap basemap, © OpenStreetMap contributors, Open Database License (ODbL).
- Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.

What is on the image: go-pmtiles 1.31.2 (compiled from the upstream source), nginx, the MapLibre web viewer, the sample basemap, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent.
Default user: ubuntu · Architecture: x86_64 · Recommended instance type: m5.large
Prerequisites
- An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet with a route to the internet (or a private route you use for management).
- A security group that allows TCP 22 (SSH) from your management network, and TCP 443 (the viewer, tiles and archives) and TCP 80 (redirects to HTTPS) from the networks that will use your tiles. Never open port 8080.
Step 1: Launch the instance
In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick go-pmtiles 1.31.2 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large, your key pair and a security group as above. go-pmtiles range reads tiles from the archive with a small in memory cache, so it needs little memory; size the root volume to the archives you plan to host.
With the AWS CLI (replace the values in angle brackets):
aws ec2 run-instances --region <region> --image-id <ami-id-from-the-listing> \
--instance-type m5.large --key-name <your-key-pair> \
--security-group-ids <your-security-group> --subnet-id <your-subnet> \
--metadata-options HttpTokens=required,HttpEndpoint=enabled \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=go-pmtiles}]'
Step 2: Connect to your instance
| Version | SSH user |
|---|---|
| go-pmtiles 1.31.2 on Ubuntu 24.04 | ubuntu |
ssh -i <your-key.pem> ubuntu@<public-ip>
Step 3: Check the services
First boot takes a few seconds. Then the three units are active:
systemctl is-active pmtiles nginx go-pmtiles-firstboot
active
active
active
go-pmtiles listens on loopback only; nginx serves ports 80 and 443:
sudo ss -tlnp | awk 'NR==1 || /pmtiles|nginx/' | awk '{print $1, $4, $6}' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g'
State Local Peer
LISTEN 127.0.0.1:8080 users:(("pmtiles"))
LISTEN 0.0.0.0:443 users:(("nginx"),("nginx"),("nginx"),("nginx"),("nginx"))
LISTEN 0.0.0.0:80 users:(("nginx"),("nginx"),("nginx"),("nginx"),("nginx"))
LISTEN [::]:443 users:(("nginx"),("nginx"),("nginx"),("nginx"),("nginx"))
LISTEN [::]:80 users:(("nginx"),("nginx"),("nginx"),("nginx"),("nginx"))
Step 4: Find your URLs
First boot writes this instance's addresses to a root only info file. There is no password to read: go-pmtiles is a read only service and the only per instance secret is the TLS private key.
sudo cat /root/go-pmtiles-info.txt
# go-pmtiles 1.31.2 on Ubuntu 24.04 (cloudimg AWS Marketplace image)
# Written by go-pmtiles-set-hostname (first boot). go-pmtiles is a READ-ONLY map tile server: there is no
# login, no admin account and no write endpoint, so no password exists. The only per-instance secret is the
# TLS private key in /etc/ssl/go-pmtiles (self-signed, minted on this instance's first boot).
GO_PMTILES_URL=https://<public-dns-name>/
GO_PMTILES_TILES_URL=https://<public-dns-name>/tiles/basemap/{z}/{x}/{y}.mvt
GO_PMTILES_TILEJSON_URL=https://<public-dns-name>/tiles/basemap.json
GO_PMTILES_ARCHIVE_URL=https://<public-dns-name>/data/basemap.pmtiles
The host name is your instance's public DNS name from the instance metadata service (or its public or private IP when it has no DNS name). Step 10 shows how to serve it under your own name.
Step 5: Open the web map viewer
Browse to the GO_PMTILES_URL from Step 4. The certificate is self signed and unique to your instance, so your browser asks you to accept it once (Step 10 replaces it with your own). The bundled MapLibre viewer 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; here the map shows the Thames and the City of London:

Step 6: Request a real map tile
The tiles the viewer draws are standard Z/X/Y vector tiles. Each is a gzip compressed Mapbox Vector Tile (.mvt). Request one and decode it to confirm it carries real map geometry:
curl -sk -D - -o /tmp/tile.mvt https://127.0.0.1/tiles/basemap/14/8186/5448.mvt | grep -iE '^(HTTP|content-type|content-encoding)'
echo "wire bytes (gzip): $(stat -c%s /tmp/tile.mvt)"
echo "decoded MVT bytes: $(gzip -dc /tmp/tile.mvt | wc -c)"
rm -f /tmp/tile.mvt
HTTP/2 200
content-type: application/x-protobuf
content-encoding: gzip
wire bytes (gzip): 83272
decoded MVT bytes: 119916
From another machine use the full URL, for example curl -sk https://<public-dns-name>/tiles/basemap/14/8186/5448.mvt -o tile.mvt.
Step 7: TileJSON and the raw archive
Map clients such as MapLibre, Leaflet and OpenLayers discover the tile URL template, zoom range and bounds from TileJSON:
curl -sk https://127.0.0.1/tiles/basemap.json | python3 -c 'import json,sys; d=json.load(sys.stdin); print(json.dumps({k: d.get(k) for k in ("tilejson","name","minzoom","maxzoom","bounds","tiles")}, indent=2))'
{
"tilejson": "3.0.0",
"name": "Protomaps Basemap",
"minzoom": 0,
"maxzoom": 15,
"bounds": [
-0.18,
51.47,
-0.05,
51.54
],
"tiles": [
"https://<public-dns-name>/tiles/basemap/{z}/{x}/{y}.mvt"
]
}
The tiles URL always carries your configured host name, even when the request arrived through another address (here 127.0.0.1).
The raw archive is also served for clients that read PMTiles directly over HTTP range requests (for example the pmtiles JavaScript library). Every .pmtiles file in /var/lib/pmtiles/tiles is available at /data/<name>.pmtiles:
curl -sk -r 0-6 -o /dev/null -w 'range status: HTTP %{http_code}\n' https://127.0.0.1/data/basemap.pmtiles
range status: HTTP 206
Step 8: Security checks
The tile server refuses writes, unknown host names are dropped, a foreign web origin gets no CORS grant, and both services are walled off from the instance metadata service:
curl -sk -o /dev/null -w 'POST a tile: HTTP %{http_code}\n' -X POST https://127.0.0.1/tiles/basemap/14/8186/5448.mvt
curl -s -o /dev/null -w 'unknown Host on port 80: HTTP %{http_code} (connection closed)\n' -H 'Host: attacker.example' http://127.0.0.1/ || true
curl -sk -D - -o /dev/null -H 'Origin: https://attacker.example' https://127.0.0.1/tiles/basemap.json | grep -ci '^access-control-allow-origin' | sed 's/^/ACAO headers for a foreign origin: /' || true
systemctl show -p IPAddressDeny pmtiles nginx
POST a tile: HTTP 405
unknown Host on port 80: HTTP 000 (connection closed)
ACAO headers for a foreign origin: 0
IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
Port 80 only redirects to HTTPS:
curl -s -o /dev/null -w 'http://127.0.0.1/ -> HTTP %{http_code} %{redirect_url}\n' http://127.0.0.1/
http://127.0.0.1/ -> HTTP 301 https://<public-dns-name>/
Step 9: Let your map site use the tiles (CORS)
A map front end on another web origin (for example https://maps.example.com) needs a CORS grant to fetch your tiles from the browser. Add that origin to the allow-list; nginx reloads immediately. An origin is https://host or https://host:port, with no path, and * is refused:
sudo go-pmtiles-allow-origin add https://maps.example.com
sudo go-pmtiles-allow-origin remove https://maps.example.com
List the origins you have added (the image ships with none, so only the instance's own HTTPS address is allowed):
sudo go-pmtiles-allow-origin list
Browsers only fetch cross origin tiles from a server whose certificate they trust, so serve the tiles under your own DNS name and certificate (Step 10) before you point a public map site at them.
Step 10: Your own DNS name and certificate
Point a DNS record at the instance, then set the name the instance serves. This mints a new certificate for that name, pins it in nginx, updates the TileJSON tile URLs and the info file, and records the name so it survives reboots:
sudo go-pmtiles-set-hostname maps-tiles.example.com
To use a certificate from a public CA instead of the self signed one, replace /etc/ssl/go-pmtiles/go-pmtiles.crt (full chain) and /etc/ssl/go-pmtiles/go-pmtiles.key (mode 0600, owned by root) with your own after running the command above, then reload nginx:
sudo nginx -t && sudo systemctl reload nginx
Step 11: Serve your own area
Replace or add archives in /var/lib/pmtiles/tiles. The pmtiles command can cut a bounded extract straight from the Protomaps global basemap with range requests, so you download only your region. Pick a recent daily build from https://maps.protomaps.com/builds/:
cd /tmp
pmtiles extract https://build.protomaps.com/<YYYYMMDD>.pmtiles your-area.pmtiles \
--bbox=<min_lon>,<min_lat>,<max_lon>,<max_lat> --maxzoom=15
sudo install -o root -g root -m 0644 your-area.pmtiles /var/lib/pmtiles/tiles/
sudo systemctl restart pmtiles
Each archive is served as /tiles/<name>/{z}/{x}/{y}.mvt (TileJSON at /tiles/<name>.json) and /data/<name>.pmtiles, where <name> is the file name without .pmtiles (letters, digits, - and _). You can also build archives from your own data with pmtiles convert (from MBTiles) or the Protomaps basemaps tooling. Each extra zoom level roughly doubles the archive size, so choose --maxzoom to match how far your users zoom in. The bundled viewer always shows the sample basemap archive; point your own MapLibre, Leaflet or OpenLayers front end at your archive's TileJSON URL.
Step 12: Versions, build provenance and licences
pmtiles version
sed -n '1,6p' /usr/share/doc/go-pmtiles/BUILD-PROVENANCE.txt
pmtiles 1.31.2, commit a3e4951ea6a0477b784c27c1dcbfd9c130878c5a, built at 2026-10-20T08:53:20Z
go-pmtiles 1.31.2 - cloudimg source build (AWS Marketplace image)
source https://github.com/protomaps/go-pmtiles/archive/refs/tags/v1.31.2.tar.gz
source sha256 022386795fae441dd35927e4061daaed8cda7e21ba0a2e2253c8fd0d85bc482f (tag v1.31.2 commit a3e4951ea6a0477b784c27c1dcbfd9c130878c5a)
licence BSD-3-Clause (LICENSE in this directory)
toolchain go1.27.3 linux/amd64, official tarball https://go.dev/dl/go1.27.3.linux-amd64.tar.gz
toolchain sha256 65efbc80fdc1fa2cd2631cad5b481777a68e8725f34e89f50fc6ac714e379b40
The full provenance (every patched dependency and the govulncheck result) is in /usr/share/doc/go-pmtiles/BUILD-PROVENANCE.txt. The sample map data and the viewer components are attributed on the instance:
grep -A3 '^Sample map data' /usr/share/doc/go-pmtiles/DATA-LICENSES.txt
The map data is an extract of OpenStreetMap, © OpenStreetMap contributors, under the Open Database License (ODbL); keep this attribution whenever you serve or redisplay the tiles. MapLibre GL JS, the Protomaps theme and sprites are BSD-3-Clause and the Noto Sans fonts use the SIL Open Font License.
Server components
| Component | Version | Path |
|---|---|---|
| go-pmtiles (compiled from source with Go 1.27.3) | 1.31.2 | /usr/local/bin/pmtiles |
| nginx (HTTPS front door) | Ubuntu 24.04 package | /etc/nginx/sites-available/cloudimg-go-pmtiles |
| Sample basemap | central London, zoom 0 to 15 | /var/lib/pmtiles/tiles/basemap.pmtiles |
| Web map viewer | MapLibre GL JS 6.12.0 | /var/lib/pmtiles/viewer/ |
Filesystem layout
| Path | Description |
|---|---|
/var/lib/pmtiles/tiles |
PMTiles archives, served at /tiles/<name>/... and /data/<name>.pmtiles |
/var/lib/pmtiles/viewer |
The self contained MapLibre viewer and its fonts and sprites |
/etc/pmtiles/pmtiles.env |
PMTILES_PUBLIC_URL, the base URL used in TileJSON (set by go-pmtiles-set-hostname) |
/etc/cloudimg/go-pmtiles-cors-origins |
Your CORS allow-list (managed by go-pmtiles-allow-origin) |
/etc/cloudimg/go-pmtiles-hostname |
The served name you set with go-pmtiles-set-hostname |
/etc/ssl/go-pmtiles/ |
This instance's TLS certificate and private key |
/root/go-pmtiles-info.txt |
This instance's URLs, written at first boot |
/usr/share/doc/go-pmtiles/ |
Licence, build provenance and data licences |
Managing the services
systemctl status pmtiles --no-pager | head -5
Restart the tile server after you add or replace an archive with sudo systemctl restart pmtiles, and read its log with journalctl -u pmtiles -n 50 --no-pager. Both services start automatically at boot, after the first boot unit has created the certificate.
Security recommendations
- Keep TCP 22 limited to your management network, and open 443 and 80 only to the networks that need your tiles.
- Serve under your own DNS name and a certificate from a public CA before publishing a map site (Step 10).
- Add only the web origins that really need cross origin access (Step 9).
- Unattended security upgrades are on; reboot when
/var/run/reboot-requiredappears.
Troubleshooting
The viewer or tiles do not load. Check systemctl is-active pmtiles nginx. Both wait for the first boot unit: journalctl -u go-pmtiles-firstboot --no-pager shows what it did. If pmtiles is not active, read journalctl -u pmtiles -n 100 --no-pager.
The browser shows a certificate warning. The certificate is self signed and unique to your instance. Accept it for testing, or install your own name and certificate (Step 10).
A tile request returns 404. The path is /tiles/<archive>/{z}/{x}/{y}.mvt with the archive file name without .pmtiles (the sample is basemap). Tiles outside the archive's bounds or above its maximum zoom (15 for the sample) do not exist.
Your map site's browser console reports a CORS error. Add the site's origin with sudo go-pmtiles-allow-origin add https://<site> (Step 9), and make sure the browser trusts the tile server's certificate.
Labels are missing for your own area. The bundled viewer ships Latin glyph ranges; labels in other scripts need the matching Noto font ranges under /var/lib/pmtiles/viewer/assets/fonts/.
Support and licensing
go-pmtiles is open source software by Protomaps under the BSD-3-Clause licence. The go-pmtiles code in this image is unmodified: cloudimg compiles the upstream 1.31.2 source with Go 1.27.3 and newer versions of some of its dependencies (listed in the build provenance file). cloudimg provides 24/7 technical support for this image by email and live chat: support@cloudimg.co.uk, with a 24 hour response SLA.
All product names are trademarks of their respective owners. cloudimg is not affiliated with or endorsed by Protomaps, the OpenStreetMap Foundation or the MapLibre project.