go-pmtiles 1.31.2
Overview
This guide covers launching and using the go-pmtiles 1.31.2 on Ubuntu 24.04 AMI from SecureAnyCloud on Amazon EC2. The AMI is one version of the SecureAnyCloud Hardened EHRbase & GIS, Health and Research Platforms AMIs listing on AWS Marketplace. You select it as the software version when you subscribe and launch.
go-pmtiles is the open source, single-binary map tile server from Protomaps. The pmtiles serve command 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.
What you get:
- HTTPS from the first boot. nginx serves everything on port 443 with a certificate that is generated on your instance's first boot and 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 that 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 that the instance does not serve are dropped. The TileJSON tile URLs always use your configured host name, never the one that a request supplied. 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. SecureAnyCloud compiles the same 1.31.2 source with Go 1.27.3 and patched dependency versions, and checks the result with govulncheck at 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 that 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 that you use for management).
- A security group that allows TCP 22 (SSH) from your management network. The group must also allow TCP 443 (the viewer, tiles and archives) and TCP 80 (redirects to HTTPS) from the networks that will use your tiles. Do not open port 8080.
Launch the instance
In the AWS Marketplace listing, choose Continue to Subscribe, then Continue to Configuration. Select go-pmtiles 1.31.2 on Ubuntu 24.04 as the software version, choose your Region, and select Launch through EC2.
Choose m5.large, your key pair and a security group as described 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.
Alternatively, launch 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}]'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>Check the services
First boot takes a few seconds. After it completes, the three units are active:
systemctl is-active pmtiles nginx go-pmtiles-firstbootactive active activego-pmtiles listens on loopback only, and 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"))Find your URLs
First boot writes this instance's addresses to a root-only info file. There is no password to read, because 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 (sac 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.pmtilesThe 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 the instance under your own name.
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 certificate.
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. In this example, the map shows the Thames and the City of London.
Request a real map tile
The tiles that 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 that 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.mvtHTTP/2 200 content-type: application/x-protobuf content-encoding: gzip wire bytes (gzip): 83272 decoded MVT bytes: 119916From another machine, use the full URL, for examplecurl -sk https://<public-dns-name>/tiles/basemap/14/8186/5448.mvt -o tile.mvt.
TileJSON and the raw archive
Map clients such as MapLibre, Leaflet and OpenLayers read 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.pmtilesrange status: HTTP 206Security checks
The tile server refuses writes, unknown host names are dropped, a foreign web origin gets no CORS grant, and both services are blocked from the instance metadata service. Run these checks:
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 nginxPOST 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/128Port 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>/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, and nginx reloads immediately. An origin is https://host or https://host:port, with no path. The value * is refused:
sudo go-pmtiles-allow-origin add https://maps.example.com
sudo go-pmtiles-allow-origin remove https://maps.example.comList the origins that you have added. The image ships with none, so only the instance's own HTTPS address is allowed:
sudo go-pmtiles-allow-origin listBrowsers fetch cross-origin tiles only from a server whose certificate they trust. Serve the tiles under your own DNS name and certificate (Step 10) before you point a public map site at them.
Your own DNS name and certificate
Point a DNS record at the instance, then set the name that the instance serves. This command creates a new certificate for that name, pins it in nginx, updates the TileJSON tile URLs and the info file, and records the name so that it survives reboots:
sudo go-pmtiles-set-hostname maps-tiles.example.comTo use a certificate from a public CA instead of the self-signed one, run the command above and then 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 files. Then reload nginx:
sudo nginx -t && sudo systemctl reload nginxServe 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 pmtilesEach 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.
Versions, build provenance and licences
pmtiles version sed -n '1,6p' /usr/share/doc/go-pmtiles/BUILD-PROVENANCE.txtpmtiles 1.31.2, commit a3e4951ea6a0477b784c27c1dcbfd9c130878c5a, built at 2026-10-20T08:53:20Z go-pmtiles 1.31.2 - sac 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 65efbc80fdc1fa2cd2631cad5b481777a68e8725f34e89f50fc6ac714e379b40The 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.txtThe 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/sac-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/sac/go-pmtiles-cors-origins Your CORS allow-list (managed by go-pmtiles-allow-origin)
/etc/sac/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 -5Restart 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 services wait for the first boot unit. Run journalctl -u go-pmtiles-firstboot --no-pager to see what it did. If pmtiles is not active, run 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. SecureAnyCloud compiles the upstream 1.31.2 source with Go 1.27.3 and newer versions of some of its dependencies, which are listed in the build provenance file.
SecureAnyCloud provides 24/7 technical support for this image by email and live chat at sales@secureanycloud.com, with a 24 hour response SLA.
All product names are trademarks of their respective owners. SecureAnyCloud is not affiliated with or endorsed by Protomaps, the OpenStreetMap Foundation or the MapLibre project.
Deploy on AWS
Launch go-pmtiles 1.31.2 on Ubuntu 24.04 on AWS with 24/7 support from SecureAnyCloud.
View on Marketplace
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com