TLS & domains
The production stack in App/docker/ runs Caddy 2 in front of SyncBins. Caddy terminates TLS and reverse-proxies to the Node server on port 8080 inside the Docker network.
SyncBins itself speaks plain HTTP on 8080 — do not expose that port directly to the internet without a proxy.
Single-tenant (one hostname)
Section titled “Single-tenant (one hostname)”Typical self-hosted setup: box.yourdomain.com → your VPS.
- Create an A (and AAAA if you have IPv6) record:
box.yourdomain.com→ server IP. - Open ports 80 and 443 on the firewall (required for ACME HTTP-01 / TLS-ALPN).
Docker env
Section titled “Docker env”In docker/.env:
DOMAIN=box.yourdomain.comBLOB_URL_SECRET=… # openssl rand -hex 32docker-compose.yml passes DOMAIN to Caddy and mounts Caddyfile.
Caddyfile behavior
Section titled “Caddyfile behavior”The bundled App/docker/Caddyfile uses on-demand TLS even for single-host — Caddy requests a cert the first time the hostname is accessed, with authorization from SyncBins:
{ on_demand_tls { ask http://syncbins:8080/api/tls-check interval 2m burst 5 }}
*.{$DOMAIN}, {$DOMAIN} { tls { on_demand } encode zstd gzip reverse_proxy syncbins:8080}For a single-tenant box with one DOMAIN, the wildcard pattern still works — Caddy issues a cert for your exact hostname on first visit.
Verify
Section titled “Verify”curl -s https://box.yourdomain.com/api/health# → {"ok":true,"version":"…"}Browser padlock + green Settings → Server status confirms end-to-end.
Local network / homelab HTTPS
Section titled “Local network / homelab HTTPS”Pick one of the paths below. All assume you use a hostname (not a bare IP) in the browser — e.g. box.lan or syncbins.home — that resolves to your server on the LAN via router DNS, Pi-hole, or /etc/hosts.
mkcert creates a local CA and installs it on the machine running Docker. You trust that root once on each phone/laptop.
- Install mkcert on the host.
- From
App/docker/:Terminal window mkcert -installmkcert box.lanmv box.lan.pem box.lan-key.pem certs/ - Set in
.env:DOMAIN=box.lanandBLOB_URL_SECRET=… - Start production stack with local TLS:
Terminal window docker compose -f docker-compose.yml -f docker-compose.local-tls.yml up -d - Open https://box.lan (port 443).
Repo files: docker/Caddyfile.local, docker/docker-compose.local-tls.yml, docker/certs/README.md.
Trust on iOS: copy mkcert’s rootCA.pem (mkcert -CAROOT) to the phone → install profile → Settings → General → About → Certificate Trust Settings → enable full trust for the mkcert root.
Same mkcert certs; HTTPS on the Vite dev server for phone testing while hacking:
cd App/dockermkcert box.lanmv box.lan.pem box.lan-key.pem certs/DOMAIN=box.lan docker compose -f docker-compose.dev.yml -f docker-compose.dev-tls.yml upOpen https://box.lan:5173. Vite reads VITE_HTTPS_CERT / VITE_HTTPS_KEY (set in docker-compose.dev-tls.yml).
Native dev without Docker:
VITE_HTTPS_CERT=../docker/certs/box.lan.pem \VITE_HTTPS_KEY=../docker/certs/box.lan-key.pem \npm run dev:webIf you already own box.yourdomain.com and can point it at your LAN IP from inside your network (split-horizon DNS or Pi-hole), the normal production Caddy + Let’s Encrypt path works — no mkcert.
Requirements:
- Public DNS or DNS-01 ACME (bundled Caddyfile uses HTTP-01 on port 80).
- Port 80 reachable from the internet or use Caddy’s DNS challenge for your provider (not in the default compose — advanced).
Best when your box is reachable on the public internet anyway; overkill for pure offline LAN.
Caddy can mint a private CA with tls internal in the Caddyfile. No mkcert install, but every device must manually trust Caddy’s root — tedious on iOS, fine for one laptop.
Not shipped in the default compose files; add to a custom Caddyfile if you want a five-minute desktop-only test. Prefer mkcert for anything involving phones.
Checklist
Section titled “Checklist”| Step | Done when |
|---|---|
| Hostname resolves on phone | ping box.lan or open in browser without wrong host |
| Padlock shows HTTPS | No “Not Secure” warning (after trusting CA) |
| API healthy | curl -sk https://box.lan/api/health → {"ok":true,…} |
| Send works on iOS | Composer send creates a row (secure context + earlier iOS tap fix) |
Multi-tenant (wildcard subdomains)
Section titled “Multi-tenant (wildcard subdomains)”Hosted-style: *.syncbins.com plus apex syncbins.com.
- DNS: Wildcard
*.yourservice.comand apexyourservice.com→ same IP. - Env:
TENANT_MODE=multi,BASE_DOMAIN=yourservice.com, plus storage and optional Stripe vars — see Multi-tenant mode. - TLS check: Caddy calls
GET /api/tls-check?domain=tenant.yourservice.combefore issuing. SyncBins returns 200 only if that subdomain exists in thetenantstable — prevents random LE cert minting. - Signup: Tenants are created at apex via
POST /api/signup; users finish onboarding onhttps://slug.yourservice.com.
Plain HTTP (localhost only)
Section titled “Plain HTTP (localhost only)”npm run dev:compose without the dev-tls overlay serves http://localhost:5173. Fine for desktop-only hacking on the same machine. Do not use bare LAN IPs over HTTP for phone testing — use homelab HTTPS instead.
Common issues
Section titled “Common issues”| Symptom | Likely cause | Fix |
|---|---|---|
| Send/crypto fails on phone, HTTP URL | Not a secure context | Use HTTPS + trusted cert (homelab section) |
| Certificate never provisions | Port 80 blocked | Open 80/443; LE needs reachability |
tls-check 404 | Subdomain not in DB (multi-tenant) | Sign up tenant first |
| Browser shows HTTP content | Hitting :8080 directly | Use Caddy on 443 |
| Wrong site / 404 tenant | DNS points to box but wrong slug | Check BASE_DOMAIN and Host header |
| mkcert warning on phone | Root CA not trusted | Install + enable trust for mkcert root (iOS Certificate Trust Settings) |
More: Troubleshooting.