Fast Scalable Basemap with TileServer GL/OpenMapTiles (Ubuntu 22.04)

TileServer GL is an open-source tile server for OpenStreetMap. Previously we explained the process of setting up OSM tile server with mapnik and mod_tile, which is a raster-based tile server. This tutorial is going to show you how to set up TileServer GL vector tile server on Ubuntu 22.04.

TileServer GL

TileServer GL can serve both vector tiles and raster tiles. It’s simple to use. You just need to feed it a .mbtiles file, which will be generated by OpenMaptiles from a PostGIS database.  The overall Steps are as follows:

  • Import OpenStreetMap data to PostgreSQL
  • Use OpenMapTiles to generate .mbtiles file
  • Start TileServer GL with the .mbtiles file to serve tiles

MBTiles are like pre-rendered map tiles. The PostGIS database won’t be needed anymore once the .mbtiles file is generated. That’s why it’s very fast to serve tiles with TileServer GL.

TileServer GL is designed to work with MBTiles in openmaptiles format, so we must use OpenMapTiles tools to generate the .mbtiles file with the latest map data.

Demo

https://www.linuxbabe.com/maps/mapbox-street.html

Fast Scalable Basemap with TileServer GL and OpenMapTiles (Ubuntu 22.04)

Benefits of Vector Tiles

  • Better display quality for high DPI devices (retina display)
  • Small efficient format (No 512 * 512 images needed)
  • Fast map rendering
  • Clearer, more readable text
  • On-the-fly labeling for heads-up display
  • Separate content and styling, which allows for creating multiple styles pointing to the same tile stack.
  • More fine-grained zoom levels such as 8.43 and 9.57. With raster tiles, you can only have integer zoom levels such as 8, 9, 10.
  • Day and night mode

Vector Tile Formats

There are several formats for vector tiles.

  • GeoJSON
  • TopoJSON
  • Mapbox Vector Tile (MVT)
  • 05m
  • OpenScienceMap binary
  • Arc GeoServices JSON

OpenMapTiles uses the Mapbox vector tile format.

Hardware Requirements

It takes a long time to import large map data to the PostgreSQL database and generate .mbtiles file from the database. Your server should have a powerful CPU and fast SSD. Also consider adding more RAM.

If you are going to host the entire world map, I recommend you buy the extra-large VPS from Contabo, which has

  • A 10-core CPU
  • 60 GB RAM
  • 1.6 TB Intel Optane SSD

It costs just 26.99 €/month.

Step 1: Upgrade Software

It’s always a good practice to update server software before doing any major work on your server. Log into your server via SSH and run the following command.

sudo apt update; sudo apt upgrade -y

Step 2: Install PostgreSQL Database Server and Some Extensions

We will use PostgreSQL to store map data. The PostgreSQL team always strives to make performance improvements with every new version. Run the following 4 commands to install the latest version of PostgreSQL.

echo "deb [signed-by=/etc/apt/keyrings/postgresql.asc] http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" | sudo tee /etc/apt/sources.list.d/pgdg.list

wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo tee /etc/apt/keyrings/postgresql.asc

sudo apt update

sudo apt install -y postgresql postgresql-contrib

Then install PostGIS, which is a geospatial extension to PostgreSQL.

sudo apt install postgis postgresql-14-postgis-3

Install PostgreSQL GZIP extension.

cd ~ 

sudo apt install git

git clone https://github.com/pramsey/pgsql-gzip

cd pgsql-gzip/

sudo apt install build-essential zlib1g-dev postgresql-server-dev-all pkg-config

make

sudo make install

The extension will be installed under /usr/share/postgresql/14/extension/.

Install OSM Localization Extension (osml10n)

cd ~

git clone https://github.com/giggls/mapnik-german-l10n

cd mapnik-german-l10n/

sudo apt install devscripts equivs python3 python3-pip -y

sudo mk-build-deps -i debian/control

sudo pip3 install tltk

make deb

sudo apt install ../postgresql*osml10n*amd64.deb

The extension will be installed under /usr/share/postgresql/14/extension/.

Step 3: Create PostgreSQL Database For OpenStreetMap

PostgreSQL database server automatically starts and listens on 127.0.0.1:5432. The postgres user is the super user for PostgreSQL database server. By default, this user has no password and there’s no need to set one because you can use sudo to switch to the postgres user and log into PostgreSQL server.

sudo -u postgres -i

Now you can create a PostgreSQL database user osm.

createuser osm

Set a password for the osm user. The password should not contain any special characters, or it might prevent OSM tools to access PostgreSQL database.

psql -c "ALTER USER osm WITH PASSWORD 'secret_password';"

Then create a database named openstreetmap and at the same time make osm as the owner of the database.

createdb -E UTF8 -O osm openstreetmap

Next, create the postgis and hstore extension for the openstreetmap database.

psql -d openstreetmap -c "CREATE EXTENSION postgis;" 

psql -d openstreetmap -c "CREATE EXTENSION hstore;"

