Public map server (container)
A read-only map for anyone, on a small shared machine. Worlds are built elsewhere (a build needs far more memory and time than serving) and copied over; the container holds only code.
1. Build the image
From the folder that holds worldgen/ and worldmap-viewer/:
podman build -f worldmap-viewer/deploy/Containerfile --ignorefile worldmap-viewer/deploy/.containerignore \
-t worldmap-viewer .
requirements.lock pins the libraries the worlds were built and checked with (same numbers → the same tiles as on
the build machine).
2. Copy a world
python deploy.py bundle WORLD_DIR server:/srv/data/map/WORLD_NAME [--res N] # --dry-run lists it first
Copies config/, places/regions.json and out/r<N>/, leaving out:
- pins, exports, lore notes and build inputs, which a public server never serves;
- refined areas and tiles of earlier builds.
It uses rsync -a, which keeps modification times. The build fingerprint depends on them; without them, the
server would rebuild every refined area. Nothing is deleted on the server: it prunes what it no longer uses at
start, and keeps the tiles it rendered.
Then, on the server, let the eras' serve caches share their identical blocks again (a copy loses that; ≈ half their size on btrfs or reflink XFS):
podman exec worldmap-viewer python deploy.py dedupe /data/WORLD_NAME
Data disk: btrfs with compress=zstd:1, normal copy-on-write (no chattr +C/nodatacow: that turns off both
compression and block sharing). Back up nothing under out/: it can all be copied again.
3. Run it
Settings come from WORLDMAP_<OPTION> variables (every serve.py option; a command-line flag wins). The image
sets WORLDMAP_PUBLIC=1, WORLDMAP_BIND=0.0.0.0, WORLDMAP_PORT=8765.
| Variable | Meaning |
|---|---|
WORLDMAP_WORLD |
world folder(s) in the container, comma separated (the first opens by default) |
WORLDMAP_RES |
build resolution to serve |
WORLDMAP_ALLOW_HOST |
the public name(s) the proxy passes on, e.g. maps.example.org |
WORLDMAP_WORKERS |
render processes (≈ one core each while rendering) |
WORLDMAP_MAX_RENDERS |
tiles rendering or waiting at once; more → 503 + Retry-After (default 4 × workers) |
WORLDMAP_TILE_CACHE_GB |
cap on saved tiles; the least recently used go first |
WORLDMAP_TILE_KEEP_Z |
relief + mesh tiles to this zoom are never deleted nor counted (a shipped prerender) |
Example Quadlet (/etc/containers/systemd/worldmap-viewer.container). This sizing is for a 4-core, 16 GB machine shared
with other services:
[Unit]
Description=Public world map
[Container]
Image=localhost/worldmap-viewer:latest
ContainerName=worldmap-viewer
Volume=/srv/data/map:/data:Z
Environment=WORLDMAP_WORLD=/data/world-a,/data/world-b,/data/world-c
Environment=WORLDMAP_ALLOW_HOST=maps.example.org
Environment=WORLDMAP_WORKERS=4
Environment=WORLDMAP_TILE_CACHE_GB=140
Environment=WORLDMAP_TILE_KEEP_Z=9
PublishPort=127.0.0.1:8765:8765
HealthCmd=python -c "import os, urllib.request as u; u.urlopen('http://127.0.0.1:' + os.environ['WORLDMAP_PORT'] + '/api/worlds', timeout=8)"
HealthStartPeriod=10m
[Service]
MemoryHigh=7G
MemoryMax=8G
CPUWeight=50
Restart=on-failure
[Install]
WantedBy=multi-user.target
Keep % out of Quadlet lines: systemd expands %s, %h and the like (write %% for a literal %). The HealthCmd
above builds its URL without one.
Leave WORLDMAP_RES unset when the worlds differ: each then serves its final build (world-a r5, the others r4).
A set WORLDMAP_RES applies to every world.
The image runs as uid 8765. /srv/data/map must be writable by that user as the container maps it: the server
writes tiles and serve caches there. :U would do this, but it re-chowns every file at each start, which is slow
with millions of tiles. Set the ownership once instead.
The first start after a new copy can take minutes, while the server loads or builds its serve caches. Ship them
built (they are part of out/r<N>/) and it starts in about half a minute.
Single-disk machines: set a smaller WORLDMAP_TILE_CACHE_GB (e.g. 20).