Each client runs Cadence on its own dedicated hardware — an EMStool-provided appliance or the client's own server, in the client's own facility — fully isolated from every other client and from EMStool'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. EMStool's own operational infrastructure (license server, its own demo/test instances, monitoring tooling, marketing site) is documented separately in the EMStool 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 EMStool's own stack ships with it:
| Component | Image | Purpose | Env Vars (names only) |
|---|---|---|---|
| Database | mariadb:latest | The client's own isolated Cadence 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 Cadence 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); EMStool 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 EMStool 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 EMStool to set up and manage the tunnel on their behalf, EMStool can do so as a service; in that case the tunnel account (and its BAA, if applicable) is EMStool's own.
EMStool's own physical server(s) run in an office environment with UPS and generator backup power. We recommend any client running Cadence 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 EMStool's ability to prevent or mitigate.
Recovery order for a client deployment mirrors the EMStool-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 Cadence 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-EMStool 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 Cadence 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), Cadence 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 Cadence 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 EMStool's internal provisioning tooling (documented in the EMStool 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, EMStool can only provision a deployment onto hardware that is physically in hand — an EMStool-provided appliance is set up before it ships to the client, or a client-owned box is provisioned while temporarily in EMStool'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).