All docs

Getting started

Set up your server

Source: docs/setup.md on GitHub · edit this page · Plain Markdown

Run a database on your own computer, NAS or cloud server, pair your iPhone, and check that your Apple Health data arrives. The stack includes the database, web dashboard, Grafana, a read-only API and an AI connection through MCP.

You can follow this guide yourself or give it to a coding assistant with terminal access to your server. A chat assistant without that access can explain the steps, but cannot run or verify them for you. The backend is pre-release; you are responsible for keeping the host running, access control and backups.

Ask AI to help#

Copy this prompt into your coding assistant:

Help me set up PulsHealth on my server. Read https://pulshealth.com/llms.txt and https://pulshealth.com/docs/setup/ first. Check my machine, network and existing installation, then follow the guide. Preserve existing data and keep secrets out of chat. Help me pair my iPhone and verify that synced data arrived.

The assistant should establish the host OS, whether Docker works, your reporting time zone, and how the phone will reach the host. If PulsHealth already exists, inspect that installation and follow upgrading instead of creating another stack. Compose uses the project name pulshealth; a second checkout on the same Docker host is not an isolated installation.

1. Prepare the host#

You need:

  • A Linux or macOS host that stays on, with persistent disk space for your data. A NAS must support the Docker Compose stack and a shell for these commands.
  • Git, Bash, Docker with Compose v2, openssl and curl. Optional make provides shortcuts used in the operations guide.
  • The PulsHealth app on your iPhone or iPad, with Apple Health data available to read.
  • Free host ports: 8080 (phone uploads), 8081 (read API), 8082 (MCP), 3000 (Grafana), 3001 (dashboard) and 5432 (Postgres).

Check Docker before continuing:

docker compose version
docker info

Choose your usual reporting time zone, such as Europe/Berlin. This is an IANA zone name, not an abbreviation such as CET. It supplies the default calendar for server reports; phone-recorded daily aggregates retain their recorded dates.

2. Start the stack#

For a new installation, run these commands on the server:

git clone https://github.com/PulsHealth/pulshealth.git
cd pulshealth
git rev-parse HEAD
scripts/bootstrap.sh --build --time-zone Europe/Berlin

Replace Europe/Berlin with your zone. This builds the four application images from the checkout, keeping the code and database migrations together. The first build downloads dependencies and can take several minutes. No local Go, Node.js or Xcode installation is needed.

Keep the printed Git revision with your setup notes. The script generates server/.env, starts the services, applies the schema, and checks ingest and its authentication. It prints credentials locally: keep that output and the pairing QR code private. An assistant should not repeat passwords, tokens or pairing links in chat.

Initially, every port listens only on the server's loopback address. A pairing URL of (none yet) is expected until the next step. The web dashboard already works at http://localhost:3001 in a browser on that host.

Using published images instead: select a published backend release and check out its exact vX.Y.Z tag before the first bootstrap, then omit --build. Use that release's README and confirm all four image tags are available. Bootstrap pins PULS_VERSION=X.Y.Z only when creating a fresh .env at a release tag. Do not combine main migrations with floating latest images. See images and versions.

3. Give the phone a reachable address#

Choose one route. localhost on the phone means the phone itself, not your server. Setting --url records a pairing address; it does not create DNS, certificates, a VPN, a firewall rule or a reverse proxy.

Private HTTPS or an existing HTTPS proxy#

For regular use, put an HTTPS proxy in front of http://127.0.0.1:8080. A private VPN with HTTPS, such as Tailscale Serve, keeps the endpoint accessible only to your devices. A public HTTPS proxy also works; it must preserve the Authorization header and accept the app's uploads without a browser login.

Once the HTTPS route works, record its base URL (no /v1/batches suffix):

scripts/bootstrap.sh --build --url https://health.example.com

Replace the example with your real URL. Leave INGEST_BIND_ADDR=127.0.0.1 for a proxy running on the host. A containerized proxy needs a route to the Compose ingest:8080 service instead of its own localhost. Exposing the server has examples. Private VPN URLs require the phone to be connected to that VPN; they are not reachable by a cloud AI connector unless you separately configure access.

Same trusted home network#

For a local trial without HTTPS:

scripts/bootstrap.sh --build --lan

Connect the phone to the same network and use the printed http://<LAN IP>:8080 address. This sends health data and credentials without TLS and binds ingest to all host interfaces. Use it only on a trusted private network, allow access only from that network, and do not forward port 8080 from your router. Do not use this option on a publicly reachable cloud host.

If the detected address is wrong, use the host's actual private LAN address. If moving from LAN to HTTPS later, set INGEST_BIND_ADDR=127.0.0.1 in server/.env before running bootstrap with --url; --url alone does not undo --lan. An existing PULS_PUBLIC_URL takes precedence over LAN detection.

