Skip to content

feat(deploy): bake custom_models into a thin GraphHopper image#75

Merged
cafca merged 1 commit into
mainfrom
deploy/graphhopper-image
May 7, 2026
Merged

feat(deploy): bake custom_models into a thin GraphHopper image#75
cafca merged 1 commit into
mainfrom
deploy/graphhopper-image

Conversation

@cafca
Copy link
Copy Markdown
Owner

@cafca cafca commented Apr 27, 2026

Summary

Replace the host-side bind mount of backend/custom_models/ with a thin custom GraphHopper image that copies the directory in at build time. Routing rules now ship atomically with docker compose pull — no host scp, no "did the file reach the server?" failure mode.

CD also gains a post-deploy probe that wipes the GH cache only when actually needed (i.e. when the new image carries a different profile hash from the one baked into the existing graph cache).

Changes

backend/graphhopper.Dockerfile (new):
```dockerfile
FROM israelhikingmap/graphhopper:latest
COPY backend/custom_models/ /custom_models/
```

compose.prod.yml: switch GH service to ghcr.io/cafca/beebeebike-graphhopper:latest. Bind mount of backend/custom_models is unnecessary (file is in the image now).

.github/workflows/cd.yml:

  • Build and push the GH image alongside the backend image (separate buildx cache scope so it doesn't bust backend builds).
  • After docker compose up -d, sleep 12s and grep recent GH logs for does not match. If found → stop GH, wipe data/osm/berlin/graphhopper, restart for a fresh import. Otherwise leave the cache alone.

Why

Today the routing rules live as a host file shipped via two separate paths (the scp source: list, and a compose.prod.yml bind mount). When PR #72 merged, both paths needed updates — neither got them — and prod GH crash-looped until the file was scp'd manually.

With the rules inside the GH image:

  • One image, one source of truth for the runtime that consumes it.
  • Rollback is automatic: docker pull an old tag and you get the matching custom model.
  • docker compose pull && up -d is the only deploy verb again.

Why the conditional cache wipe (vs. always wipe / vs. never wipe)

GraphHopper bakes the profile hash (priority/speed/distance_influence/turn_costs) into the imported graph. On boot it compares the configured hash to the stored one:

  • match → loads cache, ~10s startup.
  • mismatch → refuses to start: Profile 'bike' does not match.

Always-wiping turns every backend-only deploy into a multi-minute GH outage. Never-wiping breaks deploys that change the profile. The crash-loop signature is unambiguous, so detecting it post-up -d is reliable and lets the common path (model unchanged) stay fast.

Notes for reviewers

  • Supersedes fix(deploy): mount and sync backend/custom_models to prod #73. Once this lands, fix(deploy): mount and sync backend/custom_models to prod #73 can be closed without merging — the host bind mount approach is obsoleted.
  • The israelhikingmap/graphhopper:latest base is pinned to latest to match current behavior; consider pinning to a tag in a follow-up.
  • graphhopper_config.yml stays host-mounted because it changes more often than custom models and we don't want a CD round trip for config-only edits. Could revisit later.
  • First post-merge deploy will trigger the cache wipe (because helena's current cache was baked against the manual deploy I did yesterday with hash X, and the image-driven deploy will have hash X' even if the bytes are identical — distinct mount source = distinct hash for some GH builds; safer to assume wipe).

Test plan

  • CI build + push both images successfully on a force-push to a test branch.
  • Image pulls and starts cleanly on helena.
  • After this PR's deploy, prod GH logs show successful reimport (cache wipe path triggered once, then steady).
  • Subsequent backend-only deploys do not wipe the cache and don't fire the wipe path (verify by deploying a no-op backend change and checking CD logs for Profile hash mismatch detected).

Replace the host-side bind mount of `backend/custom_models/` with a
custom GH image that COPYs the dir at build time. CD builds and pushes
`ghcr.io/cafca/beebeebike-graphhopper:latest` alongside the backend
image; `compose.prod.yml` pulls it. Routing rules now ship atomically
with `docker compose pull`, no scp dance, no separate sync path.

CD also gains a post-deploy probe: if the freshly-started GH container
logs `Profile 'X' does not match` (i.e. the new image carries different
priority/speed/distance_influence rules from what the cached graph was
baked against), CD wipes `data/osm/berlin/graphhopper` and reimports.
Cache is preserved across normal backend-only deploys.

Supersedes #73 — the host-side custom_models scp + bind mount approach
becomes unnecessary once the file lives inside the image that reads it.
@cafca cafca marked this pull request as ready for review May 7, 2026 13:45
@cafca cafca merged commit cce00cb into main May 7, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant