This is the reverse proxy variant of Run a read-only demo. The demo binds loopback only and Caddy on the same host is the one public listener. The files live in deploy/hosted-demo/. Replace every example.com value before use.
Layout
- The
levelrail demo serve process runs as the unprivileged levelrail-demo user under systemd and listens on 127.0.0.1:8080.
- Caddy terminates HTTPS for
demo.example.com and proxies to that loopback port.
- The bind guard is unchanged: the demo refuses to start on a non-loopback
APP_HTTP_ADDR.
Set it up
- Create the user and directories: a
levelrail-demo system user with no shell, the binary under /opt/levelrail-demo/bin, state under /var/lib/levelrail-demo.
- Copy
env.example to /etc/levelrail-demo/env (mode 0640, group levelrail-demo). Keep it free of secrets.
- Copy
reseed.sh to /opt/levelrail-demo/reseed.sh and make it executable.
- Run the first seed once:
APP_DATA_DIR=/var/lib/levelrail-demo/data levelrail demo seed --seed 1, as the levelrail-demo user.
- Install
levelrail-demo.service, levelrail-demo-reseed.service and levelrail-demo-reseed.timer into /etc/systemd/system, then systemctl enable --now levelrail-demo.service levelrail-demo-reseed.timer.
- Install the
Caddyfile as /etc/caddy/Caddyfile and reload Caddy.
Reset on a schedule
- The timer runs
reseed.sh, which seeds a fresh directory, stops the service, swaps the directory in and starts the service again.
- Set
APP_DEMO_RESET_INTERVAL (for example 30m) in the env file to the same cadence as the timer. The dashboard banner then shows the next reset time. Unset, it shows the daily 04:00 UTC default.
- The value is a Go duration of at least one minute and the time is aligned to UTC multiples of it.
Harden the host
- Rate limits per client IP: the Caddyfile uses the
caddy-ratelimit module, which needs a custom Caddy build. Without it, limit at your CDN or firewall. The demo also limits session creation per IP on its own.
- Request size limits: Caddy caps bodies at 16KB and the demo caps them at 4KB.
- Security headers: the Caddyfile sets HSTS,
nosniff, frame denial, a no-referrer policy and a permissions policy. The demo adds X-Robots-Tag: noindex, nofollow itself.
- No Docker access: the service user is not in the
docker group and the unit hides the Docker socket paths. The demo refuses to start if DOCKER_HOST is set.
- Outbound network: the unit sets
IPAddressDeny=any with loopback allowed, so the process cannot call out. Keep the host firewall default-deny for outbound traffic from that user as a second layer.
- Sandboxing: the unit drops all capabilities, mounts the system read-only and caps memory, CPU and tasks.
Logging policy
- Caddy access logs drop the client address, cookies and authorization headers, keep at most two files of 10MiB and delete them after 72 hours.
- The demo process logs to the journal. Set
SystemMaxUse and MaxRetentionSec=3day in journald.conf so the journal follows the same window.
- Do not add analytics or request capture in front of the demo. The demo only keeps aggregate counters that carry no personal data.
Check it
curl -sI https://demo.example.com/ returns the security headers.
curl -s https://demo.example.com/api/v1/demo reports "enabled": true and the next reset time.
- A write such as
curl -s -X POST https://demo.example.com/api/v1/apps returns 403 with the code demo_readonly.
- To take the demo down:
systemctl stop levelrail-demo.service levelrail-demo-reseed.timer.
Last modified on October 24, 2026