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)
- PostgreSQL Out-DB Rasters - Serve VRT/COG tiles via PostGIS functions with dynamic filtering
- Static Map Images - Create embeddable map screenshots (like Mapbox/Maptiler Static API)
- 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
- 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 maplibre-native-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 maplibre-native-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/maplibre-native-sys-*
cargo build --releaseYou should see Building with real MapLibre Native renderer in the build output.
# Using Docker
docker compose up -d
# Or build from source
cargo build --release
./target/release/tileserver-rs --config config.toml# 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 up -d
# Production (uses pre-built image with resource limits)
docker compose -f compose.yml -f compose.prod.yml up -d
# View logs
docker compose logs -f tileserver
# Stop
docker compose 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 [email protected]: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 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
[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"See config.example.toml for a complete example, or config.offline.toml for a local development setup.
| Endpoint | Description |
|---|---|
GET /health |
Health check |
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) |
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/[email protected] # 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/[email protected] - 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:clienttileserver-rs/
βββ apps/
β βββ client/ # Nuxt 4 frontend (embedded in binary)
βββ docs/ # Documentation site (docs.tileserver.app)
βββ marketing/ # Landing page (tileserver.app)
βββ maplibre-native-sys/ # FFI bindings to MapLibre Native (C++)
β βββ cpp/ # C/C++ wrapper code
β β βββ maplibre_c.h # C API header
β β βββ maplibre_c.cpp # C++ implementation
β βββ src/lib.rs # Rust FFI bindings
β βββ build.rs # Build script
β βββ vendor/maplibre-native/ # MapLibre Native source (submodule)
βββ src/ # Rust backend
β βββ main.rs # Entry point, routes
β βββ config.rs # Configuration
β βββ error.rs # Error types
β βββ render/ # Native MapLibre rendering
β β βββ pool.rs # Renderer pool (per scale factor)
β β βββ renderer.rs # High-level render API
β β βββ native.rs # Safe Rust wrappers around FFI
β β βββ types.rs # RenderOptions, ImageFormat, etc.
β βββ sources/ # Tile source implementations
β βββ styles/ # Style management + rewriting
βββ compose.yml # Docker Compose (development)
βββ compose.prod.yml # Docker Compose (production overrides)
βββ Dockerfile # Multi-stage Docker build
βββ config.example.toml # Example configuration
The 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. The release process is fully automated based on Conventional Commits.
How it works:
- Commits to
mainwith conventional commit messages (feat:,fix:, etc.) trigger Release Please - Release Please creates/updates a Release PR with version bumps and changelog
- Merging the Release PR creates a GitHub Release and triggers platform builds
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) - Docker image (
ghcr.io/vinayakkulkarni/tileserver-rs) - Homebrew formula auto-update
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