4. Pair the app and choose what to sync#

  1. Open PulsHealth, complete the first-run Health permission step, then open Sync → Connect Your Server (or Sync → Database if already configured).
  2. Use Scan Pairing Code or Paste Pairing Code with the script's output. Alternatively enter the database URL and token; keep the user ID the server supplied. Pairing fills the fields and tests the connection. Use Test Connection again after editing them.
  3. Tap Add Database, then review the types and history on the Sync screen. Raw Samples → Edit selects individual readings; Aggregates → Edit selects summaries. Include daily aggregates for phone-computed totals and averages. Server daily results can also use raw-data fallbacks; the database guide explains the differences. Match Raw Samples adds daily defaults for eligible raw types.
  4. Tap Apply, review the initial-sync guidance, then Apply & Start Sync. For an existing saved destination, the Database screen uses Save & Apply. Earlier app versions may also use that label during first setup.

Keep the phone unlocked and PulsHealth open for the first sync, preferably plugged in for a large history. Apple Health permissions and your app plan limit what can be read; a connection does not grant access to every type or all history. Background sync resumes as iOS allows, rather than on a guaranteed schedule.

To display the shared pairing code again, run this locally on the host:

scripts/bootstrap.sh --print-pairing

For separate, revocable phone credentials, use --issue-device. Multiple people also need separate user IDs and appropriate viewer access; the default single-password viewer can show every user on the server.

5. Verify the whole path#

From the repository root on the server:

docker compose -f server/docker-compose.yml ps --all
docker compose -f server/docker-compose.yml logs --tail=50 migrate
curl -fsS http://127.0.0.1:8080/healthz
curl -fsS http://127.0.0.1:8081/healthz
curl -fsS http://127.0.0.1:3001/api/healthz

The one-shot migrate service should exit successfully; the long-running services should be healthy where they have health checks. Both Go probes should report "ok":true and "db":true. These checks prove server readiness, not a successful phone sync.

Finish by checking all three:

  • Phone: Test Connection succeeds over its actual network connection.
  • Upload: the Sync screen shows a completed upload without an error. Use Sync Now if needed while the phone is unlocked.
  • Read-back: the dashboard shows a recent measurement or workout you chose to sync, for the correct user and date. Confirm a daily chart too if you enabled daily aggregates. An empty database or demo dashboard is not proof.

Open http://localhost:3001 on the host and use any username with the generated WEB_AUTH_PASSWORD printed by bootstrap. For a remote server, run an SSH tunnel on your computer, replacing user@server with your SSH destination:

ssh -N -L 3001:127.0.0.1:3001 user@server

Then open http://localhost:3001 on your computer. Port 3001 must be free there. For permanent browser access, configure a separate HTTPS route to the viewer; the phone's ingest URL is not the dashboard. See web viewer access.

6. Keep it running#

Backups are off by default. Set PULS_BACKUP_DIR in server/.env to protected storage outside the database's disk, then enable the scheduler:

docker compose -f server/docker-compose.yml --profile backup up -d backup

Backup and restore explains immediate dumps, retention and restoring a copy on a scratch stack. A successful dump is not a tested restore. Keep server/.env separately in secure storage; database dumps do not replace the host configuration.

For source-built installs, use scripts/bootstrap.sh --build or make dev-up when starting updated application code. A plain docker compose up -d or make up selects published images instead. Take a backup and read upgrading before moving revisions. Do not delete .env, reset the database or run docker compose down -v to fix a connection problem.

Once phone sync and read-back work, connect an AI assistant if wanted. That is a separate step: the setup assistant does not need health-data access just to install the server.

If something fails#

SymptomCheck next
Docker command failsStart Docker and confirm this shell can run docker info and Compose v2.
A port is already allocatedIdentify the existing service or PulsHealth installation before changing ports or starting another checkout.
Build, startup or migration failsRead the failing service's logs. Preserve .env and volumes; do not reset or baseline an unknown schema.
No pairing URL or QR codeComplete step 3. Missing QR rendering still permits manual entry or the printed pairing payload.
Phone cannot connectCheck Wi-Fi/VPN, hostname, certificate, firewall and proxy. Do not use the server's localhost on the phone.
HTTP 401 or 403Use the ingest token, not PULS_API_TOKEN or PULS_MCP_TOKEN; for a device token, the paired user ID must match.
Connection works but no data arrivesApply the setup, select a type with recorded data, grant Health access, unlock the phone and tap Sync Now. Check plan/history limits and the Sync activity log.
Raw readings arrive but daily results are emptyCheck the user and dates. Enable a daily total or average for that type and Apply; API daily results need supported aggregate-series metadata even when falling back to raw data.
Browser cannot open port 3001 remotelyIt binds to loopback by default. Use the SSH tunnel or configure viewer HTTPS; --lan only exposes ingest.

For server diagnostics, run docker compose -f server/docker-compose.yml logs --tail=100 ingest api web. Review output locally and redact personal data before sharing it. An AI assistant should finish with the installed revision, reachable URLs, checks actually passed, backup status, and any phone steps still awaiting you.