# Installation
Source: https://docs.privacytracker.privacykey.org/installation

Detailed install paths for self-hosters — signed desktop app, Homebrew cask, or Docker.

privacytracker ships three install paths from one Next.js bundle. Pick whichever matches how you usually run things.

> **Note**
>
> If you want to run privacytracker from a source checkout (for development or platforms we don't ship binaries for), see [Build from source](https://docs.privacytracker.privacykey.org/develop/build-from-source) under the Develop tab.

**Desktop app (macOS)**

Signed and notarized for both Apple Silicon (`aarch64`) and Intel (`x86_64`). Releases after v0.1.2, starting with v0.3.0, need macOS 13.5 or later; v0.1.2 runs on macOS 11 and later.

**Step 1: Download the latest .dmg**

From [github.com/privacykey/privacytracker/releases/latest](https://github.com/privacykey/privacytracker/releases/latest).

**Step 2: Open and drag to /Applications**

Gatekeeper opens it without the "unidentified developer" warning because the build is signed with a Developer ID certificate and notarized by Apple.

**Step 3: Launch privacytracker**

The SQLite database is created at `~/Library/Application Support/privacytracker/` and survives app updates.

Auto-updates use [Tauri's updater](https://tauri.app/) with an ed25519 signature check on every patch.

> **Note**
>
> The desktop app is moving off its bundled Node process onto a Rust backend built into the app. The first release on it hasn't been published as of September 2026; v0.1.2, the latest, runs on Node. From that release on:
>
> - The app serves itself, with no separate Node process, and nothing is unpacked into the data folder: the frontend ships read-only inside the signed app. Earlier releases extract a ~200 MB `standalone/` folder there; once you've moved to the Rust backend nothing uses it, and you can delete it.
> - The data folder stays where it is, and either build opens the database the other left behind, so the switch never stands in the way of [going back](https://docs.privacytracker.privacykey.org/upgrading#rolling-back) to a release on Node.
> - The app reuses the local port it had last time, so what a page keeps in the browser, such as the accessibility quick toggles, survives a relaunch unless something else has taken the port. Earlier releases lose it at every launch.
> - The licence notices ship inside the app, in `Contents/Resources/third-party/`: `NOTICE`, `LICENSE`, `V8-LICENSE` and `THIRD-PARTY-RUST.md`, which lists every Rust crate compiled in. The app's **Legal** page names the crates chosen directly and links the full list.
> - macOS 13.5 stays the minimum.
>
> [Tauri & the backend](https://docs.privacytracker.privacykey.org/develop/tauri) has the details.

**Homebrew (macOS)**

```bash
brew tap privacykey/tap
brew install --cask privacytracker
```

Installs the same signed `.app` bundle into `/Applications`.

```bash
brew upgrade --cask privacytracker
brew uninstall --cask privacytracker
```

**Docker (Linux / macOS)**

Production-ready, with the SQLite database in a Docker-managed volume so data survives container rebuilds.

```bash
git clone https://github.com/privacykey/privacytracker.git
cd privacytracker
echo "AUDITOR_ADMIN_TOKEN=$(openssl rand -hex 32)" >> .env
chmod 600 .env
docker compose up --build -d
docker compose logs -f
```

Compose won't start without `AUDITOR_ADMIN_TOKEN`. Docker always requires the token, even with the port published on `127.0.0.1` only, because other containers can still reach the app. Open [http://localhost:3000](http://localhost:3000) and sign in with it; [Hardening → Set the admin token](https://docs.privacytracker.privacykey.org/hardening#set-the-admin-token) covers rotating it and using it from scripts.

The database lives in the `privacytracker-data` volume, at `/app/data/privacy.db` inside the container. Copy it out with `docker compose cp web:/app/data ./privacytracker-data-backup`. To keep it on the host at `./data/privacy.db` instead, create the directory for the container's user once, then add the bind-mount file:

```bash
mkdir -p data && sudo chown 100:101 data
docker compose -f docker-compose.yml -f docker-compose.bind-mount.yml up --build -d
```

Container healthchecks hit `GET /api/ready` (DB reachable + data dir writable).

> **Note**
>
> Since 21 September 2026 the image runs the Rust server, `pt-core serve`, in place of Next.js's `next start`: about 56 MB on Alpine, with no Node inside. Images built from `main`, and the published `latest` tag, have it; release images have it from the first release after v0.1.2. The Compose files, the volume, the port, the environment and the healthcheck are unchanged, and either image opens the database the other wrote. To go back to the Node image, which stays buildable until v0.3.0 has shipped, add `PRIVACYTRACKER_BACKEND=node` to `.env` and run `docker compose up --build -d`.

> **Note**
>
> Windows and Linux desktop builds aren't published yet. For those platforms, use Docker, or [build from source](https://docs.privacytracker.privacykey.org/develop/build-from-source) on the platform of your choice.

## Colima

On macOS without Docker Desktop, [Colima](https://github.com/abiosoft/colima) gives you a lightweight Linux VM:

```bash
brew install colima docker docker-compose
colima start --cpu 2 --memory 4
docker compose up --build -d   # from the checkout, with AUDITOR_ADMIN_TOKEN in .env as above
```

Stop the VM with `colima stop` when you're done; the `privacytracker-data` volume persists.

## iPhone import helper

Companion Python tool (stdlib-only, Python 3.9+) that exports installed-app lists from local Finder/iTunes backups or a connected iPhone via `ideviceinstaller`. It's not wired into the running app — it produces a `.txt` or `.csv` you feed back into the web onboarding flow.

```bash
# From a Finder / iTunes backup
python3 scripts/ios-app-import/export_ios_apps.py --mode backup

# From a connected device (requires libimobiledevice)
python3 scripts/ios-app-import/export_ios_apps.py --mode device
```

You can run the helper from the source repo even when the rest of privacytracker is installed as a desktop app or Docker container — its output is just a text file the web onboarding accepts.

## Behind a reverse proxy

For trusted-LAN deployments the project ships working Compose stacks in the repo checkout — you don't need to write a proxy config from scratch:

```
deploy/caddy/Caddyfile         reverse-proxy config, TLS + optional basic auth
deploy/caddy/compose.yaml      privacytracker + Caddy, ready to `docker compose up`
deploy/caddy/.env.example      image tag, hostname, admin token
deploy/traefik/compose.yaml    the Traefik equivalent
deploy/traefik/.env.example
```

Copy the `.env.example` next to the compose file, fill it in, and bring the stack up from that directory.

Three rules apply here, and the samples only cover the first. privacytracker enforces a same-origin CSRF check on every destructive route, so the proxy must forward the original `Host` header — both sample proxies do that out of the box.

The second one is yours to add. privacytracker only honours `X-Forwarded-For` / `X-Forwarded-Host` when `PRIVACYTRACKER_TRUST_PROXY` is set, and neither `.env.example` carries it, nor do the sample `compose.yaml` files pass it into the app container. Until you add it to the `privacytracker` service's `environment:` block yourself — the root `docker-compose.yml` documents the variable inline — rate-limit keys and `audit_log.actor_ip` collapse to the literal `local`, meaning one shared rate-limit bucket per route and no usable client IP in the audit trail. See [Hardening → TLS via reverse proxy](https://docs.privacytracker.privacykey.org/hardening#tls-via-reverse-proxy) for the threat-model walkthrough.

The third one is what actually breaks the deployment, and it is also yours to add. `proxy.ts` rejects every request whose `Host` isn't allowlisted — `GET` included, before any other gate — with 400 `{"error":"Host not allowed"}`, and the default allowlist is loopback only. Neither sample passes `PRIVACYTRACKER_ALLOWED_HOSTS` into the app container and neither `.env.example` mentions it; the stacks work out of the box only because `PRIVACYTRACKER_HOST` defaults to `privacytracker.localhost` and `*.localhost` is treated as loopback. The moment you follow the `.env.example` comment and set a LAN DNS name, mDNS name, or a real domain, every request 400s until you add that same name to `PRIVACYTRACKER_ALLOWED_HOSTS` in the `privacytracker` service's `environment:` block.

## Verify the install

```bash
curl http://localhost:3000/api/ready
# {"status":"ready","checks":{...}}
```

If you get `{"status":"not_ready"}` (HTTP 503), check write permissions on the `data/` directory and confirm the process can open `data/privacy.db`. WAL mode plus a 5-second `busy_timeout` are set on every open, so concurrent reads while a write is in flight should never block longer than that.

## Where the data lives

| Surface | Database path |
|---|---|
| Desktop app (macOS) | `~/Library/Application Support/privacytracker/privacy.db` |
| Docker | `/app/data/privacy.db` in the `privacytracker-data` volume, or `./data/privacy.db` on the host with the bind-mount file |
| From source | `./data/privacy.db` in the repo working directory |

The database is a single SQLite file. Back it up by copying the file (with the app stopped, or via the in-app **Settings → Backup** export which produces a versioned JSON snapshot you can restore later).