Enable the GZIP and osml10n extension on the openstreetmap database.

psql -d openstreetmap -c "CREATE EXTENSION gzip;"

psql -d openstreetmap -c "CREATE EXTENSION osml10n CASCADE;"

Set osm as the table owner.

psql -d openstreetmap -c "ALTER TABLE spatial_ref_sys OWNER TO osm;"

Grant permission to the osm user.

psql -d openstreetmap -c "GRANT ALL ON ALL TABLES IN SCHEMA public TO osm;"

psql -d openstreetmap -c "ALTER DEFAULT PRIVILEGES FOR USER osm IN SCHEMA public GRANT ALL ON TABLES TO osm;";

psql -d openstreetmap -c "ALTER DEFAULT PRIVILEGES FOR USER osm IN SCHEMA public GRANT ALL ON SEQUENCES TO osm;";

Create a database named natural_earth and at the same time make osm as the owner of the database.

createdb -E UTF8 -O osm natural_earth

Next, create the postgis and hstore extension for the natural_earth database.

psql -c "CREATE EXTENSION postgis;" -d natural_earth

psql -c "CREATE EXTENSION hstore;" -d natural_earth

Exit from the postgres user.

exit

Step 4: Optimize PostgreSQL Server Performance

The import process can take some time. To speed up this process, we can tune some PostgreSQL server settings to improve performance. Edit PostgreSQL main configuration file.

sudo nano /etc/postgresql/14/main/postgresql.conf

First, we should change the value of shared_buffer. The default setting is:

shared_buffers = 128MB

This is too small. The rule of thumb is to set it to 25% of your total RAM (excluding swap space). For example, my VPS has 60G RAM, so I set it to:

shared_buffers = 15GB

Find the following line.

#work_mem = 4MB
#maintenance_work_mem = 64MB

Again, the value is too small. I use the following settings.

work_mem = 1GB
maintenance_work_mem = 8GB

Then find the following line.

#effective_cache_size = 4GB

If you have lots of RAM like I do, you can set a higher value for the effective_cache_size like 20G.

effective_cache_size = 20GB

Save and close the file. Restart PostgreSQL for the changes to take effect.

sudo systemctl restart postgresql

By default, PostgreSQL would try to use huge pages in RAM. However, Linux by default does not allocate huge pages. Check the process ID of PostgreSQL.

sudo head -1 /var/lib/postgresql/14/main/postmaster.pid

Sample output:

7031

Then check the VmPeak value of this process ID.

grep ^VmPeak /proc/7031/status

Sample output:

VmPeak: 16282784 kB

This is the peak memory size that will be used by PostgreSQL. Now check the size of huge page in Linux.

cat /proc/meminfo | grep -i huge

Sample output:

AnonHugePages:         0 kB
ShmemHugePages:        0 kB
HugePages_Total:       0
HugePages_Free:        0
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB
Hugetlb:               0 kB

We can calculate how many huge pages we need. Divide the VmPeak value by the size of huge page: 16282784 kB / 2048 kB = 7950. Then we need to edit Linux kernel parameters.

sudo nano /etc/sysctl.d/60-custom.conf

Add the following line to allocate 7950 huge pages.

vm.nr_hugepages = 7950

Save and close the file. Then apply the changes.

sudo sysctl -p /etc/sysctl.d/60-custom.conf

If you check the meminfo again,

cat /proc/meminfo | grep -i huge

We can see there are 7950 huge pages available.

AnonHugePages:         0 kB
ShmemHugePages:        0 kB
HugePages_Total:    7950
HugePages_Free:     7950
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB

Restart PostgreSQL to use huge pages.

sudo systemctl restart postgresql

Step 5: Install Imposm3

Imposm is an importer for OpenStreetMap data.

cd ~

wget https://github.com/omniscale/imposm3/releases/download/v0.11.1/imposm-0.11.1-linux-x86-64.tar.gz

tar xvf imposm-0.11.1-linux-x86-64.tar.gz

sudo mv imposm-0.11.1-linux-x86-64 /opt/imposm

Now you invoke imposm with:

/opt/imposm/imposm

Step 6: Install OpenMapTiles Tools

OpenMapTiles relies on Docker, so we need to install Docker.

sudo apt install docker.io docker-compose

Add your user account to the docker group.

sudo usermod -aG docker username

Log out and log back in for the change to take effect. Then run the following command to check running Docker containers.

docker ps

It should at least output the following texts.

CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

Clone the OpenMapTiles Github repo.

cd ~ 

git clone https://github.com/openmaptiles/openmaptiles.git

Create the data.yml, mapping.yml and SQL files.

cd openmaptiles/

sudo make

ubuntu openmaptiles generate-sql

Edit the docker-compose.yml file.

nano docker-compose.yml

Change the port nubmer to 2345.

  postgres:
    image: "${POSTGIS_IMAGE:-openmaptiles/postgis}:${TOOLS_VERSION}"
    # Use "command: postgres -c jit=off" for PostgreSQL 11+ because of slow large MVT query processing
    volumes:
      - pgdata:/var/lib/postgresql/data
    networks:
      - postgres
    ports:
      - "2345"
    env_file: .env

