Each client runs PaceSetter on its own dedicated hardware — an PaceSetter Healthware-provided appliance or the client's own server, in the client's own facility — fully isolated from every other client and from PaceSetter Healthware's own systems. This document is generic by design: it describes what any client's deployment contains, not a specific instance's IP addresses or container names. PaceSetter Healthware's own operational infrastructure (license server, its own demo/test instances, monitoring tooling, marketing site) is documented separately in the PaceSetter Healthware Internal Infrastructure Runbook (internal only) (HIPAA-SEC-014) and is not installed on client hardware.
A single client deployment is exactly two containers plus a tunnel — nothing else from PaceSetter Healthware's own stack ships with it:
| Component | Image | Purpose | Env Vars (names only) |
|---|---|---|---|
| Database | mariadb:latest | The client's own isolated PaceSetter database — one per deployment, never shared. | MARIADB_DATABASE, MARIADB_USER, MARIADB_PASSWORD, MARIADB_ROOT_PASSWORD |
| App / Web Server | lscr.io/linuxserver/swag:latest | Reverse proxy + TLS termination + the PaceSetter PHP application itself. | EMAIL, PUID, PGID, TZ, URL, SUBDOMAINS, ONLY_SUBDOMAINS, VALIDATION, DNSPLUGIN, DHLEVEL |
| Tunnel | Cloudflare Tunnel connector | Sole external ingress path — no inbound port is ever opened on the client's network. Client-owned by default (see Section 4); PaceSetter Healthware can optionally manage it on request. | TUNNEL_TOKEN, TUNNEL_RETRIES, TUNNEL_TRANSPORT_PROTOCOL |
Application-level configuration is supplied via config/.env (see config/.env.example in the mih-scheduler repo for the full, current template) — database credentials, the app URL, and optional integrations below.
| Integration | Purpose | Default if unconfigured |
|---|---|---|
| OSRM (route optimization) | Self-hosted routing engine for the Route Optimizer feature. | Disabled — never falls back to a public OSRM instance, since that would send patient address coordinates outside the deployment. |
| Nominatim (geocoding) | Self-hosted address → coordinate geocoding. | Disabled — never falls back to the public nominatim.openstreetmap.org, for the same reason. |
| Resend (transactional email) | Password reset, welcome emails, video-consult invites. | Disabled — email sending simply doesn't happen unconfigured. |
| Backup destination | Where encrypted nightly backups are uploaded. | The client's own choice — Backblaze B2, a local NAS via rsync, or any destination the client controls. See the Vendor Security Register (HIPAA-SEC-012) for the vendor-BAA implications of choosing Backblaze. |
| Uptime/host monitoring | Optional self-hosted Uptime Kuma / Netdata, same pattern PaceSetter Healthware uses for its own instances. | Disabled — a client may stand up their own copy if desired; not part of the base deployment. |
PDF/document rendering via Gotenberg was removed from the application entirely on 2026-07-16 — all report and chart exports now use the browser's own print-to-PDF, so no rendering service is needed in a client deployment at all.
The Cloudflare Tunnel connecting a client's deployment to the internet is, by default, the client's own Cloudflare account — the client owns that account and any BAA obligation tied to it. If a client prefers PaceSetter Healthware to set up and manage the tunnel on their behalf, PaceSetter Healthware can do so as a service; in that case the tunnel account (and its BAA, if applicable) is PaceSetter Healthware's own.
PaceSetter Healthware's own physical server(s) run in an office environment with UPS and generator backup power. We recommend any client running PaceSetter on their own hardware have similar power redundancy for their server — a UPS at minimum, a generator where feasible — since an unplanned power loss on client-owned hardware is outside PaceSetter Healthware's ability to prevent or mitigate.
Recovery order for a client deployment mirrors the PaceSetter Healthware-internal runbook's Section 5, scoped down to this deployment's two containers: (1) provision replacement hardware, (2) recreate the MariaDB and SWAG containers with the same config/.env values, (3) restore the database from the client's configured backup destination, (4) clone the mih-scheduler repo and run ./deploy.sh migrate, (5) point the Cloudflare Tunnel at the new host.
| Backblaze B2 | Local + rsync | |
|---|---|---|
| Who moves the file offsite | The app itself, automatically, right after each backup completes | The client's own external rsync/cron job, on a separate machine, pulling on its own schedule |
| Truly offsite? | Yes, always — B2 is a different physical location by definition | Only if the client's rsync destination is a genuinely separate machine/location. If nothing ever copies the file off the PaceSetter host, it's not offsite at all — same building, same disaster risk (fire, theft, hardware failure) as the primary server. |
| Ongoing cost | Backblaze storage fees (billed to whoever owns that B2 account — see Section 4, Tunnel Ownership, for the same client-vs-PaceSetter Healthware ownership split) | No cloud storage cost — just whatever storage the client already owns at the destination |
| Setup effort | One-time: create a B2 account, enter 3 credentials in Admin settings | Client sets up and maintains their own rsync/cron job (Section 5b below) — nothing to configure inside PaceSetter itself |
| Encryption | Same — every backup is GPG-encrypted before either path ever touches it | Same |
If Backblaze B2 isn't configured (the b2_key_id/b2_app_key/b2_bucket_name settings are left blank), PaceSetter still writes a full, GPG-encrypted backup on every scheduled run — it just stays on local disk instead of also being uploaded. Getting that file off the box (to another server, a NAS, external drive, etc.) is the client's own responsibility, done outside the app with standard tools.
Each backup is written inside the SWAG/app container at the path set by the backup_path app setting (default /config/backups), named scheduler_backup_YYYY-MM-DD_H-i-s.sql.gpg. That container path is backed by a Docker volume mount on the host — whatever host directory was mapped to the container's /config when the deployment was provisioned is where the real files live on disk. Ask whoever set up the deployment for that exact host path if it isn't already documented for your install.
The app itself deletes its own older backups once there are more than backup_retention (default 7) present in that directory — this happens regardless of whether Backblaze is configured. An external rsync job doesn't need to manage cleanup on the source side; it only needs to copy whatever's currently there.
Run this from the machine you want the backups copied to (not from the PaceSetter host itself), as a cron job:
0 5 * * * rsync -az --delete-after user@cadence-host:/path/to/appdata/swag-instance/backups/ /local/backup/archive/cadence/
Replace user@cadence-host and the source path with your deployment's actual SSH access and the host-side backup directory identified above. --delete-after keeps the destination mirrored to whatever the app currently has (relying on the app's own retention, not a second independent retention policy) — drop that flag if you'd rather keep every backup forever on the destination side regardless of what the source has rotated out.
Each .sql.gpg file is already encrypted before it ever touches disk — copying it over plain rsync/SSH doesn't re-expose the data; the GPG passphrase (set via BACKUP_ENCRYPTION_PASSPHRASE in config/.env) is what's needed to actually restore from any of these files later.
New client deployments are set up using PaceSetter Healthware's internal provisioning tooling (documented in the PaceSetter Healthware Internal Infrastructure Runbook — internal only, Section 2.7) — this tooling itself is never installed on client hardware; it only creates and configures the two containers described in Section 2 above.
As of this writing, PaceSetter Healthware can only provision a deployment onto hardware that is physically in hand — an PaceSetter Healthware-provided appliance is set up before it ships to the client, or a client-owned box is provisioned while temporarily in PaceSetter Healthware's possession. There is no remote installer yet that provisions a deployment on hardware already in the client's own facility; that capability is tracked as a future item (a cross-platform install script, not yet built).