- Cartographie
- OpenStreetMap
- Docker
- Traefik
- MapLibre
Héberger ses propres tuiles vectorielles OpenStreetMap avec Docker et Traefik
Tutoriel : générer des tuiles vectorielles OpenStreetMap avec Planetiler, les servir avec Martin et Docker derrière Traefik en HTTPS, les afficher avec MapLibre.
Afficher une carte dans une application web ne demande pas de dépendre d’un fournisseur de cartes. Avec des tuiles vectorielles générées à partir d’OpenStreetMap et servies par votre propre serveur de tuiles, vous maîtrisez les coûts, la disponibilité et les données : aucune requête de vos utilisateurs ne part chez un tiers.
C’est le principe de G-ordon Planisphère, la cartographie de notre logiciel G-ordon : des cartes issues d’OpenStreetMap, servies par nos propres serveurs. Ce tutoriel montre une façon de le faire, de bout en bout, avec des outils libres :
- Planetiler génère les tuiles à partir des données OpenStreetMap ;
- Martin les sert, avec les polices et le style, dans un conteneur Docker ;
- Traefik les publie en HTTPS, avec un certificat Let’s Encrypt ;
- MapLibre GL JS les affiche dans le navigateur.
Tuiles raster ou tuiles vectorielles ?
Une tuile raster est une image : le serveur dessine la carte, le navigateur l’affiche. Une tuile vectorielle transporte des données (routes, bâtiments, noms de lieux) que le navigateur dessine lui-même, selon un style.
| Tuiles raster | Tuiles vectorielles | |
|---|---|---|
| Ce qui circule | Des images PNG ou JPEG | Des géométries et des attributs |
| Rendu | Sur le serveur | Dans le navigateur |
| Changer l’apparence | Régénérer toutes les tuiles | Modifier le style, rien d’autre |
| Volume à héberger | Élevé (une image par niveau de zoom) | Nettement plus faible |
| Netteté | Pixelisée entre deux niveaux | Nette à tous les zooms, rotation possible |
Pour une application métier, le vectoriel s’impose : un seul jeu de tuiles sert plusieurs styles (clair, sombre, impression), et les étiquettes restent lisibles à tous les zooms.
L’architecture cible
Navigateur (MapLibre GL JS)
│ HTTPS
▼
Traefik ── certificat Let's Encrypt, en-têtes de cache, CORS
│ réseau Docker interne
▼
Martin ── tuiles (PMTiles), polices (glyphes), style
│
▼
Fichier france.pmtiles, généré par PlanetilerSeul Traefik est exposé sur internet (ports 80 et 443). Martin n’écoute que sur le réseau Docker interne.
Étape 1 : générer les tuiles avec Planetiler
Planetiler télécharge un extrait OpenStreetMap
(fourni par Geofabrik) et produit les tuiles selon le schéma OpenMapTiles, le plus répandu.
L’extension du fichier de sortie choisit le format : .pmtiles, un fichier unique facile à
déplacer et à servir.
Commencez par un petit territoire pour valider la chaîne, Monaco par exemple :
mkdir -p data
docker run --rm \
-e JAVA_TOOL_OPTIONS="-Xmx2g" \
-v "$(pwd)/data":/data \
ghcr.io/onthegomap/planetiler:latest \
--download --area=monaco --output=/data/monaco.pmtilesPuis passez à votre territoire, la France par exemple :
docker run --rm \
-e JAVA_TOOL_OPTIONS="-Xmx8g" \
-v "$(pwd)/data":/data \
ghcr.io/onthegomap/planetiler:latest \
--download --area=france --output=/data/france.pmtiles --forceQuelle machine pour la génération ?
La génération est le seul moment gourmand de toute la chaîne : servir les tuiles demande très peu de ressources. La bonne pratique est donc de louer une machine puissante le temps de la génération, puis de la supprimer, et de copier le fichier PMTiles sur le serveur qui le sert. Chez Scaleway, notre hébergeur, deux familles s’y prêtent :
| Machine Scaleway | Mémoire | Disque | Pour quoi faire |
|---|---|---|---|
| Instance POP2-HM-8C-64G | 64 Go, 8 vCPU | Volume Block à ajouter | Une région ou un pays, une génération de temps en temps |
| Instance POP2-HM-16C-128G | 128 Go, 16 vCPU | Volume Block à ajouter | Plusieurs pays, l’Europe |
| Elastic Metal EM-I210E-NVMe | 128 Go, 16 cœurs | 2 × 1,92 To NVMe | Des générations fréquentes ou de grandes zones |
| Elastic Metal EM-B520E-NVMe | 256 Go | 2 × 2 To NVMe | La planète entière |
- Les Instances (gamme Scaleway Elements) sont facturées à l’heure : une POP2-HM-8C-64G coûte 0,41 € HT de l’heure selon la grille publique de Scaleway, de quoi générer un pays pour quelques euros. Ajoutez un volume Block SSD assez grand pour l’extrait et les fichiers de travail.
- Les serveurs Elastic Metal sont des machines physiques dédiées, avec des disques NVMe locaux : Planetiler écrit beaucoup de fichiers temporaires, et un disque rapide raccourcit nettement la génération.
Les gammes et les prix évoluent : vérifiez-les sur la page des tarifs Scaleway avant de choisir.
Étape 2 : servir les tuiles avec Martin
Martin est le serveur de tuiles du projet MapLibre. Il sert des fichiers PMTiles ou MBTiles, mais aussi les polices (glyphes) dont le navigateur a besoin pour écrire les noms de lieux, les sprites (icônes) et les styles.
Les polices
Les étiquettes d’une carte vectorielle sont dessinées avec des glyphes servis par le serveur.
Martin les produit à partir de fichiers de police classiques : placez par exemple les fichiers
.ttf de Noto Sans (licence OFL) dans un
dossier fonts/. Chaque police est ensuite désignée par sa famille et sa graisse, par exemple
Noto Sans Regular.
Le fichier de configuration
# martin/config.yaml
listen_addresses: 0.0.0.0:3000
# Seule l'application a le droit d'appeler le serveur depuis un navigateur.
cors:
origin:
- https://app.example.org
max_age: 3600
pmtiles:
sources:
france: /data/france.pmtiles
fonts:
paths:
- /fonts
styles:
sources:
base: /styles/base.jsonMartin publie alors :
| Adresse | Contenu |
|---|---|
/catalog | La liste des sources |
/france | La description TileJSON de la source |
/france/{z}/{x}/{y} | Les tuiles |
/font/{police}/{début}-{fin} | Les glyphes d’une police |
/style/base | Le style de la carte |
Étape 3 : un style minimal
Le style décrit, couche par couche, comment dessiner les données. Les noms de couches
(water, transportation, building, place…) sont ceux du schéma OpenMapTiles. Voici un
style volontairement court, à enrichir :
{
"version": 8,
"name": "Base",
"glyphs": "https://tiles.example.org/font/{fontstack}/{range}",
"sources": {
"openmaptiles": {
"type": "vector",
"url": "https://tiles.example.org/france",
"attribution": "© OpenMapTiles © Contributeurs OpenStreetMap"
}
},
"layers": [
{ "id": "background", "type": "background",
"paint": { "background-color": "#f4f1ea" } },
{ "id": "water", "type": "fill", "source": "openmaptiles", "source-layer": "water",
"paint": { "fill-color": "#a8c8e8" } },
{ "id": "park", "type": "fill", "source": "openmaptiles", "source-layer": "park",
"paint": { "fill-color": "#d5e8c4" } },
{ "id": "building", "type": "fill", "source": "openmaptiles", "source-layer": "building",
"minzoom": 14, "paint": { "fill-color": "#e0dbd2" } },
{ "id": "roads", "type": "line", "source": "openmaptiles", "source-layer": "transportation",
"paint": {
"line-color": "#ffffff",
"line-width": ["interpolate", ["linear"], ["zoom"], 8, 0.5, 16, 6]
} },
{ "id": "places", "type": "symbol", "source": "openmaptiles", "source-layer": "place",
"layout": {
"text-field": ["coalesce", ["get", "name:fr"], ["get", "name"]],
"text-font": ["Noto Sans Regular"],
"text-size": 13
},
"paint": { "text-color": "#333333", "text-halo-color": "#ffffff", "text-halo-width": 1.5 } }
]
}Pour un rendu complet sans partir de zéro, des styles libres conçus pour le schéma OpenMapTiles existent (OSM Bright, Positron…) : il suffit d’y remplacer les adresses des tuiles et des glyphes par les vôtres, et de vérifier leur licence.
Étape 4 : publier en HTTPS avec Traefik
Traefik découvre les conteneurs Docker, obtient les
certificats Let’s Encrypt et applique des règles déclarées par des labels. Voici le
docker-compose.yml complet :
# docker-compose.yml
services:
traefik:
image: traefik:${TRAEFIK_VERSION}
command:
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --entrypoints.web.http.redirections.entrypoint.to=websecure
- --entrypoints.web.http.redirections.entrypoint.scheme=https
- --certificatesresolvers.le.acme.tlschallenge=true
- --certificatesresolvers.le.acme.email=${ACME_EMAIL}
- --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt:/letsencrypt
restart: unless-stopped
martin:
image: ghcr.io/maplibre/martin:${MARTIN_VERSION}
command: ["--config", "/config/config.yaml"]
volumes:
- ./martin:/config:ro
- ./data:/data:ro
- ./fonts:/fonts:ro
- ./styles:/styles:ro
labels:
- traefik.enable=true
- traefik.http.services.tiles.loadbalancer.server.port=3000
# Les tuiles : changent une fois par mois, gardées un jour, resservies une semaine
# pendant leur revalidation.
- traefik.http.routers.tiles.rule=Host(`tiles.example.org`)
- traefik.http.routers.tiles.entrypoints=websecure
- traefik.http.routers.tiles.tls.certresolver=le
- traefik.http.routers.tiles.service=tiles
- traefik.http.routers.tiles.middlewares=tiles-gzip,cache-tiles
# Les polices : ne changent jamais, gardées un mois.
- traefik.http.routers.fonts.rule=Host(`tiles.example.org`) && PathPrefix(`/font`)
- traefik.http.routers.fonts.entrypoints=websecure
- traefik.http.routers.fonts.tls.certresolver=le
- traefik.http.routers.fonts.service=tiles
- traefik.http.routers.fonts.middlewares=tiles-gzip,cache-fonts
# Le style et les descriptions : changent quand vous les retouchez, gardés cinq minutes.
- traefik.http.routers.style.rule=Host(`tiles.example.org`) && (PathPrefix(`/style`) || Path(`/catalog`) || Path(`/france`))
- traefik.http.routers.style.entrypoints=websecure
- traefik.http.routers.style.tls.certresolver=le
- traefik.http.routers.style.service=tiles
- traefik.http.routers.style.middlewares=tiles-gzip,cache-style
# Compression gzip (et Brotli pour les navigateurs qui la demandent).
- traefik.http.middlewares.tiles-gzip.compress=true
- traefik.http.middlewares.tiles-gzip.compress.encodings=gzip,br
# En-têtes de cache, un par type de contenu.
- traefik.http.middlewares.cache-tiles.headers.customresponseheaders.Cache-Control=public, max-age=86400, stale-while-revalidate=604800
- traefik.http.middlewares.cache-fonts.headers.customresponseheaders.Cache-Control=public, max-age=2592000, immutable
- traefik.http.middlewares.cache-style.headers.customresponseheaders.Cache-Control=public, max-age=300
restart: unless-stoppedEt le fichier .env qui l’accompagne :
# .env : épinglez des versions précises, publiées et testées.
TRAEFIK_VERSION=v3.x.y
MARTIN_VERSION=x.y.z
ACME_EMAIL=[email protected]Démarrez l’ensemble :
docker compose up -d
curl -sI https://tiles.example.org/catalogLa compression et le cache, réglés par type de contenu
Trois routeurs pointent vers le même service Martin. Traefik choisit le plus précis : une
adresse en /font/… prend le routeur fonts, une adresse en /style/… le routeur style,
et tout le reste, c’est-à-dire les tuiles, le routeur tiles. Chacun reçoit sa durée de
cache :
| Contenu | Durée de cache | Pourquoi |
|---|---|---|
Tuiles /france/{z}/{x}/{y} | Un jour, resservies une semaine pendant la revalidation | Elles ne changent qu’à la mise à jour mensuelle |
Polices /font/… | Un mois, immutable | Un glyphe ne change jamais |
| Style, TileJSON, catalogue | Cinq minutes | Une retouche du style doit se voir vite |
La directive stale-while-revalidate permet au navigateur, ou à un CDN, d’afficher tout de
suite une tuile un peu ancienne pendant qu’il en vérifie la fraîcheur : la carte ne reste
jamais vide.
Pour vérifier :
# Une tuile : déjà compressée par Martin, avec son cache d'un jour.
curl -sI -H "Accept-Encoding: gzip, br" https://tiles.example.org/france/12/2064/1495 \
| grep -iE "content-encoding|cache-control"
# Le style : compressé par Traefik, cinq minutes de cache.
curl -sI -H "Accept-Encoding: gzip, br" https://tiles.example.org/style/base \
| grep -iE "content-encoding|cache-control"Quatre points de sécurité à ne pas manquer :
Martin reste en coulisse
Le risque
Publier le port de Martin le rendrait joignable directement, sans HTTPS ni en-têtes de cache.
La parade
Pas de section
ports: seul Traefik le joint, sur le réseau Docker. Seuls les ports 80 et 443 sont ouverts sur le serveur.Le socket Docker est puissant
Le risque
Il donne à Traefik une vue sur tous les conteneurs du serveur.
La parade
Il est monté en lecture seule ; pour aller plus loin, un proxy de socket limite ce que Traefik peut demander au démon Docker.
Rien n'est publié par défaut
Le risque
Sans
exposedbydefault=false, tout nouveau conteneur serait exposé sur internet.La parade
Seuls les conteneurs qui portent
traefik.enable=truesont publiés.Le tableau de bord reste fermé
Le risque
Le tableau de bord de Traefik révèle les routes, les services et les certificats.
La parade
Il n’est pas activé ici ; s’il l’est, protégez-le par une authentification et un accès restreint.
Étape 5 : afficher la carte avec MapLibre GL JS
Installez la bibliothèque dans votre application, plutôt que de la charger depuis un CDN tiers : c’est la logique de l’auto-hébergement jusqu’au bout.
npm install maplibre-glimport maplibregl from "maplibre-gl";
import "maplibre-gl/dist/maplibre-gl.css";
const map = new maplibregl.Map({
container: "map",
style: "https://tiles.example.org/style/base",
center: [1.444, 43.604], // Toulouse
zoom: 12,
});
map.addControl(new maplibregl.NavigationControl());La carte s’affiche, avec l’attribution en bas à droite. Changer d’apparence revient désormais
à modifier base.json, sans toucher aux tuiles.
Mettre à jour les données
OpenStreetMap évolue chaque jour. Une mise à jour mensuelle suffit pour la plupart des usages métier.
#!/usr/bin/env sh
# update-tiles.sh : à lancer par une tâche planifiée (cron), par exemple chaque mois.
set -eu
cd /srv/tiles
docker run --rm -e JAVA_TOOL_OPTIONS="-Xmx8g" -v "$(pwd)/data":/data \
ghcr.io/onthegomap/planetiler:latest \
--download --area=france --output=/data/france-next.pmtiles --force
# Bascule : le nouveau fichier remplace l'ancien d'un coup, puis Martin le recharge.
mv data/france-next.pmtiles data/france.pmtiles
docker compose restart martinVariante sans serveur de tuiles : PMTiles sur un stockage objet
Le format PMTiles a été pensé pour être lu directement par le navigateur, par requêtes
HTTP partielles (en-tête Range). Déposé sur un stockage objet compatible S3, le fichier
france.pmtiles n’a plus besoin de Martin :
npm install pmtilesimport maplibregl from "maplibre-gl";
import { Protocol } from "pmtiles";
const protocol = new Protocol();
maplibregl.addProtocol("pmtiles", protocol.tile);
// Dans le style : la source pointe directement sur le fichier.
// "url": "pmtiles://https://cartes.example.org/france.pmtiles"
Martin garde l’avantage quand vous voulez combiner plusieurs sources, servir des données métier depuis PostGIS à côté du fond de carte, ou garder la main sur les en-têtes et les accès.
Questions fréquentes
Faut-il une clé d'API pour afficher une carte OpenStreetMap ?
Non. Avec des tuiles vectorielles hébergées sur vos serveurs, aucun compte ni clé n’est nécessaire chez un fournisseur de cartes. Les données OpenStreetMap sont libres, sous licence ODbL : seule l’attribution « © Contributeurs OpenStreetMap » est obligatoire.
Quelle différence entre tuiles raster et tuiles vectorielles ?
Une tuile raster est une image dessinée par le serveur. Une tuile vectorielle transporte des géométries et des attributs que le navigateur dessine selon un style : un seul jeu de tuiles sert alors plusieurs apparences, nettes à tous les zooms.
Peut-on se passer d'un serveur de tuiles ?
Oui : un fichier PMTiles déposé sur un stockage objet compatible S3 est lu directement par MapLibre, par requêtes HTTP partielles. Le stockage doit accepter les requêtes Range et le CORS. Un serveur comme Martin reste utile pour combiner plusieurs sources ou servir des données PostGIS.
Quelle machine pour générer les tuiles d'un pays comme la France ?
La génération demande surtout de la mémoire et un disque rapide. Une instance à mémoire renforcée, par exemple 8 vCPU et 64 Go de RAM, convient pour une région ou un pays ; la planète entière demande un serveur dédié bien plus large.
À quelle fréquence mettre à jour les tuiles ?
Selon vos besoins : une régénération mensuelle, lancée par une tâche planifiée, suffit à la plupart des applications métier. La carte reste disponible pendant toute la génération : le nouveau fichier ne remplace l’ancien qu’une fois terminé.
En résumé
| Brique | Rôle | Alternative |
|---|---|---|
| Planetiler | Génère les tuiles depuis OpenStreetMap | Tilemaker, OpenMapTiles (outils d’origine) |
| PMTiles | Un fichier unique, facile à déplacer | MBTiles (SQLite) |
| Martin | Sert tuiles, glyphes et styles | Tileserver GL, ou PMTiles en direct |
| Traefik | HTTPS, certificats, en-têtes | NGINX, Caddy |
| MapLibre GL JS | Dessine la carte dans le navigateur | OpenLayers |
Une carte auto-hébergée, c’est quelques conteneurs, un fichier de données et un style. En échange, vos cartes ne dépendent plus d’un tiers, ni pour leur prix, ni pour leur disponibilité, ni pour la confidentialité de ce que vos utilisateurs consultent.