A tiny raster tile server for MapLibre styles.
Point it at any style.json and get back PNG / WebP / JPEG tiles, static images, or map cut-outs.
Quickstart · HTTP API · Library · Deployment
1. Run the server — one command, no config file, no database, no API key.
docker run --rm -p 3000:3000 ghcr.io/kanahiro/chiitiler:latest2. Open a tile in your browser — pass any MapLibre style URL as ?url=.
http://localhost:3000/tiles/0/0/0.png?url=https://tile.openstreetmap.jp/styles/osm-bright/style.json
You're done. That same endpoint works as an XYZ tile source for Leaflet, MapLibre, OpenLayers, QGIS, Cesium, or anywhere else that speaks {z}/{x}/{y}.
No Docker?
npx tsxworks too:git clone https://github.com/Kanahiro/chiitiler && cd chiitiler && npm i npx tsx src/main.ts tile-server --debugThen visit
http://localhost:3000/debugto preview styles interactively.
- Zero-config — no config file, no YAML, no database. Just a style URL.
- Works with any MapLibre style — remote URL or
POSTthe JSON inline - Multiple outputs — slippy tiles (
/tiles), bounding-box clips (/clip), free-form camera shots (/camera) - Serverless-friendly — small footprint, runs on AWS Lambda via Web Adapter (see
cdk/) - Pluggable caching —
memory·file·s3·gcsbackends for shared source assets - Many protocols —
http(s)·s3·gs·file·mbtiles·pmtiles·cog - Library or server — import the renderer directly into your Node.js pipeline
- Built-in debug UI —
/debugand/editorfor live style preview
- MIERUNE/tiles — live example
- PLATEAU VIEW — Cesium.js imagery via
/tiles - qgis-amazonlocationservice-plugin — QGIS integration
- Allmaps Latest — Bluesky bot
- Kumoy - used for thumbnail image of designed maps
| Method | Endpoint | Description |
|---|---|---|
GET / POST |
/tiles/{z}/{x}/{y}.{ext} |
Slippy-map raster tile |
GET / POST |
/clip.{ext} |
Bounding-box cut-out |
GET / POST |
/camera/{zoom}/{lat}/{lon}/{bearing}/{pitch}/{width}x{height}.{ext} |
Free-form camera shot |
GET |
/debug, /editor |
Debug UI (requires --debug) |
ext is one of png, jpeg, jpg, webp.
Query parameters
| Name | Default | Notes |
|---|---|---|
url |
— | Style JSON URL (required for GET) |
tileSize |
512 |
Tile size in pixels |
quality |
100 |
JPEG / WebP quality |
margin |
0 |
Tile edge margin |
bbox |
— | /clip bounding box: minLon,minLat,maxLon,maxLat |
size |
1024 |
/clip longest edge in pixels |
For POST, send the style object as JSON body: { "style": { ... } }.
Chiitiler is also published to npm. Returns Buffer or Sharp streams.
import { getRenderedTileBuffer, ChiitilerCache } from 'chiitiler';
const cache = ChiitilerCache.fileCache({ dir: './.cache', ttl: 3600 });
const png = await getRenderedTileBuffer({
stylejson: 'https://tile.openstreetmap.jp/styles/osm-bright/style.json',
z: 5, x: 27, y: 12,
tileSize: 512,
ext: 'png',
quality: 100,
margin: 0,
cache,
});Available renderers: getRenderedTileBuffer, getRenderedClipBuffer, getRenderedCameraBuffer, and their *Stream variants (Sharp instances for further piping).
For library use, explicitly call prewarm(cache) once per rendering process
during application startup, before accepting requests:
import { prewarm, ChiitilerCache } from 'chiitiler';
const cache = ChiitilerCache.noneCache();
await prewarm(cache);The promise resolves after a minimal tile has been rendered and PNG-encoded.
It rejects if warmup fails; the application decides whether to continue startup.
Library imports do not automatically run prewarm, and CHIITILER_PREWARM only
controls the CLI server. Style-specific tiles, glyphs, and sprites are not preloaded.
Types are re-exported as well, so you can annotate call sites without depending on
@maplibre/maplibre-gl-style-spec resolution in your own tree:
import type {
StyleSpecification,
GetRenderedTileOptions,
GetRenderedClipOptions,
GetRenderedCameraOptions,
SupportedFormat,
Cache,
} from 'chiitiler';Prefer chiitiler's StyleSpecification over importing it from
@maplibre/maplibre-gl-style-spec directly: if your tree resolves a different major of
that package, the two structurally-different types are not assignable to each other.
chiitiler doesn't override the User-Agent on outbound HTTP requests by default, so the runtime's default is used (Node sends node). Some tile providers (e.g. OpenStreetMap) require an identifying User-Agent — set one with setUserAgent('my-app/1.0') or the CHIITILER_USER_AGENT environment variable.
All options can be set via CLI flag or environment variable.
| Flag | Env | Default |
|---|---|---|
--port <n> |
CHIITILER_PORT |
3000 |
--debug |
CHIITILER_DEBUG |
false |
--user-agent <ua> |
CHIITILER_USER_AGENT |
(none) |
--prewarm [true|false] |
CHIITILER_PREWARM |
true |
--processes <n> |
CHIITILER_PROCESSES |
1 (set 0 for all CPUs) |
Prewarm is enabled by default: one tile is rendered at startup before the server starts listening, moving shared renderer initialization work into startup. --prewarm false or CHIITILER_PREWARM=false disables it. --prewarm alone enables it. The CLI flag takes precedence over the environment variable. Style-specific tiles, glyphs, and sprites are not preloaded.
--processes takes precedence over CHIITILER_PROCESSES and accepts non-negative integers; 0 uses all available CPUs.
Renderer Maps are reused per style and rendering mode without an idle timeout. They are released when their pool is evicted from the 10-entry LRU or explicitly closed. This avoids repeated initialization after quiet periods, at the cost of retaining native renderer memory between requests.
| Flag | Env | Default |
|---|---|---|
--cache <none|memory|file|s3|gcs> |
CHIITILER_CACHE_METHOD |
none |
--cache-ttl <seconds> |
CHIITILER_CACHE_TTL_SEC |
3600 |
--memory-cache-max-item-count <n> |
CHIITILER_MEMORYCACHE_MAXITEMCOUNT |
1000 |
--front-cache-ttl-sec <seconds> |
CHIITILER_FRONT_CACHE_TTL_SEC |
10 (seconds) |
--front-cache-max-bytes <bytes> |
CHIITILER_FRONT_CACHE_MAX_BYTES |
67108864 (64 MiB) |
--file-cache-dir <dir> |
CHIITILER_FILECACHE_DIR |
./.cache |
--s3-cache-bucket <name> |
CHIITILER_S3CACHE_BUCKET |
— |
--s3-region <region> |
CHIITILER_S3_REGION |
us-east-1 |
--s3-endpoint <url> |
CHIITILER_S3_ENDPOINT |
— |
--s3-force-path-style |
CHIITILER_S3_FORCE_PATH_STYLE |
false |
--gcs-cache-bucket <name> |
CHIITILER_GCS_CACHE_BUCKET |
— |
--gcs-project-id <id> |
CHIITILER_GCS_PROJECT_ID |
— |
--gcs-key-filename <path> |
CHIITILER_GCS_KEY_FILENAME |
— |
--gcs-cache-prefix <prefix> |
CHIITILER_GCS_CACHE_PREFIX |
— |
--gcs-api-endpoint <url> |
CHIITILER_GCS_API_ENDPOINT |
— |
Chiitiler caches source assets (vector tiles, glyphs, sprites) — not final rasters — so cached data is reused across requests. Standard AWS / GCP credentials (AWS_ACCESS_KEY_ID, GOOGLE_APPLICATION_CREDENTIALS, etc.) are respected.
The tile server adds a per-process memory front cache to file, s3, and gcs:
by default, buffers are retained for 10 seconds from insertion, with an LRU limit of 64 MiB
of buffer payloads (JavaScript overhead is additional). Reads promote backing-cache
hits into memory; writes populate memory immediately and also write to the backing
cache. none and memory keep their existing behavior. This affects sources that
already use the cache, not direct s3://, gs://, or local source reads.
Set --front-cache-ttl-sec / CHIITILER_FRONT_CACHE_TTL_SEC to a positive, finite number of seconds and
--front-cache-max-bytes / CHIITILER_FRONT_CACHE_MAX_BYTES to a positive integer number of bytes to override
these limits. CLI flags take precedence over environment variables. Setting either limit to 0 disables the front cache and uses the
backing cache directly. Otherwise, invalid values stop startup when using file,
s3, or gcs.
These settings do not affect none, memory, or library callers.
The front-cache TTL does not guarantee source freshness: promotion can retain a
value for up to the configured TTL beyond its backing-cache expiry, and the S3/GCS cache
implementations do not check expiry on reads. Each worker has its own memory limit.
Library callers can opt in and customize the limits (either limit set to 0
returns the backing cache unchanged):
const cache = ChiitilerCache.withMemoryCache(
ChiitilerCache.fileCache({ dir: './.cache', ttl: 3600 }),
{ ttlSeconds: 10, maxBytes: 64 * 1024 * 1024 },
);- Docker —
ghcr.io/kanahiro/chiitiler:latest(entrypoint:tile-server) - Docker Compose — see
docker-compose.yml(includes RustFS + fake-gcs-server for local testing) - AWS Lambda — ready-to-deploy CDK app in
cdk/
Requires Node.js 24.12+ and sharp system deps (see Dockerfile).
git clone https://github.com/Kanahiro/chiitiler.git
cd chiitiler
npm install
npm run dev # tsx watch mode
npm run test:unit # vitest
npm run test:integration # end-to-end
npm run test:benchmark # see bench/BENCHMARK.md
npm run build # bundle to build/main.cjsgraph LR
subgraph sources
direction LR
A[style.json]
B[z/x/y.pbf]
C[z/x/y.png/webp/jpg]
D[sprite]
E[glyphs]
end
subgraph chiitiler
cache
render
server
end
sources --> cache --> render --> server --/tiles/z/x/y--> png/webp/jpg
cache <--get/set--> memory/file/s3/gcs
Inspired by maptiler/tileserver-gl and developmentseed/titiler.
MIT © Kanahiro Iguchi