Overview
One binary, five subcommands. Everything below runs the same way: as terraserve <command> [flags], or inside Docker as docker run … terraserve <command> [flags] (mount your data at /data, exactly as in the quick start).
| serve | Run the live HTTP server: WMS, tiles, viewers. The everyday command; full flag reference on the Docs page. |
| render | Render one window of a raster to a PNG. The engine core, no server. |
| build-pmtiles | Bake a vector source into an offline .pmtiles tile pyramid (what the demos precompute for speed). |
| pmtiles-recompress | Re-encode an MVT .pmtiles archive into another tile compression (zstd, brotli, gzip) without re-baking it, then check every tile decodes identically. Since 0.3.4. |
| extract | Cut a per-zoom, pre-generalized subset of a vector source into a GeoPackage, so a zoom band can be baked with no per-tile selection (and so no seams). |
| build-topology | Build and report the shared-border topology of a coverage. A diagnostic; draws no tiles. |
| wms-handle | Answer a single WMS request and write it to stdout. No server: for scripts and CI. |
| info | Report which PROJ this binary uses and which proj.db it actually resolved. The first thing to run when a reprojection looks wrong. |
Every command prints its own flags with --help: terraserve build-pmtiles --help. The examples here use bare paths for readability; prefix them with /data/ when you run in a container.
serve
The live server: it publishes WMS, TMS, vector tiles and the built-in viewers. This is the command you run in production, and it has the most flags by far, data and styling, network, caching, vector tuning, multi-layer YAML. Its full reference lives on the Docs page. The shape is:
# one raster, as WMS + tiles
terraserve serve --cog ortho.cog.tif --style rgb.json \
--host 0.0.0.0 --port 8080
# one vector layer (needs a style)
terraserve serve --vector roads.fgb --vec-style roads.style.json \
--name roads --host 0.0.0.0 --port 8080
# many layers from a config
terraserve serve --config layers.yaml --host 0.0.0.0 --port 8080 The flags that matter for every deployment, --host 0.0.0.0 in a container, --public-url behind a proxy, --max-inflight as the memory ceiling, are covered with their pitfalls on the Docs.
render
The engine core with no server around it: read one window of a COG, warp and resample it into a grid, style it, write a PNG. It is the single-shot version of what serve does per WMS request, useful for a one-off image, a thumbnail, or a quick correctness check.
terraserve render \
--cog cascais.cog.deflate.tif \
--bbox -9.45,38.68,-9.38,38.72 --crs EPSG:4326 \
--width 512 --height 512 \
--resample bilinear \
--style rgb.json \
--out cascais.png | --cog PATH | The source Cloud-Optimized GeoTIFF (local path or s3://…). |
| --bbox MINX,MINY,MAXX,MAXY | The window to render, in --crs units. Values may be negative. |
| --crs CRS | Output projection, e.g. EPSG:3857 or EPSG:4326. |
| --src-crs CRS | The source projection the COG is in. Optional: defaults to EPSG:3763 (the bundled sample grid). Set it for a source in any other projection, e.g. EPSG:32629. |
| --width, --height N | Output size in pixels. |
| --resample MODE | nearest or bilinear. |
| --style PATH | A style.json (mode rgb or pseudocolor). See the Styling guide. |
| --out PATH | Where to write the PNG. |
--src-crs defaults to the sample grid
When you omit --src-crs, the source is assumed to be in EPSG:3763, the projection of the bundled sample COG, so the reprojection to --crs is only correct for a source in that CRS. For anything else, pass it explicitly (--src-crs EPSG:32629). The --bbox is always given in --crs (output) units, whatever the source projection is.
build-pmtiles
Drawing vector tiles live is decode-bound: at low zoom a single tile has to read and generalize a large slice of the data, and that can take seconds. build-pmtiles pays that cost once, offline, into a static .pmtiles archive. Serving from the archive is then a byte-range read (milliseconds), not a render. This is exactly what the live demos precompute.
1. Bake the pyramid.
terraserve build-pmtiles \
--vector roads.fgb \
--vec-style roads.style.json \
--name roads \
--min-zoom 0 --max-zoom 14 \
--out roads.pmtiles 2. Serve it. Point serve at the archive with --pmtiles; keep --vector pointed at the source so a zoom outside the baked range still falls through to a live draw.
terraserve serve \
--vector roads.fgb --vec-style roads.style.json --name roads \
--pmtiles roads.pmtiles \
--host 0.0.0.0 --port 8080 | --vector PATH | The source to bake: a GeoPackage, FlatGeoBuf or GeoJSON. |
| --out PATH | The .pmtiles archive to write. |
| --min-zoom, --max-zoom N | Zoom range to generate, inclusive. Default 0 to 14. Higher max = more detail and a bigger archive (capped at z26). |
| --grid ID | The tile grid to bake on: WebMercatorQuad (default), WorldCRS84Quad, the polar UPS grids, or a path to a custom OGC TileMatrixSet 2.0 JSON (a national grid). The archive is stamped with this grid; serve reads it only for matching-grid requests. |
| --vector-layer TABLE | Which table to read when the GeoPackage holds more than one, e.g. Parks. Required then: TerraServe refuses to start and lists the tables rather than guess. Not needed for a single-table GeoPackage, and refused for any other source. Since 0.3.6. |
| --vec-style PATH | The vector style (marker + label symbolizer) for the layer. |
| --name NAME | The layer name embedded in the tiles (the source-layer a client style targets). Match whatever you will serve and style this data under. Default vector. |
| --src-crs CRS | The feature CRS. A GeoPackage or FlatGeoBuf's own CRS is auto-detected when unset; otherwise the default is EPSG:3763. Pass EPSG:4326 for GeoJSON in degrees: read as metres, a whole country shrinks to a few metres and every tile bakes empty, with only a startup warning. |
| --bbox W,S,E,N | Limit baking to a WGS84 box. Default: the layer's own bounds. |
| --font PATH | TrueType font for label text. Default DejaVuSans.ttf. |
| --tmpdir PATH | Scratch directory for the streamed data section. Default: the system temp dir. |
| --tile-format mvt|png | mvt (default) bakes vector tiles for serve --pmtiles. png bakes rendered raster tiles for the WMTS/TMS picture paths, served with serve --raster-pmtiles: a live render of a dense layer becomes a read from disk. |
| --tile-px N | Pixel size of a png bake, default 512. Must equal the serving grid's tile size, or serve refuses the archive at startup rather than hand out quarter-size images. |
| --mvt-min-feature-px N | Skip polygons smaller than N on-screen pixels, as on serve. Also gates a png bake unless --raster-min-feature-px is given. Bake with the same value the server uses, or the map changes at the archive's edge. |
| --mvt-min-feature-px-min-zoom Z | Apply that gate only from zoom Z up. A pixel threshold quarters in ground area every zoom while feature sizes do not, so one value cannot serve a whole pyramid: the value that is right at z8 blanks z3. |
| --mvt-min-feature-len-px STEPS | The same idea for lines, by length, as per-zoom steps: 0:2.0,7:0.3. |
| --raster-min-feature-px N | The size gate for a png bake only, overriding the value above. |
| --mvt-max-features N | Cap the features per tile (default 20000). --no-safety-limit removes the cap; see the warning below before you do. |
| --tile-compression gzip|br|zstd | How an MVT archive stores its tiles, default gzip. zstd (level 19) came out 7-14 % smaller than gzip on real tiles for about 0.2 s per heavy tile, paid once; br (brotli 11) 15-19 % smaller but several times slower to bake. serve reads all three. Bake every band of one archive with the same value: the pmtiles tool refuses to merge mixed compressions. Since 0.3.3. |
| --tile-compression-level N | Override that level: gzip 0-9, brotli 0-11, zstd 1-22. |
| --mvt-fine-extent-zoom Z | Keep small features as real shapes below zoom Z, as on serve. The bake and the server must use the same value. |
| --zoom-source MIN:MAX:PATH | Bake a zoom band from its own subset made by extract. Repeatable; zooms outside every band fall back to --vector. |
Two traps when baking. Set the same TERRASERVE_FGB_MAX_QUERY_FEATURES (or _PG_) the server uses: a low-zoom tile can match every feature in the file, and past the default cap of 500,000 the bake silently truncates, blanking whole regions. The bake log says capping decode when it happens; zero of those is the pass condition. And a complete low-zoom vector tile of a dense layer is enormous: one z6 tile of an 842k-polygon land-cover map held 510,000 polygons, and a few views of tiles like that took a browser to 4 GB. Keep the per-tile cap below about z9, and let a png archive underneath show every feature instead.
pmtiles-recompress
Since 0.3.4. Changes how an existing vector archive stores its tiles, without re-baking it. A bake re-reads the source and re-runs every gate and band, which for a database layer can take hours and can change the tiles. This command never touches the source: it decompresses each stored tile with the archive's own encoding, compresses it again with the new one (in parallel), copies the header and metadata, and then checks every addressed tile decodes to exactly the same bytes before reporting success. Measured on the live demos, gzip to zstd saved 4-19 % depending on the archive: the most on few large tiles (vida buildings -13.8 %), the least on many small ones (a 922 MB land-cover archive -3.7 %).
terraserve pmtiles-recompress \
--in buildings.pmtiles --out buildings.zstd.pmtiles --tile-compression zstd
# pmtiles-recompress: buildings.pmtiles -> buildings.zstd.pmtiles (zstd) · addressed 8048 · ... -13.8% · 21.6s
# verified: 8048 addressed tiles decode identically, same ids, metadata and bounds | --in PATH | The MVT archive to read. A PNG archive is refused: its tiles are already compressed images. |
| --out PATH | The archive to write. Must differ from --in; the original is never modified. |
| --tile-compression zstd|br|gzip | Target compression, default zstd (level 19). br is brotli 11, smaller still but several times slower. |
| --tile-compression-level N | Override the level: gzip 0-9, brotli 0-11, zstd 1-22. |
| --tmpdir PATH | Scratch directory for the streamed data section. Default: next to --out. |
Point serve --pmtiles at the new file; the server reads gzip, brotli and zstd archives alike and answers each browser in an encoding it accepts. Two limits: a write-through --pmtiles-cache needs a gzip base archive, and the pmtiles tool will not merge archives of different compressions, so convert every part of a banded archive the same way.
extract
Writes a per-zoom, pre-generalized subset of a vector source as a GeoPackage. The feature selection a tile would otherwise do at render time (the size gate, the per-tile cap) is done once, on whole features, before any tile exists. Tiling the subset is then a pure spatial cut, and neighbouring tiles cannot disagree about what to keep, which is where tile seams come from. Chain the bands: extract the next shallower band from the subset you just made, not from the full table.
# a subset for zooms 5-6, on the grid the tiles will be cut on
terraserve extract \
--vector roads.gpkg --vec-style roads.vec.json \
--grid WebMercatorQuad --min-zoom 5 --max-zoom 6 \
--mvt-min-feature-px 0.5 --out roads-z5-6.gpkg
# then bake that band from it
terraserve build-pmtiles --vector roads.gpkg --vec-style roads.vec.json \
--zoom-source 5:6:roads-z5-6.gpkg --out roads.pmtiles | --vector PATH | Source to select from: .gpkg, .fgb, .geojson, s3:// or a PostGIS URI. A GeoPackage extract wrote is accepted too, which is how bands are chained. |
| --vector-layer TABLE | Which table to read when the GeoPackage holds more than one, e.g. Parks. Required then: TerraServe refuses to start and lists the tables rather than guess. Not needed for a single-table GeoPackage, and refused for any other source. Since 0.3.6. |
| --vec-style PATH | Required because the layer is built the same way serve builds it. Nothing renders, and the style does not change what is selected. |
| --out PATH | The .gpkg to write. Overwritten if it exists. |
| --grid ID | The grid the tiles will be cut on. It must be the same one: the thresholds come from that grid's resolution at each zoom. |
| --min-zoom, --max-zoom Z | The band this subset serves. It is cut at the loosest threshold in the band, so it is a superset of every zoom in it. |
| --mvt-min-feature-px N | Polygon size gate in on-screen pixels; 0 leaves only the one-grid-cell floor. With --mvt-min-feature-px-min-zoom and --mvt-min-feature-len-px exactly as on serve. |
| --columns COLS / --no-columns | Columns the subset must carry, e.g. building,highway, or none at all (much smaller, and enough for a layer drawn in one colour). The only knob that reaches a PostGIS source: its columns otherwise come from the server-side style, so fields only a client-side style needs would be missing. |
| --keep-fields COLS | For a file source: the attribute columns to copy. Default all of them. |
| --src-crs EPSG:N | Source CRS, when the file does not declare one. |
| --extent MINX,MINY,MAXX,MAXY | Read only this window, in the source CRS. Default: the full layer, which is what a real subset wants. |
| --name NAME | Table name in the GeoPackage. Default subset. |
The baked tiles match a live render exactly
build-pmtiles runs the same encode and generalization pipeline as the live /mvt route, so an archived tile is byte-identical to the one serve would draw. That means the vector-tuning flags are shared: bake with the same generalization you would serve, --mvt-min-feature-px, --mvt-max-features, --mvt-dissolve, --keep-fields, --topology-simplify, all documented under Vector tuning. Keep --name the same across bake, serve and style, or a client's source-layer will not match.
build-topology
A diagnostic, not a renderer. It loads a polygon coverage (a GeoPackage, FlatGeoBuf or GeoJSON), snaps and builds its shared-border topology (the arcs that neighbouring polygons have in common), and prints a report. Nothing is served or stored. Use it to check a coverage is clean before you turn on serve --topology-simplify, which builds this same structure at startup to smooth every shared border gap-free (that serve-time build is GeoPackage-only).
terraserve build-topology \
--vector coverage.gpkg \
--snap-tolerance 0.01 \
--verify The report lists the feature, ring, arc and junction counts, how many arcs are shared versus on the outer boundary, the vertex count before and after snapping, any degenerate or non-finite geometry it dropped, and the total area delta from snapping. With --verify it also round-trips the topology back to polygons and reports how many features differ: 0 / N means the topology reproduces the input exactly.
| --vector PATH | The coverage to analyse: a GeoPackage, FlatGeoBuf or GeoJSON. |
| --layer NAME | Which table, if the GeoPackage has more than one: required then (the command lists the tables rather than guess). Also accepted as --vector-layer. A FlatGeoBuf or GeoJSON is single-layer, so it is ignored there. |
| --snap-tolerance UNITS | Snap tolerance in source-CRS units. Default 0.01. A fine value leaves an already-clean coverage untouched. |
| --verify | After building, run the round-trip oracle and print the mismatch count. 0 = a perfect round-trip. |
wms-handle
Answers one WMS request and writes the result to stdout, then exits, no server, no port. It handles GetMap (a PNG), GetCapabilities and exceptions (XML), for both WMS 1.1.1 and 1.3.0 including the EPSG:4326 axis flip. Handy for scripting a single render or sanity-checking WMS output in CI without binding a socket. Single COG only (no --config or --vector).
# capabilities document to a file
terraserve wms-handle --cog ortho.cog.tif --style rgb.json \
--query 'SERVICE=WMS&VERSION=1.3.0&REQUEST=GetCapabilities' \
> capabilities.xml Swap REQUEST=GetMap (with LAYERS, CRS, BBOX, WIDTH, HEIGHT, FORMAT=image/png) and redirect to a .png to get a rendered map the same way.
| --cog PATH | The source COG (single layer; no --config or --vector). |
| --style PATH | The style.json for the layer. |
| --query STRING | The raw WMS KVP query, e.g. SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&…. |
info
Reports which PROJ this binary uses and which proj.db it actually resolved, by asking PROJ itself. --version and --help never touch PROJ, so they cannot tell you whether a reprojection will work; this can. Run it first when coordinates come out wrong, and paste it into a bug report. The standalone binary carries its own proj.db and unpacks it to a cache directory on first run; an explicit PROJ_DATA always wins.
$ terraserve info
terraserve 0.3.6
PROJ Rel. 9.6.0, March 15th, 2025 (statically linked, vendored)
proj.db ~/.cache/terraserve/proj-a2337534498ffcac/proj.db
9883648 bytes
embedded 9883648 bytes, id a2337534498ffcac
PROJ_DATA ~/.cache/terraserve/proj-a2337534498ffcac
PROJ_LIB (unset)
search path ~/.local/share/proj:~/.cache/terraserve/proj-a2337534498ffcac From Python the same report is terraserve.proj_info(), a dict.