Skip to content

Building a Self-hosted Basemap from Scratch

This guide takes you from nothing to a fully styled vector basemap in your browser, with every part of the stack served from your own machine. At the end, a single Martin instance serves the tiles, the glyphs, the sprites, and the style itself. No API keys and no external services are needed.

A rendered basemap needs four things:

  • a tile archive containing the map data,
  • a style telling the renderer how to draw that data,
  • the glyphs (fonts) the style labels text with, and
  • the sprites (icons) the style marks points of interest with.

We will produce each one, then wire them together.

Prerequisites

We expect you have the following already installed:

All commands below run from one working directory.

Generate a tile archive from OpenStreetMap

We will use Planetiler to build an MBTiles archive of Monaco from OpenStreetMap data. Planetiler downloads the OSM extract for you and tiles it into the OpenMapTiles schema. See Setting up a Basemap and Overlaying Points from PostGIS for a discussion of why a tiling step is needed and what OpenMapTiles is, and our tile source comparison for the pros and cons of the different archive formats.

mkdir --parents data
docker run \
  --user=$UID \
  -e JAVA_TOOL_OPTIONS="-Xmx1g" \
  -v "$(pwd)/data":/data \
  --rm \
  ghcr.io/onthegomap/planetiler:latest \
  --download \
  --minzoom=0 \
  --maxzoom=14 \
  --tile_compression=none \
  --area=monaco \
  --output /data/monaco.mbtiles

Monaco is small, so this finishes in a few minutes. Any other Geofabrik extract name works as --area the same way, with larger downloads and longer runs.

Get a style

A style only works with data in the schema it was written for. Planetiler produced tiles in the OpenMapTiles schema, so we need an OpenMapTiles style. We will use OSM Bright. Positron, Dark Matter, MapTiler Basic, and OSM Liberty are alternatives.

git clone --depth 1 https://github.com/openmaptiles/osm-bright-gl-style.git
mkdir --parents styles
cp osm-bright-gl-style/style.json styles/basemap.json

Attribution

Your map must display © OpenMapTiles and © OpenStreetMap contributors. The tile archive built above carries this attribution in its metadata, and MapLibre displays it automatically.

Get the fonts the style uses

OSM Bright labels text with Noto Sans Regular, Noto Sans Bold, and Noto Sans Italic. Martin generates the glyph ranges MapLibre asks for directly from .ttf/.otf files, so all we need are the font files themselves:

mkdir --parents fonts
curl -L -o fonts/NotoSans-Regular.ttf https://github.com/notofonts/notofonts.github.io/raw/main/fonts/NotoSans/full/ttf/NotoSans-Regular.ttf
curl -L -o fonts/NotoSans-Bold.ttf https://github.com/notofonts/notofonts.github.io/raw/main/fonts/NotoSans/full/ttf/NotoSans-Bold.ttf
curl -L -o fonts/NotoSans-Italic.ttf https://github.com/notofonts/notofonts.github.io/raw/main/fonts/NotoSans/full/ttf/NotoSans-Italic.ttf

Martin publishes each font under the name embedded in the file. Here that is Noto Sans Regular, Noto Sans Bold, and Noto Sans Italic, exactly the names the style asks for.

Get the icons the style uses

The style repository ships its icons as SVG files. Martin generates the spritesheet and index from an SVG directory on the fly, so copying the folder is all that is needed:

cp -r osm-bright-gl-style/icons icons

The directory name becomes the sprite ID, so these will be served under /sprite/icons.

Point the style at your server

The copied style still references the original author's tile, glyph, and sprite servers. Open styles/basemap.json and change three values to point at the Martin instance we are about to start:

Key New value
sources.openmaptiles.url http://localhost:3000/monaco
glyphs http://localhost:3000/font/{fontstack}/{range}
sprite http://localhost:3000/sprite/icons

The sources URL is the TileJSON endpoint of the monaco archive. The {fontstack} and {range} placeholders are filled in by MapLibre when it requests glyph ranges.

Run Martin

Everything is in place, so serve all four resources with one command:

martin data/monaco.mbtiles --font fonts --sprite icons --style styles

Check http://localhost:3000/catalog to see the monaco tile source, the three fonts, the icons sprite, and the basemap style. For more control over what is published and how, see the configuration file documentation. --save-config converts the command line above into a config file to grow from.

View the map

Save this as index.html in the working directory and open it in your browser:

index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>My self-hosted map</title>
  <script src="https://unpkg.com/maplibre-gl@^5/dist/maplibre-gl.js"></script>
  <link rel="stylesheet" href="https://unpkg.com/maplibre-gl@^5/dist/maplibre-gl.css">
  <style>
    html, body, #map { height: 100%; margin: 0; }
  </style>
</head>
<body>
  <div id="map"></div>
  <script>
    const map = new maplibregl.Map({
      container: 'map',
      style: 'http://localhost:3000/style/basemap',
      center: [7.4266, 43.7396],
      zoom: 14,
    });
    map.addControl(new maplibregl.NavigationControl());
  </script>
</body>
</html>

The finished self-hosted basemap of Monaco

The page loads the style from Martin, and the style pulls the tiles, glyphs, and sprites from Martin. The complete map is served from your machine.

Note

Small extracts may not contain every layer the style references. Monaco has no aerodrome, so the browser console warns that the aerodrome_label source layer does not exist. The map renders fine regardless.

Where to go from here