Skip to content

vinayakkulkarni/tileserver-rs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1,290 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

tileserver-rs 🦀

CI Pipeline Docker Coverage mbgl-sys on crates.io mbgl-sys on docs.rs

Deploy on Railway Deploy on Render Deploy to DO

tileserver-rs logo

High-performance vector tile server built in Rust with a modern Nuxt 4 frontend.

Features

  • 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 SIGHUP signal 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

Tech Stack

  • 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

Table of Contents

Requirements

For Native Rendering (Optional)

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 -j8

Linux:

# 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 -j8

After 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 --release

You should see Building with real MapLibre Native renderer in the build output.

Quick Start

# 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 -d

The server auto-detects .pmtiles, .mbtiles, style.json, fonts, and GeoJSON files from the given path. See the Auto-Detect Guide for details.

Installation

Using Homebrew (macOS)

# 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.toml

Pre-built Binaries

Download 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.toml

macOS Security Note: If you download via a browser, macOS Gatekeeper will block the unsigned binary. Either use the curl command above, or after downloading, run xattr -d com.apple.quarantine <binary> to remove the quarantine flag. Alternatively, right-click the binary in Finder and select "Open".

Using Docker

# 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 down

Or 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

Building from Source

# 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.toml

Note: The --recursive flag fetches the MapLibre Native submodule (~200MB) required for native raster rendering. If the clone times out, use git submodule update --init --depth 1 for a shallow clone. See CONTRIBUTING.md for detailed setup instructions.

Configuration

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"

Admin Server (Hot Reload)

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.

API Endpoints

Health & Admin Endpoints

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

Data Endpoints (Vector Tiles)

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)

Style Endpoints

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)

Font Endpoints

Endpoint Description
GET /fonts.json List available font families
GET /fonts/{fontstack}/{range}.pbf Get font glyphs (PBF format)

Other Endpoints

Endpoint Description
GET /files/{filepath} Serve static files (GeoJSON, icons, etc.)
GET /index.json Combined TileJSON for all sources and styles

PostgreSQL Out-DB Raster Endpoints

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

Rendering Endpoints (Native MapLibre)

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

Development

# 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

Cargo Feature Flags

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,mlt

Project Structure

tileserver-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

One-Click Cloud Deploy

Deploy a fully working tileserver-rs instance with sample data in minutes. No configuration needed — sample tile data is automatically downloaded on first start.

Platform Deploy Notes
Render Deploy on Render Uses render.yaml blueprint
DigitalOcean Deploy to DO Uses .do/deploy.template.yaml
Railway Deploy on Railway Uses railway.toml config
Fly.io fly launch --copy-config Uses fly.toml — see below
Docker docker compose -f deploy/compose.yml up -d Uses deploy/compose.yml (already included)

How Sample Data Works

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:latest

Set SAMPLE_DATA_VERSION=v2.12.1 to pin a specific release version instead of latest.

Deploy on Fly.io

# Install flyctl: https://fly.io/docs/flyctl/install/
fly launch --copy-config
fly deploy

The 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 iad

Deployments

Documentation Site (docs.tileserver.app)

The docs site is deployed automatically via Cloudflare Pages (linked repo). Any changes to docs/ trigger a rebuild.

Marketing Site (tileserver.app)

The marketing/landing page is deployed via GitHub Actions to a separate CF Pages project.

Setup (one-time):

  1. Create a new CF Pages project named tileserver-marketing (Direct Upload, not linked to repo)
  2. Add custom domain tileserver.app to the project
  3. Add these secrets to GitHub repo settings:
    • CLOUDFLARE_API_TOKEN - API token with "Cloudflare Pages: Edit" permission
    • CLOUDFLARE_ACCOUNT_ID - Your Cloudflare account ID

Deployments are triggered on push to main when files in marketing/ change.

Releases

This project uses release-please for automated releases based on Conventional Commits.

How it works:

  1. Commits to main with conventional commit messages (feat:, fix:, etc.) trigger release-please to create/update a Release PR
  2. The Release PR contains version bumps in Cargo.toml, apps/client/package.json, homebrew/Formula/tileserver-rs.rb, and changelog updates
  3. Merging the Release PR pushes a vX.Y.Z tag, 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!: or BREAKING 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-arch linux/amd64 + linux/arm64
  • Homebrew formula auto-update

Known release race: retry Docker after mbgl-sys build

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.

Contributing

We welcome contributions! Please see CONTRIBUTING.md for detailed guidelines.

Quick Start:

  1. Fork it (https://github.com/vinayakkulkarni/tileserver-rs/fork)
  2. Clone with submodules: git clone --recursive <your-fork-url>
  3. Create your feature branch (git checkout -b feat/new-feature)
  4. Commit your changes (git commit -Sam 'feat: add feature')
  5. Push to the branch (git push origin feat/new-feature)
  6. 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 1

Notes:

  1. Please contribute using GitHub Flow
  2. Commits & PRs will be allowed only if the commit messages & PR titles follow the conventional commit standard
  3. Ensure your commits are signed. Read why

Author

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

Special Thanks

  • tileserver-gl - Inspiration for this project
  • MapLibre - Open-source mapping library
  • PMTiles - Cloud-optimized tile archive format
  • PostGIS - Spatial database extension for PostgreSQL

About

High-performance Rust tile server for PMTiles, MBTiles, PostGIS, and Cloud Optimized GeoTIFFs

Topics

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors