Skip to content

Self Hosting

Eric Slutz edited this page Aug 20, 2026 · 4 revisions

Self-Hosting

Self-hosting is for users with real Tandem accounts who want to run their own PumpSync-compatible backend. This page is the end-to-end walkthrough; the backend repository's docs/docker-self-host.md is the repo-local quickstart these steps stay in sync with.

Want to try PumpSync without a Tandem account first? See Demo Mode instead.

1. Run the server

You need Docker, and — to reach the server from a physical iPhone — a reverse proxy (below).

Using the published image (no build needed; it is public on GitHub Container Registry):

mkdir pumpsync && cd pumpsync
curl -fsSLO https://raw.githubusercontent.com/eslutz/PumpSync-Backend/main/.env.example
cp .env.example .env
sed -i.bak "s#^PumpSync__ServiceTokenSigningKey=.*#PumpSync__ServiceTokenSigningKey=$(openssl rand -base64 32)#" .env
rm .env.bak
mkdir -p data
docker run --detach --name pumpsync-backend \
  --publish 127.0.0.1:8080:8080 \
  --env-file .env \
  --volume "$PWD/data:/data" \
  --restart unless-stopped \
  ghcr.io/eslutz/pumpsync-backend-self-hosted:latest

Or build from source with Docker Compose:

git clone https://github.com/eslutz/PumpSync-Backend.git && cd PumpSync-Backend
cp .env.example .env
sed -i.bak "s#^PumpSync__ServiceTokenSigningKey=.*#PumpSync__ServiceTokenSigningKey=$(openssl rand -base64 32)#" .env
rm .env.bak
docker compose up --build -d

.env.example already runs against a real Tandem account by default — the only required change is the signing key, which replaces a well-known placeholder with a real random secret (the backend refuses to start with the placeholder).

2. Verify it

curl http://localhost:8080/api/v1/capabilities
curl http://localhost:8080/health

The capabilities response should include "serviceMode":"selfHosted" and "dataSourceMode":"tandemSource". The backend also logs its active mode in its first startup lines (docker logs pumpsync-backend).

3. Expose it safely (physical iPhone)

The container only speaks plain HTTP and only listens on 127.0.0.1:8080. Self-hosted mode now requires a device-bound Secure Enclave proof, but transport encryption is still required to protect session credentials and Tandem credentials in transit, so the container must never be directly reachable over plain HTTP from other machines. The PumpSync app also requires https:// for any address other than localhost/127.0.0.1/::1, so a phone needs HTTPS.

  • iOS Simulator on the same machine: nothing more to do; use http://localhost:8080/api in the app.
  • Physical iPhone: put a reverse proxy in front of 127.0.0.1:8080 — something that speaks HTTPS to the phone and plain HTTP to the container — then use https://your-host.example/api.

This project doesn't try to be the reverse-proxy documentation — pick a well-maintained, self-hosted option and follow its own guide:

  • Nginx Proxy Manager (recommended if you're new to this) — a web UI over nginx with a built-in Let's Encrypt client, so there's no separate certbot install or renewal cron to set up. For PumpSync: add a Proxy Host, forward it to 127.0.0.1:8080, enable "Request a new SSL certificate." See its setup guide.
  • Caddy — config-file-based, also does automatic HTTPS. See its reverse proxy quickstart.
  • Traefik — Docker-label-driven auto-discovery, a common choice if you're already running several containers under Compose.

All three are actively maintained, self-hosted (nothing routes through a third party), and equally capable of the one thing PumpSync needs.

If your proxy forwards the real client IP via X-Forwarded-For (all three above do by default), set PumpSync__TrustForwardedHeaders=true in .env so per-IP rate limiting uses the phone's real address instead of the proxy's; leave it unset otherwise.

4. Connect the app

  1. Settings → Connection: set the mode to Self-hosted, enter the server URL (including /api), tap Connect. The app enrolls a protocol 3 Secure Enclave P-256 key and creates a renewable session. Self-hosted authentication does not contact or depend on Apple, does not submit an App Attest receipt, and does not store an App Attest risk outcome.
  2. Settings → Tandem Account: enter your real Tandem username, password, and region, then Save Credentials. The backend validates them against Tandem Source and never stores them — they live only in your device's Keychain.
  3. Settings → Apple Health: allow insulin and carbohydrate write access.
  4. Sync tab: pick the initial history range and tap Start Initial Sync. Tandem Source retains roughly 14 days of history, so that is the maximum lookback.

Maintenance

  • Persistence: operational state (never credentials, tokens, or health samples) lives in data/pumpsync.db. Back it up before host or volume changes.
  • Permissions: the container runs as non-root UID 1654; on Linux, ./data must be writable by that UID (chown 1654:1654 data).
  • Upgrades: stop the container, back up data/pumpsync.db, pull the new stable latest image or an explicit numeric SemVer tag (or git pull for source builds), start again, and re-run the capabilities smoke test. latest moves only for stable backend releases; unreleased builds are available by commit-SHA tag.
  • Keys: generate independent random values for PumpSync__ServiceTokenSigningKey and SessionSecurity__MasterKey. Changing the signing key invalidates access tokens. Changing the session-security key invalidates challenges and refresh credentials; the app must reconnect in the foreground.
  • Data deletion: the backend repo's docs/data-deletion.md covers removing an installation's rows from your SQLite database.

Troubleshooting

  • Container exits immediately: the signing key is still the placeholder, or ./data is not writable by UID 1654. Check docker logs.
  • App says the URL is invalid: self-hosted URLs must be https:// unless the host is loopback (localhost, 127.0.0.1, ::1), and should end in /api.
  • Real Tandem credentials rejected: the backend is running in demo mode instead — the startup log and /api/v1/capabilities (dataSourceMode) both show this. Confirm you copied .env.example, not .env.demo.example.
  • "Sync limit reached": the backend rate-limits sync/validate to 12 per hour per user; wait and retry.
  • "Re-save your Tandem account credentials": Tandem rejected the stored password (for example after you changed it on tandemdiabetes.com); re-enter it under Settings → Tandem Account.

Clone this wiki locally