Save and close the file. Then edit the .env file.

nano .env

Find the following lines.

# Make sure these values are in sync with the ones in .env-postgres file
PGDATABASE=openmaptiles
PGUSER=openmaptiles
PGPASSWORD=openmaptiles
PGHOST=postgres
PGPORT=5432

Change them to the following. 172.17.0.1 is the Docker network interface.

PGDATABASE=openstreetmap
PGUSER=osm
PGPASSWORD=osm_password
PGHOST=172.17.0.1
PGPORT=5432

Find the following lines.

# Which zooms to generate with   make generate-tiles-pg
MIN_ZOOM=0
MAX_ZOOM=7

Set max zoom level to 14.

# Which zooms to generate with make generate-tiles-pg
MIN_ZOOM=0
MAX_ZOOM=14

Save and close the file. Create a .osmenv file in your home directory.

nano ~/.osmenv

Add the following lines.

export PGDATABASE=openstreetmap
export PGUSER=osm
export PGPASSWORD=osm_password
export PGHOST=172.17.0.1
export PGPORT=5432

Save and close the file. Then run the following command to set the above environment variables.

source ~/.osmenv

chmod 700 ~/.osmenv

OpenMapTiles will start a Docker container and access PostgreSQL database via 172.17.0.1, so we need to configure PostgreSQL to listen on this IP address.

sudo nano /etc/postgresql/14/main/postgresql.conf

Find the following line.

#listen_addresses = 'localhost'

Change it to:

listen_addresses = 'localhost,172.17.0.1'

Save and close the file. Then edit pg_hba.conf file.

sudo nano /etc/postgresql/14/main/pg_hba.conf

Find the following line.

host    all             all             127.0.0.1/32            scram-sha-256

This allows users to log in to PostgreSQL from the 127.0.0.1 IP address. We need to add the 172.17.0.0/24 and 172.18.0.0/24 network to allow login from Docker.

host    all             all             127.0.0.1/32            scram-sha-256
host    all             all             172.17.0.0/24           scram-sha-256
host    all             all             172.18.0.0/24           scram-sha-256

Save and close the file. Restart PostgreSQL for the changes to take effect.

sudo systemctl restart postgresql

If you enabled firewall on the server, you should also allow connections from the 172.17.0.0/24 and 172.18.0.0/24 network. For example, if you use the UFW firewall, run the following command.

sudo ufw insert 1 allow in from 172.17.0.0/24

sudo ufw insert 1 allow in from 172.18.0.0/24

Step 7: Use Screen on Remote Servers

It can take a long time to import the OSM pbf file and your computer might be disconnected from Internet, it’s recommended to use the screen utility to keep your session alive. Install screen on the Ubuntu 22.04 server:

sudo apt install screen

Then start screen:

screen

Upon the first launch, you will see an introduction text, simply press Enter to end. Then you will be able to run commands as usual.

The GNU screen session will reset environment variables, so we need to set them again.

source ~/.osmenv

Run the following command to check the PostgreSQL environment variables are set.

env

Step 8: Import the Map Data to PostgreSQL

Now we can import external OSM data from OpenStreetMapData, Natural Earth and OpenStreetMap Lake Labels into the PostgreSQL database.

cd ~/openmaptiles/

sudo make import-data

openmaptiles import Natural Earth

If you encounter the following error, it’s because you didn’t change the port number in docker-compose.yml file.

Error starting userland proxy: listen tcp4 0.0.0.0:5432: bind: address already in use

To fix this error, you should change the port number, then remove the existing Docker container.

sudo docker rm openmaptiles_postgres_1

And run sudo make import-data again.

Next, run the following command to download the map data of the whole planet (50G) in PBF (ProtoBufBinary) format to the ~/openmaptiles/data/ directory.

cd ~/openmaptiles/data/

sudo wget -c http://planet.openstreetmap.org/pbf/planet-latest.osm.pbf

Note that download speeds for openstreetmap.org are currently restricted to 2048 KB/s. You can download the plant map from another mirror, like

sudo wget -c https://download.bbbike.org/osm/planet/planet-latest.osm.pbf

If you want a map of individual country/state/province/city, go to http://download.geofabrik.de. Also, BBBike.org provides extracts of more than 200 cities and regions worldwide in different formats. For example, download the map data of Great Britain (1.1G) with the following command.

sudo wget -c http://download.geofabrik.de/europe/britain-and-ireland-latest.osm.pbf

Import the PBF file. If your map data is very large, then this process will take some time.

cd ~/openmaptiles/ 

sudo make import-osm

openmaptiles import osm data

Import labels from Wikidata.

sudo make import-wikidata

Extract bbox (bounding box) from the .osm.pbf file.

sudo make generate-bbox-file

Import SQL files.

sudo make import-sql

