No description
  • HTML 42.1%
  • Python 30.2%
  • Shell 26.9%
  • Dockerfile 0.8%
Find a file
Edouard Goudenhoofdt 0099616638
All checks were successful
ci/woodpecker/push/deploy Pipeline was successful
precip.be website
2026-11-04 14:09:58 +01:00
.woodpecker precip.be website 2026-11-04 14:09:58 +01:00
tiler precip.be website 2026-11-04 14:09:58 +01:00
www precip.be website 2026-11-04 14:09:58 +01:00
.gitignore precip.be website 2026-11-04 14:09:58 +01:00
bootstrap.sh precip.be website 2026-11-04 14:09:58 +01:00
deploy.sh precip.be website 2026-11-04 14:09:58 +01:00
README.md precip.be website 2026-11-04 14:09:58 +01:00

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 works
  • www/terms.html — Terms of Service
  • www/live/index.html — live viewer (OpenStreetMap map + rainfall overlay)
  • bootstrap.sh — executable documentation: provisions a fresh box end to end
  • deploy.sh — rsync www/ to the target box over SSH (key-based)
  • tiler/ — NetCDF → XYZ rain tiles + manifest (runs on the box on a timer)
  • .woodpecker/deploy.yml — runs deploy.sh on push to main

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:

  1. verifies pylone's layer (Caddyfile present, caddy active, detects the domain)
  2. directories: /srv/naaulu/inbox (feed — outside the web root), /var/lib/precip-tiler (lock), site root
  3. 2 G swapfile (fstab-persisted) — render bursts have wedged a 4 GB box without it
  4. 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)
  5. OSM import (~25 min, ~23 GB; guarded by an in-volume sentinel, never repeated)
  6. tiler image build from tiler/ (local, no registry)
  7. tiler quadlet + native systemd timer every minute (/etc/systemd/system — quadlet dirs don't generate timers)
  8. 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
  9. OSM cache pre-seed (render_list, z5–13 over the data footprint — every visitor request becomes a cache hit)
  10. 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 .nc per duration (the tiler prunes the feed itself) — steady growth = prune not running
  • manifest.json timestamps 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.