This document outlines everything necessary to build, from scratch, an
operational OpenStreetMap vector tile server. The stack consists of
the latest LTS Ubuntu Server distribution, Tegola as
the tile server, PostgreSQL as the database,
and the use of the open source OpenMapTiles
standard schema. We will be importing and updating our raw OSM data using
the imposm3 import tool, via the
OpenMapTiles toolchain.
Maintained by: Andrew DeChristopher <[email protected]>- Prerequisites
- Standing up Postgres
- OpenMapTiles Configuration
- OSM Import Configuration
- Tile Server Configuration
- Updating Data
- Integration
- Example
- Conclusions
WARNING: This guide is currently incompatible with hosted PostgreSQL services like
AWS RDS/Aurora and others that do not natively support the osml10n extension out of
the box. It will likely remain that way until these services support custom extension
installation.
We will be operating out of the /osm directory. Create it and ensure proper
permissions are set to your administrative user. Nothing during the import
process will be run as your user. All actions are run from pre-built docker
containers which makes setup replicable and easy for us. We recommend operating
as root for the initial setup process since libraries and files will need to be
copied around to various system directories.
This guide assumes decent foundational knowledge of systems administration on Linux hosts, PostgreSQL, and Docker. It has been tested against vanilla PostgreSQL v13+ installed on the latest long-term-support version of Ubuntu.
Please ensure you have a working, modern PostgreSQL installation with the PostGIS extension installed on the latest Ubuntu.
The OpenMapTiles toolchain, as mentioned, runs out of docker containers. Let's make sure we have it installed:
sudo apt install docker.io docker-compose
sudo usermod -aG docker <your username>
docker psIf you get an error after running docker ps, just log out and log back in. This
is usually due to the permissions to access the docker daemon not having taken
effect yet.
Depending on the use case, you can install imposm3 to manually configure and run
data updates. This isn't necessary for the initial data import.
The server will need Golang to run the imposm3 OSM import tool. Install it
with the snap package manager for the easiest time:
sudo snap install goClone and install imposm3 with the following commands:
go get github.com/omniscale/imposm3
go install github.com/omniscale/imposm3/cmd/imposm3If this does not work, you will simply have to run imposm3 using the
prebuilt binary. More on this later.
Postgres does not ship with a GZip extension out of the box. OpenMapTiles' layer functions require it to compress tiles over the wire.
Begin by cloning the pgsql-gzip repo within /osm
git clone https://github.com/pramsey/pgsql-gzipEnter the repo directory. We'll need a few prerequisite libraries before we can build the extension from source. Run:
sudo apt install build-essential zlib1g-dev postgresql-server-dev-all pkg-configOnce that's succeeded, we can move forward with building the extension:
make
#important to run as sudo if not already root
sudo make installIf the compilation and installation succeeded, you'll have the extension's control files listed within your postgres extension directory. Run the following to discover your share directory:
pg_config --sharedir
# Should be something like:
# /usr/share/postgresql/<VERSION>Remember this directory, we'll need it later. Your extensions directory is
a subdirectory named extension under the share directory. Let's make sure
all is right:
ls $(pg_config --sharedir)/extension | grep gzip
# You should see the following:
gzip--1.0.sql
gzip.controlIf you don't see gzip.control and its accompanying sql file. Try to
run the DEB package installation process outlined in the README.md in
the repo. If that fails, try your best to troubleshoot the compilation
and subsequent extension installation process. Being root helps.
The linchpin to this all working together is the osml10n extension.
Short for OSM Localization, this PG extension will translate and transcribe
other languages with different alphabets into the standard International
Phonetic Alphabet
to give us the best user experience when viewing labels on the map that are
sourced from data in different languages.
The osml10n project on Github,
currently the only working fork of the original project, is targeted to be built
on Debian-derived linux distributions like Ubuntu. Again, this guide assumes a vanilla
PostgreSQL v13+ server installed on the latest long-term-support version of Ubuntu.
The OSM Localization extension is very simple to build on a Debian/Ubuntu system. Please don't waste your time sifting through compatible packages on other distributions. It is possible, but it just stinks to get working.
Clone the repo into /osm
git clone https://github.com/giggls/mapnik-german-l10nEnter the repo directory and begin by installing the required libraries:
sudo apt install devscripts equivs
sudo mk-build-deps -i debian/controlInstall python3 and pip3 if not already installed and add the Thai transcription library:
sudo apt install python3 python3-pip
sudo pip3 install tltkOn success, simply compile the DEB package. It should take a minute or two:
make debIn the root of the repo, you'll now see plenty of new control files and compiled extensions. Lucky for us, we don't have to toss those around manually.
The makefile dropped a nice deb package one level up in the /osm directory for us!
cd /osm && ls
# you'll see some sort of
# postgresql-<VERSION>-osml10n_<VERSION>_amd64.deb
# Install it with dpkg
sudo dpkg --install postgresql-<VERSION>-osml10n_<VERSION>_amd64.debOnce that finishes a few seconds later, let's make sure everything's in order.
ls $(pg_config --sharedir)/extension | grep osml10n
# You should see something like the following:
osml10n--2.5.8--2.5.9.sql
osml10n--2.5.9.sql
osml10n.control
osml10n_country_osm_grid.data
osml10n_thai_transcript--2.5.8--2.5.9.sql
osml10n_thai_transcript--2.5.9.sql
osml10n_thai_transcript.controlAt this point, we're ready to configure Postgres!
In this step, we'll create an OSM database, user, and bootstrap all the extensions we required in the last step.
SU into the postgres user:
sudo su postgresLet's create an OSM user, as well as our OSM table:
createuser --no-superuser --no-createrole --createdb osm
createdb -E UTF8 -O osm osmNow we can bootstrap our extensions and secure the osm user:
psql -d osm -c "CREATE EXTENSION postgis;"
psql -d osm -c "CREATE EXTENSION hstore;"
psql -d osm -c "CREATE EXTENSION gzip;"
psql -d osm -c "CREATE EXTENSION osml10n CASCADE;"If all of that succeeded, especially the osml10n extension, then you're
good to go! Otherwise, you're going to have to go do some debugging and figure
out why the extensions weren't loaded properly. The output is usually fairly
useful. Remember that lots of the osml10n libraries (.so files) were compiled
and left within the root of the repo. You can manually drop them into their
proper directories if need be.
Just a bit of housekeeping here. Let's ensure that the osm user retains access to the osm db and schemas within it even if we have to nuke them in the future for something:
psql -d osm -c "GRANT ALL ON ALL TABLES IN SCHEMA public TO osm;"
psql -d osm -c "ALTER DEFAULT PRIVILEGES FOR USER osm IN SCHEMA public GRANT ALL ON TABLES TO osm;";
psql -d osm -c "ALTER DEFAULT PRIVILEGES FOR USER osm IN SCHEMA public GRANT ALL ON SEQUENCES TO osm;";Let's secure our osm user:
echo "ALTER USER osm WITH PASSWORD 'surreptitious';" | psql -d osm;Please use a password more secure than this!!
If your tile server will be run from a server separate from the one hosting your OSM
database, you'll have to modify postgresql.conf and pg_hba.conf to allow remote
connections.
These files are located in: /etc/postgresql/<VERSION>/main/
In postgresql.conf change:
listen_address = 'localhost'
TO
listen_address = '*';In pg_hba.conf add this line to the end of the file:
host all all all md5Then restart the PG server with: sudo service postgresql restart
From now on, we'll need environment variables set up to access the db through psql
and our other import tooling. Let's do so now. Create a file in the home directory
named .osmenv with the contents:
export PGUSER=osm
export PGPASSWORD=surreptitious
export PGHOST=127.0.0.1
export PGPORT=5432
export PGDATABASE=osmThen run:
chmod 700 ~/.osmenv
source ~/.osmenvThe environment variables should now be present in your session. You can make sure
with the env command to list all the current environment variables.
With the database primed and ready, we can begin the OpenMapTiles configuration.
OpenMapTiles has built an excellent db schema and toolset that we'll need to import to be able to use their open source styles.
Clone their repo into /osm
git clone https://github.com/openmaptiles/openmaptilesEnter the repo directory. We'll need to have docker installed for this next part. If
it isn't already installed, please do so now. Run docker ps to make sure your user
has access to the docker daemon. Again, this guide will not outline any docker
troubleshooting. It assumes you have basic proficiency in docker and docker-compose.
There are plenty of resources available to troubleshoot things if need be.
First off, let's generate the OpenMapTiles schema migrations and imposm3 mappings.
makeWe're going to have to make a few adjustments to the default config. OpenMapTiles repo is tailored by default to those that actively work on the schema itself and drive it forward, not to those that run it in production. Luckily this isn't too much of a lift.
Open the docker-compose.yml in the root of the repo in your text editor of choice. Change the port line in the postgres service to any other port than the default:
services:
postgres:
image: "${POSTGIS_IMAGE:-openmaptiles/postgis}:${TOOLS_VERSION}"
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- postgres_conn
ports:
- "2345" # <-- CHANGE THIS TO ANYTHING BUT 5432
env_file: .env-postgresWe make this change to prevent port overlap with your PG server running locally. If not changed, the import tooling gets confused and uses the containerized PG server. We don't want to confuse the lad.
Next, we'll edit the .env file in the repo, which the tooling uses to populate the
PG connection data and credentials. Change the lines up top to what we defined in your
.osmenv earlier. If anything in here is wrong, the tools will fail, and they will make
sure you know they did.
NOTE: Ignore the warning about keeping the values in sync with .env-postgres. This
is only necessary if running the docker postgres container bundled into the tooling which
we are not.
OpenMapTiles is a lovely amalgamation of geo data from various open sources across the web.
Sources are currently: OSM, OSMData, OSM Lake Labels, NaturalEarth, and Wikidata.
We'll start off by dumping the latest OSMData, NaturalEarth, and OSM Lake Labels data into our database:
make import-dataNOTE: If you get an error saying the script gave up waiting for Postgres then you may have to
specify a different IP than localhost in your .env and .osmenv. I believe this is due
to an inconsistency with docker networking. I have yet to investigate it in depth.
When this finishes, verify the tables are present in your db. We can now start the process that'll take 95% of the time we're going to spend rolling out our OSM basemaps: the import.
For any import for a region smaller than the entire planet, you can look up the ID of the region on Geofabrik. This id can be used to run an import for just that region.
For example, to download just Massachusetts data, you'd run:
make download area=massachusettsFor the entire planet:
make download area=planetTo ensure our cache location is in the best place possible, i.e. an SSD, we can set the
IMPOSM_CACHE_DIR environment variable to a directory of our choosing. Add it to your
.osmenv file and re-source it before running an import.
The OSM data download directory is hardcoded to the data directory within the
OpenMapTiles repo. You can change this in the Makefile, but it isn't very worth it for
import performance. Just keep all your files on fast storage, and the PG tables on even
faster storage if at all possible.
DO NOT initiate the import from an SSH session without protecting it with tools like
screen, tmux, or even just unix backgrounding. If not done, the import process will
quit if you lose your SSH session along with all the import progress. These imports
cannot be resumed, forcing you to start from the beginning.
It's time for the import using imposm3. Run:
make import-osmThis process will take anywhere from 5-10 minutes for a region as large as a state or
European country, all the way to multiple days for the entire planet depending on your
hardware. Be patient and don't bail out of the import if it does not appear to be
making any progress. Monitor system IO with htop, iotop, and iftop.
If the import fails for any reason, you will see error output. In the case of a SQL issue check that you properly installed all extensions earlier. If PG runs out of space, you will need to expand the device PG stores tables on or move it entirely to a larger drive. Keep an eye on current disk utilization with:
watch -n 15 "df -h" | grep "<your drive>"It's worth noting here, in the case that you aren't familiar with imposm3, that the
import runs in three stages. The first stage unpacks the raw binary protocol buffer that
your OSM data was downloaded as. The importer indexes and caches everything in an on-disk
LevelDB. This on-disk cache takes up a lot more space than the raw downloaded OSM data.
My estimate is usually 3-4 times the size of the .osm.pbf file downloaded. Keep this
in mind when building out the DB server. You'll want enough storage space to keep the
raw data, the on-disk cache, and the PG tables all at the same time.
If you're strapped for disk space, you can monitor the import and nuke the raw data as soon as the first stage is done. Stage two begins and writes the data from the on-disk cache directly to your database, so you can safely get rid of the raw data here. Be mindful that you may need the raw data to retry from the beginning if something goes wrong with the import. Stage two, in general, is where you'll want to start keeping an eye on disk usage.
Stage three creates indexes and optimizes the tables before deploying them. Indexes can also cause fairly large disk usage. Be mindful here and make sure you don't run out of space. If you fill up, you'll have to re-run the whole import after expanding storage since this stage cannot be resumed from the middle.
imposm3 keeps a three-schema architecture in place to keep your data intact during an
update. The import schema contains tables being actively imported. Your first import
will sit in here before the importer deploys the tables to the public schema. When new
tables are deployed to the public schema, the existing tables are rotated into the
backup schema for safe keeping in the case of an issue with the tables you just deployed.
It's worth reading up on imposm3 documentation to learn how to manually roll back a
deployment.
When the import completes (hopefully without error), you have a few more small steps to complete before we can use the data.
If an OSM feature has key:wikidata, OpenMapTiles checks
the corresponding item in Wikidata and uses its labels for languages listed in the
openmaptiles.yaml in the repo.
Generated vector tiles will contain multiple languages in the name fields for objects, making our tiles able to be displayed for users using default languages other than English.
Import labels from Wikidata:
make import-wikidataThe last and arguably most tense step in this process is importing OpenMapTiles layer generation functions, views, and additional indices. This is the bread and butter that defines all the usable source layers contained in our generated vector tiles. Index and materialized view creation in this stage will consume additional disk space. Keep an eye out. Luckily this stage can be "resumed" if you hit a snag and need to add more disk space
Simply run:
make import-sqlCross your fingers. I've seen this script fail for so many reasons. If you followed
this guide exactly, you should not have an error thrown at you. In case you do,
however, you can comb through the generated sql files in build/sql in the repo to
diagnose any possible issues. I've ocassionally had to manually run some of these
when doing a full planet import while testing this guide. No conclusions have yet
been made as to what the cause is.
Re-running make import-sql is not harmful as it is idempotent. Correct issues and
re-run the script if you need to.
NOTE: Upgrading the openmaptiles repo that we cloned may lead to changes in the compiled SQL
if you have an existing schema installed. This may cause failures in the import-sql
stage. Keep an eye on the log output and drop any existing functions that may have their
return types changed.
If you got this far with no issues, you need to pat yourself on the back. It is done.
As previously mentioned, this guide assumes the use of the Tegola tile server as it is fairly simple to set up while remaining powerful and fast.
The assumption is made that you've followed the Tegola docs to stand up a cluster of tile server instances on your own. Their docs are great and plenty of additional help is available on their GitHub issues.
You will find an example Tegola config.toml alongside this README with all available
tile layers defined. Reference the OpenMapTiles schema
for fields to pull from the layer functions in the case that this guide becomes outdated.
You are going to want some sort of caching enabled. Luckily Tegola includes a caching mechanism with multiple backing storage plugins. S3 is fine, file caching is great if you've got a ton of fast SSD storage, and Redis is a decent option if you want to manage the cluster.
I recommend using LOD, an in-memory tile cache backed by Redis that sits in front of Tegola. With LOD you can disable Tegola's built in caching and leverage, in my opinion, a much faster cache that can be run at the edge rather than behind a proxy or firewall like Tegola does on its own. LOD gives you insight via stats and metrics and exposes powerful administrative functionality. It allows you to invalidate and prime portions of the cache for a given area or for the map as a whole. See a bad tile? One click later, it's removed from the cache and re-primed from Tegola.
Disclaimer: I built LOD.
// TODO coming soon
The vector tile server stack you just created may now be used with any MVT-compatible mapping library such as MapLibre (my favorite), MapboxGL.js, Leaflet using the MapboxVectorTile plugin, or OpenLayers using the built-in MVT source.
You'll need to load a style to actually view the MVT tiles on your map. You can create your own style using the Maputnik visual style editor for the Mapbox Style Specification.
This repository contains an example style.json derived from the MapTiler Streets style.
Font stacks and sprite sheets are included in the carto directory and must be hosted
alongside the tiles on some webserver. Be sure to change the URL to the tiles, glyphs,
and sprites in the style.json when deploying. Be mindful of CORS issues. There are a few
known issues with sprites for various icons, most notably highway icons. A PR investigating
and fixing would be appreciated.
Alternatively, you can use one of many open-source MVT styles compatible with the OpenMapTiles schema, made available from the OpenMapTiles contributors and team. Here are links to a few:
| Name | Source | Preview |
|---|---|---|
| MapTiler Basic | GitHub | MapTiler Cloud |
| OSM Bright | GitHub | MapTiler Cloud |
| Positron | GitHub | MapTiler Cloud |
| Dark Matter | GitHub | MapTiler Cloud |
| MapTiler Terrain | GitHub | MapTiler Cloud |
| MapTiler 3D | GitHub | N/A |
A full-stack example exists in this repo within the test directory. You will need to
edit the style.json within test/public to point to your tileserver properly after you've
rolled it out. Additionally, the URLs to the sprites and glyphs will work out of the box in
the example style.json since they're copied to the public directory.
You've done it. Congratulations! Vector tile basemaps are the future. Hopefully, you'll think so as well after using them in production for a bit.
