Skip to content

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.

Typical self-hosted setup: box.yourdomain.com → your VPS.

  1. Create an A (and AAAA if you have IPv6) record: box.yourdomain.com → server IP.
  2. Open ports 80 and 443 on the firewall (required for ACME HTTP-01 / TLS-ALPN).

In docker/.env:

DOMAIN=box.yourdomain.com
BLOB_URL_SECRET=… # openssl rand -hex 32

docker-compose.yml passes DOMAIN to Caddy and mounts Caddyfile.

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.

Terminal window
curl -s https://box.yourdomain.com/api/health
# → {"ok":true,"version":"…"}

Browser padlock + green Settings → Server status confirms end-to-end.

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.

  1. Install mkcert on the host.
  2. From App/docker/:
    Terminal window
    mkcert -install
    mkcert box.lan
    mv box.lan.pem box.lan-key.pem certs/
  3. Set in .env: DOMAIN=box.lan and BLOB_URL_SECRET=…
  4. Start production stack with local TLS:
    Terminal window
    docker compose -f docker-compose.yml -f docker-compose.local-tls.yml up -d
  5. 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.

StepDone when
Hostname resolves on phoneping box.lan or open in browser without wrong host
Padlock shows HTTPSNo “Not Secure” warning (after trusting CA)
API healthycurl -sk https://box.lan/api/health{"ok":true,…}
Send works on iOSComposer send creates a row (secure context + earlier iOS tap fix)

Hosted-style: *.syncbins.com plus apex syncbins.com.

  1. DNS: Wildcard *.yourservice.com and apex yourservice.com → same IP.
  2. Env: TENANT_MODE=multi, BASE_DOMAIN=yourservice.com, plus storage and optional Stripe vars — see Multi-tenant mode.
  3. TLS check: Caddy calls GET /api/tls-check?domain=tenant.yourservice.com before issuing. SyncBins returns 200 only if that subdomain exists in the tenants table — prevents random LE cert minting.
  4. Signup: Tenants are created at apex via POST /api/signup; users finish onboarding on https://slug.yourservice.com.

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.

SymptomLikely causeFix
Send/crypto fails on phone, HTTP URLNot a secure contextUse HTTPS + trusted cert (homelab section)
Certificate never provisionsPort 80 blockedOpen 80/443; LE needs reachability
tls-check 404Subdomain not in DB (multi-tenant)Sign up tenant first
Browser shows HTTP contentHitting :8080 directlyUse Caddy on 443
Wrong site / 404 tenantDNS points to box but wrong slugCheck BASE_DOMAIN and Host header
mkcert warning on phoneRoot CA not trustedInstall + enable trust for mkcert root (iOS Certificate Trust Settings)

More: Troubleshooting.