Generate vector tiles (.mbtiles file). If your map data is very large, then this process will take some time. The generated file will be saved as ~/openmaptiles/data/tile.mbtiles.

sudo make generate-tiles-pg

Step 9: Install TileServer GL

The easiest way is to use Docker. It will listen on TCP port 8080.

sudo docker run --restart=always -it -d -v /home/username/openmaptiles/data:/data -p 8080:80 maptiler/tileserver-gl

TileServer GL will automatically detect .mbtiles file in the data directory.

If you want to run TileServer GL natively, you can install it with npm, but I don’t recommend this method. It’s error-prone. TileServer GL is compatible with NodeJS 10.

curl -fsSL https://deb.nodesource.com/setup_10.x | sudo -E bash -

sudo apt install nodejs

sudo npm install -g tileserver-gl

Then in your web browser address bar, type

your-server-ip-address:8080

You should see the vector tile map. Congrats! You just successfully built your own vector tile server. Note that old versions of Firefox can’t display these vector tiles. You need to use a third-party library to display vector tile based maps, which is explained at the end of this tutorial.

OpenMapTiles mbtiles

Step 10: Generate Fonts for OpenStreetMap

Download the OpenMapTiles fonts generation tool.

cd ~

git clone https://github.com/openmaptiles/fonts.git

Install NodeJS.

sudo apt install nodejs npm

Install required nodejs module.

cd fonts/

npm install

Generate fonts, which will be saved in the _output directory.

node ./generate.js

Move this directory to /var/www/.

sudo make -p /var/www/

sudo mv _output/ /var/www/font-family/

It’s also recommeneded to download the KlokanTech Noto Sans font family.

cd ~

git clone https://github.com/klokantech/klokantech-gl-fonts.git

Move it to /var/www/font-family/ directory.

