Self-Hosting Mapbox Tiles – Guide to Self-Hosting Mapbox Vector Tiles

mapboxmbtilestile-servervectorvector-tiles

As presented in a talk at FOSS4G Mapbox Studio allows to create Mapbox vector tiles and export them as a .mbtiles file.

The mapbox-gl.js library can be used to dynamically style and render Mapbox vector tiles on client (browser) side.

The missing part: How can I self-host Mapbox vector tiles (.mbtiles) so that I can consume them with mapbox-gl.js?

I know that Mapbox Studio can upload the vector tiles to the Mapbox server and let it host the tiles. But that's no option for me, I want to host the vector tiles on my own server.


The TileStream approach below turned out to be a dead end. See my answer for a working solution with Tilelive.


I tried TileStream which can serve image tiles out of .mbtiles files:

My webpage uses mapbox-gl v0.4.0:

<link href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.4.0/mapbox-gl.css' rel='stylesheet' />
<script src='https://api.tiles.mapbox.com/mapbox-gl-js/v0.4.0/mapbox-gl.js'></script>

and it creates a mapboxgl.Map in a JavaScript script:

  var map = new mapboxgl.Map({
    container: 'map',
    center: [46.8104, 8.2452],
    zoom: 9,
    style: 'c.json'
  });

The c.json style file configures the vector tile source:

{
  "version": 6,
  "sprite": "https://www.mapbox.com/mapbox-gl-styles/sprites/bright",
  "glyphs": "mapbox://fontstack/{fontstack}/{range}.pbf",
  "constants": {
    "@land": "#808080",
    "@earth": "#805040",
    "@water": "#a0c8f0",
    "@road": "#000000"
  },
  "sources": {
    "osm_roads": {
      "type": "vector",
      "url": "tile.json"
    }
  },
  "layers": [{
    "id": "background",
    "type": "background",
    "paint": {
      "background-color": "@land"
    }
  }, {
    "id": "roads",
    "type": "line",
    "source": "osm_roads",
    "source-layer": "roads",
    "paint": {
      "line-color": "@road"
    }
  }]
}

… with the following TileJSON specification in tile.json:

{
  "tilejson": "2.1.0",
  "tiles": [
    "http://localhost:8888/v2/osm_roads/{z}/{x}/{y}.png"
  ],
  "minzoom": 0,
  "maxzoom": 12
}

… which points to my TileStream server running at localhost:8888.
TileStream has been started with:

node index.js start --tiles="..\tiles"

… where the ..\tiles folder contains my osm_roads.mbtiles file.

With this setup, I can open my webpage but only see the background layer. In the browser network trace I can see that tiles are indeed loaded when I zoom in, but the browser JavaScript error console contains several errors of the form

Error: Invalid UTF-8 codepoint: 160      in mapbox-gl.js:7

Since vector tiles are not .png images but rather ProtoBuf files, the tiles URL http://localhost:8888/v2/osm_roads/{z}/{x}/{y}.pbf would actually make more sense, but that doesn't work.

Any ideas?

Best Answer

As pointed out by @Greg, instead of TileStream (my first attempt) you should use Tilelive to host your own vector tiles.

Tilelive isn't a server itself but a backend framework that deals with tiles in different formats from different sources. But it's based on Node.js so you can turn it into a server in a pretty straight-forward way. To read tiles from a .mbtiles source as exported by Mapbox Studio, you need the node-mbtiles tilelive module.

Side note: Current Mapbox Studio has a bug under Windows and OS X that prevents an exported .mbtiles file to show up at your chosen destination. Workaround: Just grab the latest export-xxxxxxxx.mbtiles file in ~/.mapbox-studio/cache.

I found two server implementations (ten20 tile server by alexbirkett and TileServer by hanchao) who both use Express.js as a web app server.

Here is my minimalistic approach which is loosely based on these implementations:

  1. Install Node.js

  2. Grab the node packages with npm install @mapbox/tilelive @mapbox/mbtiles express

  3. Implement the server in the file server.js:

     var express = require('express');
     var http = require('http');
     var app = express();
     var tilelive = require('tilelive');
     require('mbtiles').registerProtocols(tilelive);
    
     //Depending on the OS the path might need to be 'mbtiles:///' on OS X and linux
     tilelive.load('mbtiles://path/to/osm_roads.mbtiles', function(err, source) {
    
         if (err) {
             throw err;
         }
         app.set('port', 7777);
    
         app.use(function(req, res, next) {
             res.header("Access-Control-Allow-Origin", "*");
             res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
             next();
         });
    
         app.get(/^\/v2\/tiles\/(\d+)\/(\d+)\/(\d+).pbf$/, function(req, res){
    
             var z = req.params[0];
             var x = req.params[1];
             var y = req.params[2];
    
             console.log('get tile %d, %d, %d', z, x, y);
    
             source.getTile(z, x, y, function(err, tile, headers) {
                 if (err) {
                     res.status(404)
                     res.send(err.message);
                     console.log(err.message);
                 } else {
                   res.set(headers);
                   res.send(tile);
                 }
             });
         });
    
         http.createServer(app).listen(app.get('port'), function() {
             console.log('Express server listening on port ' + app.get('port'));
         });
     });
    

    Note: The Access-Control-Allow-... headers enable cross-origin resource sharing (CORS) so webpages served from a different server may access the tiles.

  4. Run it with node server.js

  5. Set up the webpage using Mapbox GL JS in minimal.html:

     <!DOCTYPE html >
     <html>
       <head>
         <meta charset='UTF-8'/>
         <title>Mapbox GL JS rendering my own tiles</title>
         <link href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.4.0/mapbox-gl.css' rel='stylesheet' />
         <script src='https://api.tiles.mapbox.com/mapbox-gl-js/v0.4.0/mapbox-gl.js'></script>
         <style>
           body { margin:0; padding:0 }
           #map { position:absolute; top:0; bottom:50px; width:100%; }
         </style>
       </head>
       <body>
         <div id='map'>
         </div>
         <script>
           var map = new mapboxgl.Map({
             container: 'map',
             center: [46.8, 8.5],
             zoom: 7,
             style: 'minimal.json'
           });
         </script>
       </body>
     </html>
    
  6. Indicate the location of the tile source and style the layers with the following minimal.json:

     {
       "version": 6,
       "constants": {
         "@background": "#808080",
         "@road": "#000000"
       },
       "sources": {
         "osm_roads": {
           "type": "vector",
           "tiles": [
             "http://localhost:7777/v2/tiles/{z}/{x}/{y}.pbf"
           ],
           "minzoom": 0,
           "maxzoom": 12
         }
       },
       "layers": [{
         "id": "background",
         "type": "background",
         "paint": {
           "background-color": "@background"
         }
       }, {
         "id": "roads",
         "type": "line",
         "source": "osm_roads",
         "source-layer": "roads",
         "paint": {
           "line-color": "@road"
         }
       }]
     }
    
  7. Serve the webpage and rejoice.

Related Question