|
| 1 | +# tile-server-lite |
| 2 | + |
| 3 | +> From a local vector file to a live slippy map in one command — on-the-fly Mapbox Vector Tiles with a built-in MapLibre preview. |
| 4 | +
|
| 5 | +[](https://github.com/python-geospatial/tile-server-lite/actions/workflows/ci.yml) |
| 6 | +[](https://www.python.org/) |
| 7 | +[](LICENSE) |
| 8 | + |
| 9 | +`tile-server-lite` is a minimal [FastAPI](https://fastapi.tiangolo.com/) server that reads any |
| 10 | +[geopandas](https://geopandas.org/)-readable vector file — GeoPackage, GeoParquet, GeoJSON, |
| 11 | +Shapefile — and serves it as XYZ **Mapbox Vector Tiles (MVT)**, cut on the fly. Point it at a file, |
| 12 | +open your browser, and you get a working [MapLibre GL JS](https://maplibre.org/) map fit to your |
| 13 | +data's bounds. |
| 14 | + |
| 15 | +Maintained by [python-geospatial.com](https://python-geospatial.com), a knowledge base for the |
| 16 | +modern Python geospatial stack. |
| 17 | + |
| 18 | +## What & why |
| 19 | + |
| 20 | +Static vector tile pipelines (Tippecanoe, PMTiles) are the right call for large, published datasets. |
| 21 | +But while you are *exploring* — checking a fresh export, sharing a quick preview, iterating on a |
| 22 | +layer — pre-baking tiles is friction. `tile-server-lite` cuts tiles per request straight from your |
| 23 | +file, so there is no build step between "I have a file" and "I have a map". |
| 24 | + |
| 25 | +It complements, rather than replaces, the static [PMTiles](https://python-geospatial.com/web-mapping-interactive-visualization/vector-tile-pipelines-with-pmtiles/) |
| 26 | +workflow covered on the site: use this server for local development and previews, then bake to |
| 27 | +PMTiles for production hosting (see [How it works](#how-it-works)). |
| 28 | + |
| 29 | +## Install |
| 30 | + |
| 31 | +Not published to PyPI — install straight from the repository: |
| 32 | + |
| 33 | +```bash |
| 34 | +pip install "git+https://github.com/python-geospatial/tile-server-lite.git" |
| 35 | +``` |
| 36 | + |
| 37 | +Or clone and install in editable mode: |
| 38 | + |
| 39 | +```bash |
| 40 | +git clone https://github.com/python-geospatial/tile-server-lite.git |
| 41 | +cd tile-server-lite |
| 42 | +pip install -e ".[dev]" |
| 43 | +``` |
| 44 | + |
| 45 | +## Usage |
| 46 | + |
| 47 | +Serve a single GeoPackage: |
| 48 | + |
| 49 | +```bash |
| 50 | +tile-server-lite serve parcels.gpkg |
| 51 | +``` |
| 52 | + |
| 53 | +Serve several files at once (each layer is named after its file stem), on a chosen host/port and |
| 54 | +zoom window: |
| 55 | + |
| 56 | +```bash |
| 57 | +tile-server-lite serve parcels.gpkg sensors.geoparquet \ |
| 58 | + --host 0.0.0.0 --port 8080 --min-zoom 6 --max-zoom 18 |
| 59 | +``` |
| 60 | + |
| 61 | +Example startup output: |
| 62 | + |
| 63 | +``` |
| 64 | + tile-server-lite |
| 65 | +┏━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ |
| 66 | +┃ Layer ┃ Geometry ┃ Features ┃ TileJSON ┃ |
| 67 | +┡━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ |
| 68 | +│ parcels │ Polygon │ 1 240 │ http://127.0.0.1:8080/tilejson/parcels… │ |
| 69 | +│ sensors │ Point │ 317 │ http://127.0.0.1:8080/tilejson/sensors… │ |
| 70 | +└─────────┴──────────┴──────────┴──────────────────────────────────────────┘ |
| 71 | +Preview map: http://127.0.0.1:8080/ · Served by python-geospatial.com |
| 72 | +``` |
| 73 | + |
| 74 | +Then: |
| 75 | + |
| 76 | +- open [http://127.0.0.1:8080/](http://127.0.0.1:8080/) for the MapLibre preview, |
| 77 | +- fetch a tile at `GET /tiles/parcels/16/35208/21489.mvt`, |
| 78 | +- fetch the TileJSON at `GET /tilejson/parcels.json`. |
| 79 | + |
| 80 | +Example TileJSON response (abridged): |
| 81 | + |
| 82 | +```json |
| 83 | +{ |
| 84 | + "tilejson": "3.0.0", |
| 85 | + "name": "parcels", |
| 86 | + "scheme": "xyz", |
| 87 | + "tiles": ["http://127.0.0.1:8080/tiles/parcels/{z}/{x}/{y}.mvt"], |
| 88 | + "minzoom": 6, |
| 89 | + "maxzoom": 18, |
| 90 | + "bounds": [13.4028, 52.5185, 13.4098, 52.5232], |
| 91 | + "vector_layers": [ |
| 92 | + {"id": "parcels", "geometry_type": "Polygon", |
| 93 | + "fields": {"parcel_id": "Integer", "zoning": "String"}} |
| 94 | + ] |
| 95 | +} |
| 96 | +``` |
| 97 | + |
| 98 | +## Deploy with Docker |
| 99 | + |
| 100 | +A single-file `Dockerfile` is included. Build the image, then mount a directory of vector files at |
| 101 | +`/data`: |
| 102 | + |
| 103 | +```bash |
| 104 | +docker build -t tile-server-lite . |
| 105 | + |
| 106 | +docker run --rm \ |
| 107 | + -v $(pwd)/data:/data \ |
| 108 | + -p 8080:8080 \ |
| 109 | + tile-server-lite serve /data/parcels.gpkg --host 0.0.0.0 --port 8080 |
| 110 | +``` |
| 111 | + |
| 112 | +The map is then live at [http://localhost:8080/](http://localhost:8080/). Binding to `0.0.0.0` |
| 113 | +inside the container lets the published port reach the server; keep it on `127.0.0.1` when running |
| 114 | +directly on your machine. |
| 115 | + |
| 116 | +## Features |
| 117 | + |
| 118 | +- **Any geopandas-readable input** — GeoPackage, GeoParquet, GeoJSON, Shapefile, and more. |
| 119 | +- **True MVT** — geometries clipped to each tile (with an edge buffer) and encoded with |
| 120 | + `mapbox-vector-tile` in the standard 0–4096 grid. |
| 121 | +- **Spatial-index-backed selection** — only features intersecting a tile are considered. |
| 122 | +- **Valid TileJSON 3.0.0** — with 4326 bounds, zoom window, and `vector_layers` metadata. |
| 123 | +- **Built-in MapLibre preview** — served at `/`, auto-styled per geometry type, fit to bounds. |
| 124 | +- **Correct CRS handling** — reprojects once to Web Mercator for tiling; refuses files with a |
| 125 | + missing CRS instead of guessing silently. |
| 126 | +- **One-file Docker deploy** and a friendly `click` + `rich` CLI. |
| 127 | + |
| 128 | +## How it works |
| 129 | + |
| 130 | +For each requested `z/x/y` tile the server: |
| 131 | + |
| 132 | +1. computes the tile's EPSG:3857 bounding box with [`mercantile`](https://github.com/mapbox/mercantile), |
| 133 | +2. queries the layer's spatial index for intersecting features, |
| 134 | +3. clips them to the (buffered) tile box with Shapely, |
| 135 | +4. affine-transforms the clipped coordinates into the MVT 0–4096 grid (origin top-left, y down), |
| 136 | +5. encodes the result with `mapbox-vector-tile`. |
| 137 | + |
| 138 | +The source file is reprojected to **EPSG:3857 exactly once**, at load time, because web map tiles |
| 139 | +are *defined* in Web Mercator. That is correct here — but do not reuse EPSG:3857 for metric analysis |
| 140 | +(areas, distances, buffers), where it distorts badly; use a projected/UTM CRS for measurement |
| 141 | +instead. For production, bake your explored layers into static PMTiles and serve those; this server |
| 142 | +is for the fast local loop before that step. |
| 143 | + |
| 144 | +## Learn more |
| 145 | + |
| 146 | +Deep dives on the concepts behind this tool, from [python-geospatial.com](https://python-geospatial.com): |
| 147 | + |
| 148 | +- [Vector Tile Pipelines with PMTiles](https://python-geospatial.com/web-mapping-interactive-visualization/vector-tile-pipelines-with-pmtiles/) |
| 149 | +- [Generating PMTiles from GeoParquet](https://python-geospatial.com/web-mapping-interactive-visualization/vector-tile-pipelines-with-pmtiles/generating-pmtiles-from-geoparquet/) |
| 150 | +- [Serving GeoJSON to MapLibre GL JS](https://python-geospatial.com/web-mapping-interactive-visualization/maplibre-gl-vector-web-maps/serving-geojson-to-maplibre-gl-js/) |
| 151 | +- [MapLibre GL Vector Web Maps](https://python-geospatial.com/web-mapping-interactive-visualization/maplibre-gl-vector-web-maps/) |
| 152 | +- [Cloud-Native Geospatial Formats](https://python-geospatial.com/geospatial-data-ingestion-processing-workflows/cloud-native-geospatial-formats/) |
| 153 | + |
| 154 | +## Development |
| 155 | + |
| 156 | +```bash |
| 157 | +git clone https://github.com/python-geospatial/tile-server-lite.git |
| 158 | +cd tile-server-lite |
| 159 | +pip install -e ".[dev]" |
| 160 | + |
| 161 | +ruff check . |
| 162 | +pytest -q |
| 163 | +``` |
| 164 | + |
| 165 | +Tests generate tiny in-memory geometries in `tmp_path` and run entirely offline — no network access |
| 166 | +and no large fixtures required. |
| 167 | + |
| 168 | +## License |
| 169 | + |
| 170 | +[MIT](LICENSE) · Copyright (c) 2026 python-geospatial.com |
0 commit comments