sudo mv klokantech-gl-fonts/* /var/www/font-family/

Some map styles expect the font name Klokantech Noto Sans, so we need to make a copy.

cd /var/www/font-family/
sudo cp -r KlokanTech\ Noto\ Sans\ Regular/ Klokantech\ Noto\ Sans\ Regular/
sudo cp -r KlokanTech\ Noto\ Sans\ Bold/ Klokantech\ Noto\ Sans\ Bold
sudo cp -r KlokanTech\ Noto\ Sans\ CJK\ Bold/ Klokantech\ Noto\ Sans\ CJK\ Bold/
sudo cp -r KlokanTech\ Noto\ Sans\ CJK\ Regular/ Klokantech\ Noto\ Sans\ CJK\ Regular/
sudo cp -r KlokanTech\ Noto\ Sans\ Italic/ Klokantech\ Noto\ Sans\ Italic/

Step 11: Setting Up Reverse Proxy

To access TileServer GL using a domain name, we can set up a reverse proxy for TileServer GL with Nginx or Apache. This will also allow us to enable HTTPS with free Let’s Encrypt certificate.

Nginx

Nginx is a very popular web server and reverse proxy. If you prefer to use Nginx, run the following command to install it.

sudo apt install nginx

Then create a server block file for TileServer GL.

sudo nano /etc/nginx/conf.d/tileserver-gl.conf

Add the following content to this file. Replace tileserver.example.com with your own domain name. You should also create DNS A record for this sub-domain. If you don’t have a real domain name, I recommend going to NameCheap to buy one. The price is low and they give whois privacy protection free for life.

server {
      listen 80;
      listen [::]:80;
      server_name tileserver.example.com;

      access_log /var/log/nginx/openstreetmap.access;
      error_log /var/log/nginx/openstreetmap.error;

      location / {
          proxy_pass http://127.0.0.1:8080;
          proxy_set_header Host $host;
          proxy_set_header X-Real-IP $remote_addr;

          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
          proxy_set_header X-Forwarded-Proto $scheme;
          proxy_set_header X-Forwarded-Protocol $scheme;
          proxy_set_header X-Forwarded-Host $http_host;

          if ($http_origin ~ "example.com$") {
             add_header "Access-Control-Allow-Origin" $http_origin;
          }
      }
}

Save and close this file. Then test Nginx configuration.

sudo nginx -t

If the test is successful, reload Nginx for the change to take effect.

sudo systemctl reload nginx

Now you can access TileServer GL via tileserver.example.com.

Apache

If you prefer Apache over Nginx, then install Apache web server by using the following command.

sudo apt install apache2

To use Apache as a reverse proxy, we need to enable the proxy modules and the header module.

sudo a2enmod proxy proxy_http headers substitute

Then create a virtual host file for TileServer GL.

sudo nano /etc/apache2/sites-available/tileserver-gl.conf

Put the following configurations into the file. Replace tileserver.example.com with your actual domain name. Don’t forget to create DNS A record for this sub-domain. If you don’t have a real domain name, I recommend going to NameCheap to buy one. The price is low and they give whois privacy protection free for life.

<VirtualHost *:80>
   ServerName tileserver.example.com
   ErrorDocument 404 /404.html

   # disable proxy for the /font-family sub-directory
   # must be placed on top of the other ProxyPass directive
   ProxyPass /font-family !
   Alias "/font-family" "/var/www/font-family"
   #HTTP proxy
   ProxyPass / http://127.0.0.1:8080/
   ProxyPassReverse / http://127.0.0.1:8080/
   ProxyPreserveHost On

   # Only allow authorizied domains to fetch resources on this tile server
   SetEnvIf Origin "^http(s)?://(.+\.)?(example\.com|otherdomain\.tld)$" origin_is=$0
   Header unset Access-Control-Allow-Origin
   Header always set Access-Control-Allow-Origin %{origin_is}e env=origin_is

   ErrorLog ${APACHE_LOG_DIR}/tileserver-gl.error.log
   CustomLog ${APACHE_LOG_DIR}/tileserver-gl.access.log combined

</VirtualHost>

Save and close the file. Then enable this virtual host.

sudo a2ensite tileserver-gl.conf

Restart Apache

sudo systemctl restart apache2

Now you can access TileServer GL using the domain name tileserver.example.com.

Step 12: Enable HTTPS

To encrypt the HTTP traffic when you visit TileServer GL server from outside, we can enable HTTPS by installing a free TLS certificate issued from Let’s Encrypt. Run the following command to install Let’s Encrypt client (certbot) on Ubuntu 22.04.

sudo apt install certbot

If you use Nginx, then you also need to install the Certbot Nginx plugin.

sudo apt install python3-certbot-nginx

Next, run the following command to obtain and install TLS certificate.

sudo certbot --nginx --agree-tos --redirect --hsts --staple-ocsp --email [email protected] -d tileserver.example.com

If you use Apache, then you need to install the Certbot Apache plugin.

sudo apt install python3-certbot-apache

Next, run the following command to obtain and install TLS certificate.

sudo certbot --apache --agree-tos --redirect --hsts --staple-ocsp --uir --email [email protected] -d tileserver.example.com

Where:

  • --nginx: Use the nginx plugin.
  • --apache: Use the Apache plugin.
  • --agree-tos: Agree to terms of service.
  • --redirect: Force HTTPS by 301 redirect.
  • --hsts: Add the Strict-Transport-Security header to every HTTP response. Forcing browser to always use TLS for the domain. Defends against SSL/TLS Stripping.
  • --staple-ocsp: Enables OCSP Stapling. A valid OCSP response is stapled to the certificate that the server offers during TLS.
  • --uir: upgrade insecure requests.

The certificate should now be obtained and automatically installed.

tileserver gl https

And you can access TileServer GL via HTTPS: https://tileserver.example.com.

TileServer GL - Server for vector and raster maps with GL styles

If you click the vector HTTP link, you will see an example map with the Klokantech Basic Style.

tileserver GL Klokantech Basic Style

You can use the following HTML code to display the map on any web page.

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Basic preview - TileServer GL</title>
  <link rel="stylesheet" type="text/css" href="https://tileserver.example.com/mapbox-gl.css" />
  <script src="https://tileserver.example.com/mapbox-gl.js"></script>
  <link rel="stylesheet" type="text/css" href="https://tileserver.example.com/mapbox.css" />
  <script src="https://tileserver.example.com/mapbox.js"></script>
  <script src="https://tileserver.example.com/leaflet-hash.js"></script>
  <style>
    body { margin:0; padding:0; }
    #map { position:absolute; top:0; bottom:0; width:100%; }
  </style>
</head>
<body>
  <h1 style="display:none;">Basic preview</h1>
  <div id='map'></div>
  <script>
    var q = (location.search || '').substr(1).split('&');
    var preference =
      q.indexOf('vector') >= 0 ? 'vector' :
        (q.indexOf('raster') >= 0 ? 'raster' :
          (mapboxgl.supported() ? 'vector' : 'raster'));
    if (preference == 'vector') {
      mapboxgl.setRTLTextPlugin('https://tileserver.example.com/mapbox-gl-rtl-text.js');
      var map = new mapboxgl.Map({
        container: 'map',
        style: 'https://tileserver.example.com/styles/basic-preview/style.json',
        zoom: 5.43,  
        center: [-3.9, 54.5],
        hash: true
      });
      map.addControl(new mapboxgl.NavigationControl());
    } else {
      var map = L.mapbox.map('map', 'https://tileserver.example.com/styles/basic-preview.json', { zoomControl: false });
      new L.Control.Zoom({ position: 'topright' }).addTo(map);
      setTimeout(function() {
        new L.Hash(map);
      }, 0);
    }
  </script>
</body>
</html>

Note that if you serve the map over HTTPS, you need to add the upgrade-insecure-requests header to convert all HTTP URLs to HTTPS URLs.

Nginx

add_header Content-Security-Policy upgrade-insecure-requests;

Apache

Header always set Content-Security-Policy upgrade-insecure-requests

Step 13 : Use Other Open-Source Map Styles

The following is a list of open-source map styles compatible with OpenMapTiles vector tile schema.

Let me show you how to use these map styles. For example, I download the MapTiler 3D style.

git clone https://github.com/openmaptiles/maptiler-3d-gl-style.git

Then edit the style.json file.

cd maptiler-3d-gl-style/

nano style.json

Find the following lines.

  "sources": {
    "openmaptiles": {
      "type": "vector",
      "url": "https://api.maptiler.com/tiles/v3/tiles.json?key={key}"
    }
  },
  "glyphs": "https://api.maptiler.com/fonts/{fontstack}/{range}.pbf?key={key}",

Change the tileset URL and glyphs URL:

  "sources": {
    "openmaptiles": {
      "type": "vector",
      "url": "https://tileserver.example.com/data/v3.json"
    }
  },
  "glyphs": "https://tileserver.example.com/font-family/{fontstack}/{range}.pbf",

Save and close the file. Then upload this style.json file to a web server and create an HTML file.

nano maptiler-3d.html

Content:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Maptiler 3D Style</title>
  <link rel="stylesheet" type="text/css" href="https://tileserver.example.com/mapbox-gl.css" />
  <script src="https://tileserver.example.com/mapbox-gl.js"></script>
  <link rel="stylesheet" type="text/css" href="https://tileserver.example.com/mapbox.css" />
  <script src="https://tileserver.example.com/mapbox.js"></script>
  <script src="https://tileserver.example.com/leaflet-hash.js"></script>
  <style>
    body { margin:0; padding:0; }
    #map { position:absolute; top:0; bottom:0; width:100%; }
  </style>
</head>
<body>
  <h1 style="display:none;">Basic preview</h1>
  <div id='map'></div>
  <script>
    var q = (location.search || '').substr(1).split('&');
    var preference =
      q.indexOf('vector') >= 0 ? 'vector' :
        (q.indexOf('raster') >= 0 ? 'raster' :
          (mapboxgl.supported() ? 'vector' : 'raster'));
    if (preference == 'vector') {
      mapboxgl.setRTLTextPlugin('https://tileserver.example.com/mapbox-gl-rtl-text.js');
      var map = new mapboxgl.Map({
        container: 'map',
        style: 'https://www.example.com/style.json',
        zoom: 5.43,  
        center: [-3.9, 54.5],
        hash: true
      });
      map.addControl(new mapboxgl.NavigationControl());
    } else {
      var map = L.mapbox.map('map', 'https://tileserver.example.com/styles/basic-preview.json', { zoomControl: false });
      new L.Control.Zoom({ position: 'topright' }).addTo(map);
      setTimeout(function() {
        new L.Hash(map);
      }, 0);
    }
  </script>
</body>
</html>

Save and close the file. Here’s how the map looks like: https://www.linuxbabe.com/maps/maptiler-3d.html

Maptiler 3D Style

You might also want to check out other tools provided by OpenMapTiles on Github.

Step 14: Create Custom Styles

You can use maputnik to edit map styles. Make sure it conforms to OpenMapTiles schema.

You need to know the TileServer GL endpoints when editing your style JSON file.

  • TileJSON URL: https://tileserver.example.com/data/v3.json
  • Tiles URLhttps://tileserver.example.com/data/v3/{z}/{x}/{y}.pbf
  • Glyphshttps://tileserver.example.com/font-family/{fontstack}/{range}.pbf

There are two ways to specify Tile URL in the style JSON file.

TileJSON URL:

  "sources": {
    "openmaptiles": {
      "type": "vector",
      "url": "https://tileserver.linuxbabe.com/data/v3.json"
    }
  },

Tiles URL:

  "sources": {
    "openmaptiles": {
      "type": "vector",
      "tiles": ["https://tileserver.linuxbabe.com/data/v3/{z}/{x}/{y}.pbf"],
       "attribution": "&copy; OpenStreetMap contributors",
       "minzoom": 0,
       "maxzoom": 14
    }
  },

I recommend using the second method: Tiles URL. Using the TILEJSON URL might cause the NetworkError when attempting to fetch resource error in Firefox.

Note: When using the Tiles URL, you must set maxzoom to 14 like in the above code.

Step 15: Enable HTTP2

To improve map loading performance, you can enable HTTP2 protocol.

Nginx

Simply open the virtual host file and change

listen 443 ssl;

to

listen 443 ssl http2;

Save and close the file. Then reload Nginx.

sudo systemctl reload nginx

Apache

First, you need to enable the HTTP2 module.

sudo a2enmod http2

Then open the SSL virtual host file.

sudo nano /etc/apache2/sites-enabled/tileserver_site-le-ssl.conf

Put the following directive after the opening <VirtualHost *:443> tag.

Protocols h2 http/1.1

Save and close the file. Then restart Apache for the changes to take effect.

sudo systemctl restart apache2

Display Map with MapLibre GL

Maplibre GL is an open-source fork of Mapbox GL. Demo: https://www.linuxbabe.com/maps/maplibre-streets.html

HTML code:

<!DOCTYPE html>
<html>
    <head>
         <meta charset="utf-8">
         <title>vector tile map made with Tegola and Maplibre</title>
         <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">
         <script src="https://unpkg.com/[email protected]/dist/maplibre-gl.js"></script>
         <link href="https://unpkg.com/[email protected]/dist/maplibre-gl.css" rel="stylesheet" />
         <style>
            body { margin: 0; padding: 0; }
            #map { position: absolute; top: 0; bottom: 0; width: 100%; }
         </style>
   </head>
   <body>
        <div id="map"></div>
        <script>
                var map = new maplibregl.Map({
                container: 'map',
                style:
                        'https://www.linuxbabe.com/maps/mapbox-street-style.json',
                zoom: 5.43,
                center: [-3.9, 54.5]
        });     


        map.on('load', function () {
                  // Add a new vector tile source with ID 'linuxbabe'.
                  map.addSource('linuxbabe', {
                         'type': 'vector',
                         'tiles': [
                              'https://tileserver.linuxbabe.com/data/v3/{z}/{x}/{y}.pbf'
                          ],
                          'minzoom': 6,
                          'maxzoom': 14
                    });
                    map.addLayer(
                       {
                            'id': 'default', // Layer ID
                            'type': 'line',
                            'source': 'linuxbabe', // ID of the tile source created above
                               // Source has several layers. We visualize the one with name 'sequence'.
                            'source-layer': 'sequence',
                            'layout': {
                                      'line-cap': 'round',
                                      'line-join': 'round'
                             },
                            'paint': {
                                       'line-opacity': 0.6,
                                       'line-color': 'rgb(53, 175, 109)',
                                       'line-width': 2
                             }
                      },
                   );
                });

                map.addControl(new maplibregl.NavigationControl());

                //set max zoom level (0-24)
                map.setMaxZoom(19);
        </script>
        </body>
</html>

Integrate Maplibre with Nominatim Geocoder

If you want to provide address search functionality, you can set up a Nominatim geocoding server and integrate it with your map via the Maplibre GL Geocoder plugin.

Add the following lines in HTML header.

  <script src="https://unpkg.com/@maplibre/[email protected]/dist/maplibre-gl-geocoder.min.js"></script>
  <link rel="stylesheet" href="https://unpkg.com/@maplibre/[email protected]/dist/maplibre-gl-geocoder.css" type="text/css" />

Then add the following code in the HTML body, above the </script> tag. Replace tile.example.com/nominatim with the URL of your own Nonimatim server.

var geocoder_api = {
    forwardGeocode: async (config) => {
        const features = [];
        try {
            let request =
                'https://tile.example.com/nominatim/search?q=' +
                config.query +
                '&format=geojson&polygon_geojson=1&addressdetails=1';
            const response = await fetch(request);
            const geojson = await response.json();
            for (let feature of geojson.features) {
                let center = [
                    feature.bbox[0] +
                    (feature.bbox[2] - feature.bbox[0]) / 2,
                    feature.bbox[1] +
                    (feature.bbox[3] - feature.bbox[1]) / 2
                ];
                let point = {
                    type: 'Feature',
                    geometry: {
                        type: 'Point',
                        coordinates: center
                    },
                    place_name: feature.properties.display_name,
                    properties: feature.properties,
                    text: feature.properties.display_name,
                    place_type: ['place'],
                    center: center
                };
                features.push(point);
            }
        } catch (e) {
            console.error(`Failed to forwardGeocode with error: ${e}`);
        }

        return {
            features: features
        };
    }
};
map.addControl(
    new MaplibreGeocoder(geocoder_api, {
        maplibregl: maplibregl
    })
);

Display Map with Leaflet

By default, TileServer GL uses Mapbox GL to display maps. Some folks use Leaflet for rendering raster tile maps, but Leaflet doesn’t support vector tiles natively. You need to use the Maplibre GL leaflet plugin. Example map: https://www.linuxbabe.com/maps/leaflet-vector.html

Here’s the code. We need to load the Leaflet and Maplibre GL library in the HTML header, then use Mapbox GL Leaflet to render the map in the HTML body.

<html>
<head>
    <title>Vector map loaded by Leaflet</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
      html, body, #map {
        width: 100%;
        height: 100%;
        margin: 0;
      }
    </style>

    <!-- Leaflet -->
    <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
    <script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>

    <!-- Maplibre GL -->
    <script src="https://unpkg.com/[email protected]/dist/maplibre-gl.js"></script> 
    <link href="https://unpkg.com/[email protected]/dist/maplibre-gl.css" rel="stylesheet" />

</head>

<body>
<div id="map"></div>

<script src="https://www.linuxbabe.com/maps/leaflet-maplibre-gl.js"></script>
<script>
var map = L.map('map').setView([54.5, -3.9], 6.4);

var gl = L.maplibreGL({
    style: 'https://www.linuxbabe.com/maps/mapbox-street-style.json'
}).addTo(map);

</script>
</body>
</html>

It’s compatible with the Nomination Geocoder.

Troubleshooting PostgreSQL Error

Check PostgreSQL status:

sudo systemctl status [email protected]

If you encounter the following error.

FATAL: could not map anonymous shared memory: Cannot allocate memory

It means you allocated too much RAM to PostgreSQL shared_buffers. Edit PostgreSQL config file.

sudo nano /etc/postgresql/14/main/postgresql.conf

Set shared_buffers to a smaller value like 5GB.

shared_buffers = 5GB

Save and close the file. Then restart PostgreSQL.

sudo systemctl restart [email protected]

How to Upgrade PostgreSQL Database Server

When a new version of PostgreSQL comes out, you can upgrade to take advantage of performance improvements. Simply run the following command, and the apt package manager will install the latest version of PostgreSQL from the apt.postgresql.org repository.

sudo apt update; sudo apt upgrade -y

After that, you also need to upgrade existing PostgreSQL clusters. Pro Tip: You should open a GNU Screen session to prevent connection drop because the upgrade will take sometime.

screen

Then list current clusters.

sudo pg_lsclusters

Sample output:

Ver Cluster Port Status Owner    Data directory              Log file
10  main    5432 online postgres /var/lib/postgresql/10/main /var/log/postgresql/postgresql-10-main.log
12  main    5433 online postgres /var/lib/postgresql/12/main /var/log/postgresql/postgresql-12-main.log
14  main    5434 online postgres /var/lib/postgresql/14/main /var/log/postgresql/postgresql-14-main.log

As you can see, PostgreSQL 10 is using the default 5432 port. PostgreSQL 12 is using port 5433 and PostgreSQL 14 is using port 5434. Stop PostgreSQL server.

sudo systemctl stop postgresql

Let’s check the cluster status again with: sudo pg_lsclusters. They are all down.

Ver Cluster Port Status Owner    Data directory              Log file
10  main    5432 down   postgres /var/lib/postgresql/10/main /var/log/postgresql/postgresql-10-main.log
12  main    5433 down   postgres /var/lib/postgresql/12/main /var/log/postgresql/postgresql-12-main.log
14  main    5434 down   postgres /var/lib/postgresql/14/main /var/log/postgresql/postgresql-14-main.log

Rename the cluster name of PostgreSQL 14 from main to latest_stable.

sudo pg_renamecluster 14 main latest_stable

Then we need to shrink the PostgreSQL shared_buffer.

sudo nano /etc/postgresql/10/main/postgresql.conf

Decrease the value to prevent out-of-memory problem during upgrade. For example, I reduce the value from 15GB to 5GB.

shared_buffers = 5GB

Save and close the file.

Next, upgrade the old PostgreSQL 10 cluster.

sudo pg_upgradecluster 10 main

Start PostgreSQL server.

sudo systemctl start postgresql

Now check if your application is working. If it’s working well, then drop the old cluster.

sudo pg_dropcluster 10 main

Check cluster status again:

sudo pg_lsclusters

If the upgrade is successful, you can change the shared_buffer back to the original value in the /etc/postgresql/14/main/postgresql.conf file. Don’t forget to restart PostgreSQL.

If the upgrade procedure can’t stop PostgreSQL,

pg_ctl: server does not shut down
Error: Could not stop target cluster

then you need to run the following command to stop PostgreSQL.

pkill postgres

Then edit PostgreSQL 10 config file.

sudo nano /etc/postgresql/14/main/postgresql.conf

Set the listen port to 5432.

port = 5432

Save and close the file. Then start PostgreSQL 14.

sudo systemctl restart [email protected]

Run the following command to make sure you can log into PostgreSQL console.

sudo -u postgres -i psql

Press Ctrl+D to log out.

Restart TileServer GL.

sudo systemctl restart openstreetmap

Conclusion

I hope this article helped you set up TileServer GL Vector Tile Server on Ubuntu 22.04. As always, if you found this post useful, then subscribe to our free newsletter to get more tips and tricks. Take care 🙂

Rate this tutorial
[Total: 0 Average: 0]

One Response to “Fast Scalable Basemap with TileServer GL/OpenMapTiles (Ubuntu 22.04)

  • If you encounter the following error:

    onnection to server at "172.17.0.1", port 5432 failed: FATAL:  password authentication failed for user "osm"

    Then you need to reset osm password.

    sudo -u postgres -i psql
    
    ALTER USER osm WITH PASSWORD 'secret_password';
    
    \q;
    

Leave a Comment

  • Comments with links are moderated by admin before published.
  • Your email address will not be published.
  • Use <pre> ... </pre> HTML tag to quote the output from your terminal/console.
  • Please use the community (https://community.linuxbabe.com) for questions unrelated to this article.
  • I don't have time to answer every question. Making a donation would incentivize me to spend more time answering questions.

The maximum upload file size: 2 MB. You can upload: image. Links to YouTube, Facebook, Twitter and other services inserted in the comment text will be automatically embedded. Drop file here