aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/deploy
diff options
context:
space:
mode:
Diffstat (limited to 'deploy')
-rw-r--r--deploy/.containerignore13
-rw-r--r--deploy/Containerfile27
-rw-r--r--deploy/README.md101
-rw-r--r--deploy/requirements.lock6
4 files changed, 147 insertions, 0 deletions
diff --git a/deploy/.containerignore b/deploy/.containerignore
new file mode 100644
index 0000000..62a308d
--- /dev/null
+++ b/deploy/.containerignore
@@ -0,0 +1,13 @@
+# build context = the folder holding worldgen/ and worldmap-viewer/: send only the code
+*
+!worldgen/mapgen
+!worldmap-viewer/*.py
+!worldmap-viewer/LICENSE
+!worldmap-viewer/CREDITS.md
+!worldmap-viewer/licenses.md
+!worldmap-viewer/deploy/requirements.lock
+!worldmap-viewer/viewer/index.html
+!worldmap-viewer/viewer/style.css
+!worldmap-viewer/viewer/js
+!worldmap-viewer/viewer/vendor
+**/__pycache__
diff --git a/deploy/Containerfile b/deploy/Containerfile
new file mode 100644
index 0000000..4e82eea
--- /dev/null
+++ b/deploy/Containerfile
@@ -0,0 +1,27 @@
+# The map server for a public, read-only map. Build from the folder that holds worldgen/ and worldmap-viewer/:
+# podman build -f worldmap-viewer/deploy/Containerfile -t worldmap-viewer .
+# Code only: worlds (config/, places/, built out/) are mounted at run time — see deploy/README.md.
+FROM docker.io/library/python:3.14-slim
+
+ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 \
+ WORLDGEN=/app/worldgen NUMBA_CACHE_DIR=/var/cache/worldmap-viewer/numba \
+ OPENBLAS_THREAD_TIMEOUT=4 \
+ WORLDMAP_PUBLIC=1 WORLDMAP_BIND=0.0.0.0 WORLDMAP_PORT=8765
+
+COPY worldmap-viewer/deploy/requirements.lock /app/requirements.lock
+RUN pip install --no-cache-dir --root-user-action=ignore -r /app/requirements.lock
+
+COPY worldgen/mapgen /app/worldgen/mapgen
+COPY worldmap-viewer/*.py worldmap-viewer/LICENSE worldmap-viewer/CREDITS.md worldmap-viewer/licenses.md /app/worldmap-viewer/
+COPY worldmap-viewer/viewer/index.html worldmap-viewer/viewer/style.css /app/worldmap-viewer/viewer/
+COPY worldmap-viewer/viewer/js /app/worldmap-viewer/viewer/js
+COPY worldmap-viewer/viewer/vendor /app/worldmap-viewer/viewer/vendor
+
+RUN useradd --uid 8765 --no-create-home --home-dir /nonexistent worldmap-viewer \
+ && mkdir -p /var/cache/worldmap-viewer/numba && chown -R worldmap-viewer /var/cache/worldmap-viewer
+USER worldmap-viewer
+WORKDIR /app/worldmap-viewer
+EXPOSE 8765
+# health check (Quadlet: HealthCmd=; OCI images drop HEALTHCHECK):
+# python -c "import os, urllib.request as u; u.urlopen('http://127.0.0.1:' + os.environ['WORLDMAP_PORT'] + '/api/worlds', timeout=8)"
+ENTRYPOINT ["python", "serve.py"]
diff --git a/deploy/README.md b/deploy/README.md
new file mode 100644
index 0000000..4ce6a0a
--- /dev/null
+++ b/deploy/README.md
@@ -0,0 +1,101 @@
+# 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/`:
+
+```sh
+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
+
+```sh
+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):
+
+```sh
+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:
+
+```ini
+[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).
diff --git a/deploy/requirements.lock b/deploy/requirements.lock
new file mode 100644
index 0000000..215776c
--- /dev/null
+++ b/deploy/requirements.lock
@@ -0,0 +1,6 @@
+h3==4.5.0
+llvmlite==0.50.0
+numba==0.68.0
+numpy==2.5.3
+pillow==12.3.0
+scipy==1.18.1