OSM ベクトルタイル自前ホスティング構築記録
Cloudflare R2 + Workers + PMTiles
コミュニティ運営のタイルサーバー
tile.openstreetmap.jp への依存をやめ、
日本全国の地図タイルを月額ほぼ 0 円で自前配信するまでの全記録。
- 構築日: 2026年8月4日
- 配信ドメイン:
tiles.naughty-ghost.org - リポジトリ: naughty-ghost/map-tiles
目次
1. 背景と課題
地域の危険度を可視化するアプリ hazardmap(Web /
Android / iOS)は、地図表示に OSMFJ(OpenStreetMap Foundation
Japan)が運営する無料コミュニティタイルサーバー
tile.openstreetmap.jp を利用していた。
しかし次の課題があった。
- タイル読み込みが稀にエラーになる事象を確認していた
- 無料・コミュニティ運営のため、商用利用を想定した信頼性保証がない
- 配信の可用性を自分たちで制御できない
そこで「日本全国の OSM
ベクトルタイルを自前ホスティングし、外部タイルサーバーへの依存を完全に廃止する」
ことにした。要件定義〜詳細設計は naughty-ghost/hazardmap
リポジトリ(非公開)で実施し、
実体のインフラは複数アプリから共用できる汎用基盤として独立リポジトリ
naughty-ghost/map-tiles
に切り出した。
- 常時稼働するタイルサーバー(TileServer GL / Martin 等)は運用しない — 静的ファイル配信方式
- 現行スタイル 2 種(ライト / ダーク)と同等の見た目を維持する
- 月 1 回程度の頻度で OSM 元データを取り込み、タイルを差し替えられる運用フロー
- 現行と同等以上の読み込み速度・タイル取得エラー率の低減
2. 設計判断
| 論点 | 採用した選択 | 理由 |
|---|---|---|
| タイルの持ち方 | PMTiles(単一静的ファイル) | 数百万個のタイルを 1 ファイルにまとめ、HTTP Range Request で必要な部分だけ読める。 オブジェクトストレージに置くだけで配信でき、タイルサーバーの常時稼働が不要 |
| 配信方式 | エッジ(Worker)で z/x/y URL に展開 | クライアントは標準のタイル URL を叩くだけ。フロントエンドに pmtiles ライブラリの追加が不要で、変更は「スタイル URL の差し替えのみ」に収まる。 CDN キャッシュがタイル単位で効く |
| ホスティング先 | Cloudflare R2 + Workers + CDN | エグレス(転送量)課金が無料。読み取り偏重のタイル配信に最適。 Protomaps 公式のサーバーレス実装がそのまま使える。S3 + CloudFront だと転送量課金が支配的になる |
| タイル生成ツール | Planetiler | 現行スタイルは OpenMapTiles スキーマ前提。Planetiler は同スキーマを公式プロファイルで 生成できるため、スタイル定義を無改変で流用でき「見た目同等」を最小工数で満たせる |
| グリフ(フォント) | 現行サーバーの実物を流用 | 再生成ではなく配信中の実物(Migu 系 3 スタック)を取得して配置することで、 ラベル描画の見た目同等を確実に担保 |
3. アーキテクチャ
構成(上から下へリクエストが流れる):
- クライアント(MapLibre GL) — Web / Android / iOS。style.json の URL を参照するだけ
- Cloudflare CDN — tiles.naughty-ghost.org(カスタムドメイン・キャッシュ 24h)
-
Cloudflare Worker(map-tiles) —
/tiles/…→ PMTiles から Range Request でタイル切り出し。/styles/…・/fonts/…→ R2 の静的ファイルをそのまま配信。/tiles/{source}.json→ TileJSON をメタデータから動的生成 - Cloudflare R2(バケット: map-tiles / apac) — japan.pmtiles(1.67GB)+ スタイル 2 種 + スプライト + グリフ 768 ファイル
エンドポイント契約
| パス | 内容 |
|---|---|
/styles/{name}/style.json |
MapLibre スタイル(osm-bright-ja = ライト / maptiler-basic-ja = ダーク) |
/tiles/{source}/{z}/{x}/{y}.mvt |
ベクトルタイル本体(source = japan | takeshima | hoppo) |
/tiles/{source}.json |
TileJSON(zoom 0–14、帰属表示を含む) |
/fonts/{fontstack}/{range}.pbf |
グリフ(Migu 系 3 スタック × 256 range) |
/styles/{name}/sprite*.{json,png} |
スプライト(アイコン・国道番号バッジ等) |
Worker は
protomaps/PMTiles
公式の Cloudflare
実装をベースに、上記のパス構成と静的ファイル配信を追加した約 250 行の
TypeScript。 新しいタイルソースを追加したくなったら
{name}.pmtiles を R2 に置くだけで
/tiles/{name}/… として配信される。
4. コスト設計 — 月額ほぼ 0 円のからくり
| 項目 | 単価 | 本件の見込み |
|---|---|---|
| R2 ストレージ | $0.015/GB・月(無料枠 10GB) | $0(実測 約1.7GB) |
| R2 読み取り(Class B) | $0.36/100万件(無料枠 1,000万/月) | $0 |
| R2 書き込み(Class A) | $4.50/100万件(無料枠 100万/月) | $0(月次アップロードのみ) |
| Workers | 無料枠 10万リクエスト/日 | $0 |
| CDN・DNS・TLS・エグレス | 無料 | $0 |
R2 には支出上限(ハードキャップ)を設定する機能がない。しかし本構成では R2 への読み取りは すべて Worker 経由であり、Workers 無料プランは 10万リクエスト/日を超えると 課金ではなくエラーで停止する。つまり Worker が構造的なボトルネックとなり、 R2 の読み取りオペレーションは無料枠に到達し得ない。 課金が発生し得るのはストレージの無料枠超過分だけ(現状は枠内なので 0 円)。
加えて R2 の使用量通知を 5GB(定常の約 2.5 倍)に設定し、二重アップロードや マルチパートの不完全パーツ残留といった異常を早期検知できるようにした。 トラフィックが恒常的に増えたら Workers Paid($5/月・1,000万リクエスト込み)へ移行する。
5. 構築ステップ全記録
-
リポジトリ作成と Worker プロジェクトの雛形作成(naughty-ghost/map-tiles)
Protomaps 公式の Cloudflare Worker 実装を取得してエンドポイント契約に合わせて改変し、 wrangler 設定・スタイル生成スクリプト・運用手順書(README)とともに push。 TypeScript 型チェックを通してからコミットした。
-
Cloudflare 側の準備(ダッシュボード + wrangler login)
R2 の有効化(無料枠でも支払い方法の登録が必須)と
npx wrangler loginによる OAuth 認証。ドメインnaughty-ghost.orgは既に Cloudflare 管理下だったため ゾーン追加は不要だった。 -
R2 バケット作成(location hint: apac)
npx wrangler r2 bucket create map-tiles --location apac利用者が日本のため、データの物理配置をアジア太平洋リージョンに寄せた。
-
タイル生成 — Planetiler(Java 21(Corretto ポータブル版)/ 処理 13 分 / 出力 1.67GB)
java -Xmx8g -jar planetiler.jar --download --area=japan --output=japan.pmtilesGeofabrik の日本抽出データ(2.3GB)と水域等の共通データを自動ダウンロードし、 日本全国 zoom 0–14 のベクトルタイルを生成。事前見積もりは「10GB・1〜2 時間」だったが、 実際は 1.67GB・13 分で完了した。
-
スタイル・スプライト・グリフの準備(スクリプト 2 本で自動化)
build-styles.mjsが現行スタイル 2 種を取得して sources / glyphs / sprite の URL を 自前 CDN に書き換え、スプライトも同時取得。続いてfetch-fonts.mjsが スタイルの参照する Migu 系 3 フォントスタック × 全 256 range = 768 グリフファイルを 取得(失敗 0 件、計 28MB)。 -
R2 アップロード — rclone(計 786 オブジェクト)
rclone copy japan.pmtiles r2:map-tiles/ --s3-no-check-bucket \ --s3-upload-cutoff=100M --s3-chunk-size=100M rclone copy dist/ r2:map-tiles/ --s3-no-check-bucket1.67GB の本体は 100MB チャンクのマルチパートアップロード。wrangler の 300MiB 制限を超えるため rclone(S3 互換 API)を採用した。
-
Worker デプロイ + カスタムドメイン(wrangler deploy 一発)
wrangler.tomlにcustom_domain = trueを書いておくだけで、 デプロイと同時にtiles.naughty-ghost.orgの DNS レコードと TLS 証明書が 自動作成された。Worker 本体のアップロードは gzip 後 10.6KB。
6. つまずきと解決
実作業で遭遇した問題は 4 件。いずれも原因を特定して解決済み。
| 事象 | 原因 | 解決 |
|---|---|---|
npm install が ERESOLVE で失敗 |
wrangler v4 の peer dependency が
@cloudflare/workers-types の v5
系に更新されていた
|
devDependencies を ^5.20260730.1 に更新 |
rclone lsd r2: が 403 |
バケット限定トークンではアカウント全体の ListBuckets が拒否される正常な挙動 |
バケット直接指定(rclone lsf r2:map-tiles)で確認するよう変更。エラーではなかった
|
| アップロードが PutObject 403 で全滅 | ①作成されたトークンが読み取り専用だった ②権限修正後もバケット存在確認(HeadBucket)が 403 になり、rclone がバケットを作成しようとして失敗 |
①トークンを「オブジェクトの読み取りと書き込み」に修正 ②--s3-no-check-bucket
フラグで存在確認をスキップ(バケット限定トークンの定石)
|
| HEAD リクエストだけ 500 | Worker のキャッシュ書き込み処理が HEAD と干渉(GET は全パス正常) | MapLibre は GET しか使わないため、GET のみ許可(他メソッドは 405)に修正してデプロイ |
7. 動作確認の結果
【合格】ライト / ダーク両スタイルを、ズーム 4 段階(z4 全国 / z10 都市 / z14 タイル最大 / z16 オーバーズーム)で実表示確認。
| 確認項目 | 結果 |
|---|---|
| タイル配信(GET、全パス) | 200 OK — TileJSON・タイル・スタイル・グリフ・スプライトすべて正常 |
| 日本語ラベル(Migu グリフ) | 正常 — 市区町村名・丁目・道路名まで描画 |
| スプライト(国道番号バッジ等) | 正常 |
| z16 オーバーズーム | 正常 — z14 タイルの拡大描画(建物・小路まで精細) |
| 帰属表示 | 正常 — TileJSON の attribution に © OpenMapTiles と © OpenStreetMap contributors の両方を確認 |
| TileJSON の zoom 範囲 | 0–14 — 設計の期待値どおり |
| 国外陸地(台湾・朝鮮半島等) | 設計どおり非表示 — 日本抽出データのため。許容済みの既知の制限 |
| takeshima / hoppo ソース | 【未配置】TileJSON が 404(コンソールエラーのみで他地域の描画に影響なし)。データ確保を継続検討中 |
8. ライブデモ
以下は実際に稼働中の自前配信基盤(tiles.naughty-ghost.org)からタイルを読み込む MapLibre の地図。
9. ライセンスと帰属表示
| 対象 | ライセンス | 対応 |
|---|---|---|
| タイルデータ(OSM) | ODbL | TileJSON の attribution で「© OpenStreetMap contributors」を表示(義務) |
| 生成タイル(OpenMapTiles スキーマ) | CC-BY | 「© OpenMapTiles」のクレジットも必要と Planetiler 生成ログで判明。attribution に自動付与されていることを確認済み |
| Worker コード | BSD-3-Clause(protomaps/PMTiles 由来) | ソースコードに出典を明記 |
| グリフ(Migu フォント) | IPA フォントライセンス v1.0 | 再配布可を確認し README に記録 |
| スタイル JSON・スプライト | BSD 3-Clause(コード)/ CC BY 4.0(デザイン) | osm-bright-ja は openmaptiles/osm-bright-gl-style、 maptiler-basic-ja は openmaptiles/maptiler-basic-gl-style 由来と確認。CC BY 4.0 が義務付ける OpenMapTiles へのクレジット表示は TileJSON の attribution 経由で充足済み |
10. 運用 — 月次更新フロー
月 1 回、3 コマンド + キャッシュパージで最新の OSM データに差し替えられる。
# 1. 最新データで再生成(--download が japan-latest.osm.pbf を自動取得)
java -Xmx8g -jar planetiler.jar --download --area=japan --output=japan.pmtiles
# 2. R2 へ差し替えアップロード
rclone copy japan.pmtiles r2:map-tiles/ --s3-no-check-bucket \
--s3-upload-cutoff=100M --s3-chunk-size=100M
# 3. Cloudflare ダッシュボード → キャッシュ → すべてをパージ
プレフィックス指定パージは Enterprise プラン限定のため全パージで運用(タイルは 24 時間で 再キャッシュされるため実用上の影響は軽微)。スタイル・グリフ・スプライトは変更時のみ再アップロード。 所要時間は生成 13 分 + アップロード数分。
11. まとめ
外部依存だった地図タイル配信を、約半日の作業で自前基盤に置き換える準備が整った。 ランニングコストは実質 0 円(すべて無料枠内・課金経路も構造的に遮断)、 月次更新は15 分程度の定型作業。基盤は汎用設計のため、 今後の別アプリでもスタイル URL を参照するだけで同じ地図が使える。
更新履歴
- 新規作成。