Artifact 5ff56ae9e9199c285632b1509c861157e679bcfe5254859e40ae2a67c8cf9911:
- File docs/site/content/docs/deployment.md — part of check-in [28f11dc3c4] at 2026-04-27 19:51:26 on branch trunk — pre-public: LICENSE, untrack .mcp.json, placeholder IPs, module READMEs, fix wasm targets (user: [email protected], size: 3371) [annotate] [blame] [check-ins using] [more...]
title: Deployment
weight: 30
A working production recipe for EdgeSync, derived from a reference deployment at sync.example.com. The shape: one VPS running NATS + a leaf agent, exposed publicly via Cloudflare Tunnel and privately via Tailscale.
Topology
public internet
│
▼ HTTPS
Cloudflare Tunnel ──► VPS:9000 (leaf HTTP /xfer)
│
▼
leaf agent ◄────► NATS:4222 (Tailscale-only)
│
▼
.fossil repo (volume mount)
tailnet ──────────────► VPS:9000 (HTTP)
└───► VPS:4222 (NATS)
Files
The reference deployment lives in deploy/:
deploy/
├── Dockerfile # multi-stage Go build, GOWORK=off
├── docker-compose.yml # NATS on Tailscale IP, leaf on :9000
└── data/ # host volume mount with .fossil files
Dockerfile
The leaf binary is built standalone (libfossil pulled from the public Go module proxy) so no private-module auth is needed. The build uses GOWORK=off because Docker copies only leaf/, not the whole workspace.
docker-compose.yml
Two containers:
nats— officialnats:latestimage, bound to the host's Tailscale interface only (100.64.0.1:4222)leaf— built fromDockerfile, exposed on:9000
Ports 8080 and 8090 are intentionally avoided because they are occupied by Coolify and Caddy on the reference VPS. Adjust as needed.
Cloudflare Tunnel
# ~/.cloudflared/config.yml on the VPS
tunnel: <tunnel-id>
credentials-file: /home/user/.cloudflared/<tunnel-id>.json
ingress:
- hostname: sync.example.com
service: http://localhost:9000
- service: http_status:404
Run as a systemd service: cloudflared-fossil.
Update flow
# On the VPS
cd ~/EdgeSync && git pull
cd deploy && sudo docker compose up -d --build
This rebuilds the leaf container with the latest code and restarts both services. NATS does not need to restart for a leaf upgrade.
Endpoints
| Endpoint | URL | Access |
|---|---|---|
| Public HTTPS | https://sync.example.com |
Cloudflare Tunnel (anyone with the URL) |
| Tailscale HTTP | http://100.64.0.1:9000 |
Tailnet only |
| Tailscale NATS | nats://100.64.0.1:4222 |
Tailnet only |
Mobile clients
For Expo / React Native clients (e.g. edgesync-notify-app), you'll typically:
- Run a NATS leafnode on the VPS bound to port
7422(separate from the regular4222so leafnode auth can be scoped) - Open the leafnode port to your tailnet (
ufw route allow ...) - In the app, configure the NATS leafnode URL and pair via QR (see Notify)
Observability
Set OTEL_EXPORTER_OTLP_ENDPOINT and (if exporting to Honeycomb) the API key. EdgeSync uses Doppler to manage OTel secrets without committing .env files:
doppler run -- docker compose up -d --build
When the env var is unset the agent runs with a no-op observer — telemetry is zero-cost when not configured.
The reference Honeycomb dashboard ("EdgeSync Sim — Operational Overview") tracks sync latency, convergence, file throughput, and errors across the dataset edgesync-sim in the test environment.