High-performance vector tile server built in Rust with a modern Nuxt 4 frontend.
- PMTiles Support - Serve tiles from local and remote PMTiles archives
- MBTiles Support - Serve tiles from SQLite-based MBTiles files
- Native Raster Rendering - Generate PNG/JPEG/WebP tiles using MapLibre Native (C++ FFI)
- MLT (MapLibre Tiles) - Serve and transcode MLT tiles with MLT↔MVT on-the-fly conversion (feature-gated)
- PostgreSQL Out-DB Rasters - Serve VRT/COG tiles via PostGIS functions with dynamic filtering
- OGC API Features - PostGIS tables as OGC-spec collections: read, filter with CQL2, transform CRS, write (CRUD), and introspect schemas
- STAC Catalog Sources - Serve COGs directly from any STAC API (static, dynamic per-tile bbox, multi-asset mosaic)
- Static Map Images - Create embeddable map screenshots (like Mapbox/Maptiler Static API)
- Zero-Config Auto-Detect - Point at a directory or file and start serving instantly
- Hot Reload - Reload configuration via
SIGHUPsignal or admin API without downtime - High Performance - ~100ms per tile (warm cache), ~800ms cold cache
- TileJSON 3.0 - Full TileJSON metadata API
- MapLibre GL JS - Built-in map viewer and data inspector
- Docker Ready - Easy deployment with Docker Compose v2
- Fast - Built in Rust with Axum for maximum performance
- Backend: Rust 1.75+, Axum 0.8, Tokio
- Native Rendering: MapLibre Native (C++) via FFI bindings
- Frontend: Nuxt 4, Vue 3.5, Tailwind CSS v4, shadcn-vue
- Tooling: Bun workspaces, Docker multi-stage builds
- Features
- Tech Stack
- Requirements
- Quick Start
- Installation
- Configuration
- API Endpoints
- Deploy
- Development
- Contributing
- Author
- Rust 1.75+
- Bun 1.0+
- (Optional) Docker
Native raster tile rendering requires building MapLibre Native. If you don't need raster tiles, the server runs without it (stub implementation returns placeholder images).
macOS (Apple Silicon/Intel):
# Install build dependencies
brew install ninja ccache libuv glfw bazelisk cmake
# Build MapLibre Native
cd crates/mbgl-sys/vendor/maplibre-native
git submodule update --init --recursive
cmake --preset macos-metal
cmake --build build-macos-metal --target mbgl-core mlt-cpp -j8Linux:
# Install build dependencies (Ubuntu/Debian)
apt-get install ninja-build ccache libuv1-dev libglfw3-dev cmake
# Build MapLibre Native
cd crates/mbgl-sys/vendor/maplibre-native
git submodule update --init --recursive
cmake --preset linux
cmake --build build-linux --target mbgl-core mlt-cpp -j8After building MapLibre Native:
# Clear Cargo's cached build to detect the new libraries
cd /path/to/tileserver-rs
rm -rf target/release/build/mbgl-sys-*
cargo build --releaseYou should see Building with real MapLibre Native renderer in the build output.
# Zero-config: point at a directory of tile files
./tileserver-rs /path/to/data
# Or with an explicit config file
./tileserver-rs --config config.toml
# Using Docker
docker compose up -dThe server auto-detects .pmtiles, .mbtiles, style.json, fonts, and GeoJSON files from the given path. See the Auto-Detect Guide for details.
# Add the tap and install
brew tap vinayakkulkarni/tileserver-rs https://github.com/vinayakkulkarni/tileserver-rs
brew install vinayakkulkarni/tileserver-rs/tileserver-rs
# Run the server
tileserver-rs --config config.tomlDownload the latest release from GitHub Releases.
| Platform | Architecture | Download |
|---|---|---|
| macOS | Apple Silicon (ARM64) | tileserver-rs-aarch64-apple-darwin.tar.gz |
| macOS | Intel (x86_64) | tileserver-rs-x86_64-apple-darwin.tar.gz |
| Linux | x86_64 | tileserver-rs-x86_64-unknown-linux-gnu.tar.gz |
| Linux | ARM64 | tileserver-rs-aarch64-unknown-linux-gnu.tar.gz |
# macOS ARM64 (Apple Silicon)
curl -L https://github.com/vinayakkulkarni/tileserver-rs/releases/latest/download/tileserver-rs-aarch64-apple-darwin.tar.gz | tar xz
chmod +x tileserver-rs
# Remove macOS quarantine (required for unsigned binaries)
xattr -d com.apple.quarantine tileserver-rs
# Linux x86_64
curl -L https://github.com/vinayakkulkarni/tileserver-rs/releases/latest/download/tileserver-rs-x86_64-unknown-linux-gnu.tar.gz | tar xz
chmod +x tileserver-rs
# Run
./tileserver-rs --config config.tomlmacOS Security Note: If you download via a browser, macOS Gatekeeper will block the unsigned binary. Either use the
curlcommand above, or after downloading, runxattr -d com.apple.quarantine <binary>to remove the quarantine flag. Alternatively, right-click the binary in Finder and select "Open".
# Development (builds locally, mounts ./data directory)
docker compose -f deploy/compose.yml up -d
# Production (uses pre-built image with resource limits)
docker compose -f deploy/compose.yml -f deploy/compose.prod.yml up -d
# View logs
docker compose -f deploy/compose.yml logs -f tileserver
# Stop
docker compose -f deploy/compose.yml downOr run directly with Docker:
docker run -d \
-p 8080:8080 \
-v /path/to/data:/data:ro \
-v /path/to/config.toml:/app/config.toml:ro \
ghcr.io/vinayakkulkarni/tileserver-rs:latest# Clone the repository with submodules
git clone --recursive git@github.com:vinayakkulkarni/tileserver-rs.git
cd tileserver-rs
# Or using HTTPS
git clone --recursive https://github.com/vinayakkulkarni/tileserver-rs.git
# If you already cloned without --recursive:
git submodule update --init --recursive
# Install dependencies
bun install
# Build the Rust backend
cargo build --release
# Build with MLT transcoding support (optional)
cargo build --release --features mlt
# Build the frontend
bun run build:client
# Run the server
./target/release/tileserver-rs --config config.tomlNote: The
--recursiveflag fetches the MapLibre Native submodule (~200MB) required for native raster rendering. If the clone times out, usegit submodule update --init --depth 1for a shallow clone. See CONTRIBUTING.md for detailed setup instructions.
Create a config.toml file. Important: Root-level options (fonts, files) must come before any [section] headers:
# Root-level options (must come BEFORE [sections])
fonts = "/data/fonts"
files = "/data/files"
[server]
host = "0.0.0.0"
port = 8080
cors_origins = ["*", "https://example.com"] # Supports multiple origins
# Admin server bind address for hot-reload endpoint (default: disabled)
# admin_bind = "127.0.0.1:9099"
[telemetry]
enabled = false
[[sources]]
id = "openmaptiles"
type = "pmtiles"
path = "/data/tiles.pmtiles"
name = "OpenMapTiles"
attribution = "© OpenMapTiles © OpenStreetMap contributors"
[[sources]]
id = "terrain"
type = "mbtiles"
path = "/data/terrain.mbtiles"
name = "Terrain Data"
[[styles]]
id = "osm-bright"
path = "/data/styles/osm-bright/style.json"
# PostgreSQL Out-of-Database Rasters (optional)
[postgres]
connection_string = "postgresql://user:pass@localhost:5432/gis"
[[postgres.outdb_rasters]]
id = "imagery" # Also used as function name if 'function' is omitted
schema = "public"
# function = "get_raster_paths" # Optional: defaults to 'id' value
name = "Satellite Imagery"Enable the admin server by setting admin_bind in [server]:
[server]
admin_bind = "127.0.0.1:9099"This exposes POST /__admin/reload on a separate port for reloading configuration without restarting the server. You can also send SIGHUP to the process for the same effect. See the Hot Reload Guide for details.
See data/configs/example.toml for a complete example, or data/configs/offline.toml for a local development setup.
| Endpoint | Description |
|---|---|
GET /health |
Health check (returns OK) |
GET /ping |
Runtime metadata (config hash, loaded sources/styles, version) |
POST /__admin/reload |
Hot-reload configuration (admin server only) |
POST /__admin/reload?flush=true |
Force reload even if config unchanged |
| Endpoint | Description |
|---|---|
GET /data.json |
List all tile sources |
GET /data/{source}.json |
TileJSON for a source |
GET /data/{source}/{z}/{x}/{y}.{format} |
Get a vector tile (.pbf, .mvt, .mlt) |
GET /data/{source}/{z}/{x}/{y}.geojson |
Get tile as GeoJSON (for debugging) |
| Endpoint | Description |
|---|---|
GET /styles.json |
List all styles |
GET /styles/{style}/style.json |
Get MapLibre GL style JSON |
GET /styles/{style}/sprite[@2x].{png,json} |
Get sprite image/metadata |
GET /styles/{style}/wmts.xml |
WMTS capabilities (for QGIS/ArcGIS) |
| Endpoint | Description |
|---|---|
GET /fonts.json |
List available font families |
GET /fonts/{fontstack}/{range}.pbf |
Get font glyphs (PBF format) |
| Endpoint | Description |
|---|---|
GET /files/{filepath} |
Serve static files (GeoJSON, icons, etc.) |
GET /index.json |
Combined TileJSON for all sources and styles |
| Endpoint | Description |
|---|---|
GET /data/{outdb_source}/{z}/{x}/{y}.{format} |
Raster tile from PostgreSQL-referenced VRT/COG |
GET /data/{outdb_source}/{z}/{x}/{y}.{format}?satellite=... |
With dynamic filtering via query params |
| Endpoint | Description |
|---|---|
GET /styles/{style}/{z}/{x}/{y}[@{scale}x].{format} |
Raster tile (PNG/JPEG/WebP) |
GET /styles/{style}/static/{type}/{size}[@{scale}x].{format} |
Static map image |
Raster Tile Examples:
/styles/protomaps-light/14/8192/5461.png # 512x512 PNG @ 1x
/styles/protomaps-light/14/8192/5461@2x.webp # 1024x1024 WebP @ 2x (retina)
Performance:
- Warm cache: ~100ms per tile
- Cold cache: ~700-800ms per tile (includes tile fetching)
- Static images: ~3s for 800x600
Static Image Types:
- Center:
{lon},{lat},{zoom}[@{bearing}[,{pitch}]]/styles/protomaps-light/static/-122.4,37.8,12/800x600.png /styles/protomaps-light/static/-122.4,37.8,12@45,60/800x600@2x.webp - Bounding Box:
{minx},{miny},{maxx},{maxy}/styles/protomaps-light/static/-123,37,-122,38/1024x768.jpeg - Auto-fit:
auto(with?path=or?marker=query params)/styles/protomaps-light/static/auto/800x600.png?path=path-5+f00(-122.4,37.8|-122.5,37.9)
Static Image Limits:
- Maximum dimensions: 4096x4096 pixels
- Maximum scale: 4x
# Install dependencies
bun install
# Start Rust backend (with hot reload via cargo-watch)
cargo watch -x run
# Start Nuxt frontend (in another terminal)
bun run dev:client
# Start marketing site (landing page)
bun run dev:marketing
# Run linters
bun run lint
cargo clippy
# Build for production
cargo build --release
bun run build:client| Feature | Description |
|---|---|
http |
Enable serving PMTiles from remote HTTP URLs |
mlt |
Enable MLT (MapLibre Tiles) transcoding support |
# Build with MLT support
cargo build --release --features mlt
# Build with all optional features
cargo build --release --features http,mlttileserver-rs/
├── apps/
│ ├── client/ # Nuxt 4 frontend (embedded in binary)
│ ├── docs/ # Documentation site (docs.tileserver.app)
│ └── marketing/ # Landing page (tileserver.app)
├── benchmarks/ # Load-test harness vs titiler/martin/tileserver-gl
├── crates/
│ ├── tileserver-rs/ # Binary crate (Rust backend)
│ │ ├── src/ # main.rs, admin, config, render, sources, …
│ │ ├── benches/ # criterion benchmarks (mlt, cache, raster)
│ │ └── tests/ # integration tests
│ └── mbgl-sys/ # FFI bindings to MapLibre Native (C++)
│ ├── cpp/ # C wrapper over mbgl::*
│ ├── src/lib.rs # Rust FFI bindings
│ └── vendor/maplibre-native/ # MapLibre Native source (submodule)
├── data/ # Runtime assets + configs
│ ├── configs/ # tileserver-rs config files (TOML)
│ │ ├── example.toml # Reference configuration
│ │ ├── offline.toml # Offline/local development
│ │ ├── dev.toml # Development config
│ │ ├── dev-postgres.toml # Development with PostGIS + OGC API
│ │ ├── geoparquet.toml # GeoParquet source testing
│ │ ├── duckdb.toml # DuckDB source testing
│ │ └── benchmark-raster.toml # Raster benchmark config
│ ├── styles/ # MapLibre GL style JSONs
│ ├── tiles/, fonts/, raster/, overture/, postgres-dev/
├── deploy/ # Deployment manifests
│ ├── compose.yml # Docker Compose (base)
│ ├── compose.dev.yml # Docker Compose (development overrides)
│ ├── compose.prod.yml # Docker Compose (production overrides)
│ └── docker-entrypoint.sh # Container entrypoint
├── homebrew/ # Homebrew formula (tileserver-rs.rb)
├── Cargo.toml # Virtual workspace manifest
├── Cargo.lock
├── Dockerfile # Multi-stage build (Rust + Node + MapLibre)
├── fly.toml, railway.toml, render.yaml # One-click platform configs
└── release-please-config.json + .release-please-manifest.json
Deploy a fully working tileserver-rs instance with sample data in minutes. No configuration needed — sample tile data is automatically downloaded on first start.
When the Docker container starts with an empty /data directory (no tile files mounted), it automatically downloads sample data (~15 MB) from the latest GitHub release. This includes:
- Protomaps sample tiles (PMTiles) — world basemap extract
- Zurich MBTiles — detailed city extract
- Noto Sans fonts — for label rendering
- Protomaps Light style — ready-to-use map style
- Sample raster data — COG test files
To use your own data, mount a volume at /data:
docker run -d -p 8080:8080 -v /path/to/your/data:/data:ro ghcr.io/vinayakkulkarni/tileserver-rs:latestSet SAMPLE_DATA_VERSION=v2.12.1 to pin a specific release version instead of latest.
# Install flyctl: https://fly.io/docs/flyctl/install/
fly launch --copy-config
fly deployThe included fly.toml configures auto-stop/start machines, health checks, and 512MB RAM. Add a persistent volume for your own tile data:
fly volumes create tile_data --size 10 --region iadThe docs site is deployed automatically via Cloudflare Pages (linked repo). Any changes to docs/ trigger a rebuild.
The marketing/landing page is deployed via GitHub Actions to a separate CF Pages project.
Setup (one-time):
- Create a new CF Pages project named
tileserver-marketing(Direct Upload, not linked to repo) - Add custom domain
tileserver.appto the project - Add these secrets to GitHub repo settings:
CLOUDFLARE_API_TOKEN- API token with "Cloudflare Pages: Edit" permissionCLOUDFLARE_ACCOUNT_ID- Your Cloudflare account ID
Deployments are triggered on push to main when files in marketing/ change.
This project uses release-please for automated releases based on Conventional Commits.
How it works:
- Commits to
mainwith conventional commit messages (feat:,fix:, etc.) trigger release-please to create/update a Release PR - The Release PR contains version bumps in
Cargo.toml,apps/client/package.json,homebrew/Formula/tileserver-rs.rb, and changelog updates - Merging the Release PR pushes a
vX.Y.Ztag, which triggers the Linux/macOS binary, Docker image, sample-data, and Homebrew formula workflows
Version bumping:
feat:commits → minor version (0.1.0 → 0.2.0)fix:commits → patch version (0.1.0 → 0.1.1)feat!:orBREAKING CHANGE:→ major version (0.1.0 → 1.0.0)
Release artifacts:
- GitHub Release with changelog
- macOS ARM64 binary (
.tar.gz) - Linux x86_64 + ARM64 binaries — full (with MapLibre Native) and headless variants
- Docker image (
ghcr.io/vinayakkulkarni/tileserver-rs) — multi-archlinux/amd64+linux/arm64 - Homebrew formula auto-update
When release-please merges a Release PR that bumps both tileserver-rs AND mbgl-sys (our two-package workspace), both tags (v2.x.y and mbgl-sys-v0.1.z) push simultaneously. This triggers build-mbgl-native.yml and release-docker-images.yml in parallel — but the Docker workflow starts with a pre-flight Verify mbgl-sys assets exist step that 404s because build-mbgl-native hasn't finished uploading release assets yet (build takes 10–15 min).
Symptom: Release Docker Images run fails within seconds, job Verify mbgl-sys assets exist = failure, all downstream build jobs = skipped.
Fix: wait for Build MapLibre Native run to conclude success, then rerun the failed Docker workflow:
gh run list --workflow release-docker-images.yml --limit 1 --json databaseId --jq '.[].databaseId' \
| xargs -I{} gh run rerun {}The pre-flight gate is intentional and load-bearing — it prevents shipping a Docker image that points at non-existent mbgl-sys binaries. Do not remove it. Upstream fix blocked on release-please not supporting cross-tag workflow_run chaining (build-mbgl-native triggers on mbgl-sys-v*, Docker on v*).
Observed on v2.26.0, v2.26.2. Manual rerun is the idiomatic workaround.
We welcome contributions! Please see CONTRIBUTING.md for detailed guidelines.
Quick Start:
- Fork it (https://github.com/vinayakkulkarni/tileserver-rs/fork)
- Clone with submodules:
git clone --recursive <your-fork-url> - Create your feature branch (
git checkout -b feat/new-feature) - Commit your changes (
git commit -Sam 'feat: add feature') - Push to the branch (
git push origin feat/new-feature) - Create a new Pull Request
Working with Git Submodules:
# After cloning (if you forgot --recursive)
git submodule update --init --recursive
# After pulling changes from upstream
git pull
git submodule update --init --recursive
# If clone times out (shallow clone)
git submodule update --init --depth 1Notes:
- Please contribute using GitHub Flow
- Commits & PRs will be allowed only if the commit messages & PR titles follow the conventional commit standard
- Ensure your commits are signed. Read why
tileserver-rs © Vinayak, Released under the MIT License.
Authored and maintained by Vinayak Kulkarni with help from contributors (list).
vinayakkulkarni.dev · GitHub @vinayakkulkarni · Twitter @_vinayak_k
- tileserver-gl - Inspiration for this project
- MapLibre - Open-source mapping library
- PMTiles - Cloud-optimized tile archive format
- PostGIS - Spatial database extension for PostgreSQL
