- HTML 42.1%
- Python 30.2%
- Shell 26.9%
- Dockerfile 0.8%
|
All checks were successful
ci/woodpecker/push/deploy Pipeline was successful
|
||
|---|---|---|
| .woodpecker | ||
| tiler | ||
| www | ||
| .gitignore | ||
| bootstrap.sh | ||
| deploy.sh | ||
| README.md | ||
precip.be
Commercial website for precip.be — real-time rainfall images and datasets delivered to customer SFTP servers, built on naaulu.
Contents
www/index.html— landing page: products, pricing, how it workswww/terms.html— Terms of Servicewww/live/index.html— live viewer (OpenStreetMap map + rainfall overlay)bootstrap.sh— executable documentation: provisions a fresh box end to enddeploy.sh— rsyncwww/to the target box over SSH (key-based)tiler/— NetCDF → XYZ rain tiles + manifest (runs on the box on a timer).woodpecker/deploy.yml— runsdeploy.shon push tomain
Architecture
Two sites, two hosts — deliberately split so the sales pages survive any app-box incident:
| URL | Host | Content | Deploy |
|---|---|---|---|
precip.be (apex) |
OVH web hosting (with naaulu.org, egouden.com) | landing + terms | deploy.sh phase 1 (sftp, password) |
live.precip.be |
the app box — pylone [[sites]] |
viewer, rain tiles, OSM proxy | deploy.sh phase 2 (rsync, key) |
pipeline box app box browser
naaulu-live ──► /srv/naaulu/inbox │
every 5 min precip-tiler.timer (every minute) │
nc ──► XYZ tiles + manifest+prune │
/var/www/precip.be/live ───────────────┤ https://live.precip.be
overv/openstreetmap /tile/* proxy ──► 127.0.0.1:8080 ──────┤ viewer + rain + OSM
OVH hosting ◄── sftp (deploy.sh phase 1) ────────────────────┤ https://precip.be (landing)
Rules: the pipeline owns live/tiles/ and manifest.json — the website deploy
never deletes them (no --delete in phase 2); the raw .nc inbox lives OUTSIDE
the web root, because it is the paid dataset product and is never served —
and the tiler prunes it itself (the sender no longer deletes anything).
## Bootstrap a fresh box
`bootstrap.sh` is the executable, self-documenting form of this whole
section — every step below is a commented phase in it (idempotent: safe to
re-run, converges to "all ok"). Prerequisites: a Debian box declared in
`~/.pylone/pylone.toml`, a DNS `A` record *before* deploying (bootstrap's
final health check curls it), and `pylone deploy` already run.
Box sizing: **≥ 4 GB RAM, ≥ 40 GB disk** — the OSM import alone takes
~23 GB, render bursts need the 2 G swapfile, and the tiler renders
CPU-bound every minute.
The minimum `~/.pylone/pylone.toml` a new server needs:
```toml
[[boxes]]
name = 'chronos'
address = '194.164.206.255'
user = 'debian'
port = 61017 # ssh port — pylone boxes are never 22
[[sites]]
domain = 'live.precip.be'
box = 'chronos'
root = '/var/www/precip.be/live'
mail = 'hello@precip.be'
# Caddy path to the OSM container, which bootstrap publishes on loopback
# only (127.0.0.1:8080) — keep this stanza and bootstrap step 3 in sync.
[[sites.proxies]]
path = '/tile/*'
to = '127.0.0.1:8080'
Then pylone deploy from the operator's machine (Caddy/DNS/firewall —
never this repo's job), and on the box:
git clone <this repo> && ./bootstrap.sh # or --skip-import / --skip-seed
What it does, in order:
- verifies pylone's layer (Caddyfile present, caddy active, detects the domain)
- directories:
/srv/naaulu/inbox(feed — outside the web root),/var/lib/precip-tiler(lock), site root - 2 G swapfile (fstab-persisted) — render bursts have wedged a 4 GB box without it
- OSM tile-server quadlet:
ShmSize=512m(z12+ PostGIS exceeds podman's 64 MB default), loopback-only publish (public path = Caddy's/tile/*), no auto-update (never surprise a 23 GB data volume) - OSM import (~25 min, ~23 GB; guarded by an in-volume sentinel, never repeated)
- tiler image build from
tiler/(local, no registry) - tiler quadlet + native systemd timer every minute (
/etc/systemd/system— quadlet dirs don't generate timers) - feed public key (via prompt or
FEED_PUB=...; the private half lives on the pipeline box and is mounted there into the container) + prints the ready-to-paste cron snippet with this box's real IP/ssh-port - OSM cache pre-seed (
render_list, z5–13 over the data footprint — every visitor request becomes a cache hit) - first tiler run + health checks
Data feed (pipeline box → this box)
Target spec user:host:password:path[:port] — empty password = SSH key
auth (boxes refuse passwords), :port required (defaults to 22; most
pylone boxes are non-standard). Cleanup is one-sided and lives entirely on
this box: the sender pushes and never deletes, so the tiler keeps both sides
of the last 36 files per duration window — it deletes inbox .nc and
rendered tile dirs past the same cutoff (TILER_RETENTION_FILES=36 → 3 h of
5-min, 36 h of hourly, 36 d of daily), so a source and its tiles age out on
the same tick. That is why the tiler's inbox mount is read-write (see the
quadlet in bootstrap.sh phase 6) instead of :ro.
A new box stays empty until something pushes to it: the feed comes from naaulu-live on the pipeline box (separate repo, see naaulu); bootstrap phase 7 prints that box's ready-to-paste cron snippet with this server's real IP and ssh port.
Health checks
- inbox newest mtime < 6 min → feed alive ·
list-timers precip-tiler.timer→ tick ≤1 min - inbox size bounded: ≤ ~36
.ncper duration (the tiler prunes the feed itself) — steady growth = prune not running manifest.jsontimestamps advancing ·/tile/8/131/85.png→ 200 ·https://<domain>/→ 200- tripwires: available memory < 300 MB sustained, swap near full, disk > 80 %
After changing tiler/
The image is built once — bootstrap.sh deliberately skips the build
when localhost/precip-tiler:latest exists, so a code change in tiler/
never reaches the box on its own. Rebuild and retrigger by hand (from the
checkout on the box):
sudo podman build -t precip-tiler:latest tiler
sudo systemctl start precip-tiler.service # don't wait for the next tick
Symptom of a stale image: the inbox keeps receiving a duration the site
never shows (e.g. p1d files arriving while manifest.json has no p1d).
A change to the units (mount flags, env, timer) is a different story:
re-run ./bootstrap.sh — it rewrites only what differs, then
daemon-reloads. The tiler must see the inbox read-write to prune the .nc
feed itself (the sender deletes nothing anymore); a stale :ro quadlet
leaves the inbox growing while the journal says !! ... NOT deleted.
Hosting
- Landing + terms — OVH panel-mapped web root (same hosting family as naaulu.org); needs precip.be added with SSL in the OVH panel (done).
- Viewer — pylone
[[sites]] domain = 'live.precip.be'on the app box,root = '/var/www/precip.be/live': page at/, rain at/tiles/*, OSM behind the/tile/*proxy.
| Path | Owner |
|---|---|
| landing + terms (OVH account web root) | this repo, deploy.sh phase 1 |
live/ tree — viewer + leaflet vendor |
this repo, deploy.sh phase 2 |
live/tiles/, manifest.json |
naaulu-live pipeline (server-side state — CI never deletes) |
Deploy
Two independent phases in one script — each runs only when its credentials are present, so either half can ship alone:
# landing/terms -> OVH (phase 1)
WEB_HOST=... WEB_USER=... WEB_PASS=... ./deploy.sh
# viewer -> app box (phase 2)
DEPLOY_HOST=... DEPLOY_PORT=... DEPLOY_USER=... DEPLOY_KEY=... ./deploy.sh
CI: push to main → Woodpecker runs both phases. Secrets (names only —
values live in the forge's secret store): WEB_HOST, WEB_USER, WEB_PASS
for phase 1; DEPLOY_HOST, DEPLOY_PORT, DEPLOY_USER, DEPLOY_KEY
for phase 2.
One-time phase-2 key setup — a dedicated deploy key, never your own:
ssh-keygen -t ed25519 -C precip-be-deploy -f precip_deploy
# public half -> append to DEPLOY_USER's ~/.ssh/authorized_keys on the app box
# private half -> base64 it as ONE line for the DEPLOY_KEY secret:
base64 -w0 precip_deploy
CI decodes DEPLOY_KEY from base64 before use — a single-line secret
survives any secret-UI copy/paste with no newline mangling, and an empty
or non-base64 value fails the build instead of deploying garbage.