On-Prem Deployment
Self-hosted Control Seat. Two install paths, both with the same product surface as the cloud SaaS.
Pick an install path
| Path | Use it when |
|---|---|
| Native installer | You want the simplest possible install. Single binary, embedded PostgreSQL, no Docker, no .env. Recommended for almost everyone. |
| Docker Compose | You're running on shared infrastructure (a Kubernetes node, an existing Postgres cluster, a multi-app server) or your operators prefer containers. Air-gapped deploys go through this path. |
You can switch between paths later — the data model is the same, just point Docker Compose at your existing Postgres.
Native installer
Download an installer for your OS from controlseat.com/download:
| Platform | Installer | What it does |
|---|---|---|
| macOS 11+ | .pkg | Installs to /Applications/Control Seat.app, registers a LaunchAgent so the gateway starts at login, opens the browser. |
| Windows 10 / 11 | .msi | Installs to C:\Program Files\Control Seat\, registers a Windows Service, adds a Start Menu shortcut. |
| Linux (Debian / Ubuntu) | .deb | Installs to /usr/lib/controlseat/, creates a systemd unit, auto-starts. |
| Linux (RHEL / Fedora / Rocky) | .rpm | Same as the .deb, for the RPM family. |
Double-click → install → browser opens to http://localhost:8090. No .env editing, no certificate setup, no terminal. PostgreSQL runs embedded inside the install — you don't need to provision a database.
Docker Compose
Use this on a Linux host with Docker 24+ and the docker compose plugin. Roughly:
# 1. Get the deploy bundle (from the v0.1.4 release tarball or this repo)
scp -r deploy/onprem user@host:~/cs-pilot
ssh user@host
cd ~/cs-pilot
# 2. Configure
cp .env.example .env
$EDITOR .env
# Required: POSTGRES_PASSWORD, CS_SECRET_KEY, CS_BASE_URL
# If you're serving HTTPS or going through a reverse proxy, also set
# PUBLIC_PUBLISH_API_BASE, PUBLIC_TAG_API_BASE, CS_ALLOWED_ORIGINS.
# 3. (Optional) Drop a license file for offline activation.
# Skip this if you'd rather activate online from the License page.
mkdir -p ./data/controlseat
cp /path/to/license.json ./data/controlseat/license.json
chmod 600 ./data/controlseat/license.json
# 4. Authenticate to GHCR.
# The container images live at ghcr.io/jackgrodnick/<service>. We'll
# issue you a read-scoped registry token at trial / purchase time —
# contact us if you don't have one yet.
docker login ghcr.io
# 5. Pull and start.
docker compose pull
docker compose up -d
Resource floor: ~4 GB RAM, ~20 GB disk (more if you enable the historian).
Updates
The gateway's Platform → Updates page shows your current version and a Check for updates button. There's no automatic phone-home — we only check when you click. The exact upgrade procedure depends on which install path you used.
Native installers
Re-download the installer for your OS from controlseat.com/download and double-click. The new installer:
- Stops the running service.
- Replaces the binary in
/Applications/Control Seat.app/(macOS),/usr/lib/controlseat/(Linux), orC:\Program Files\Control Seat\(Windows). - Runs database migrations against the embedded PostgreSQL.
- Restarts the service.
Your data directory (database, license, TLS cert, configuration) is never touched by the installer — only the binaries are replaced. To roll back, install the previous version's package.
Docker Compose
Run the updater script on the gateway host:
sudo /opt/controlseat/update.sh
The script:
- Snapshots the currently running image digests.
- Re-reads
.envso any pinnedIMAGE_*overrides are picked up. - Pulls the new images.
- Runs database migrations.
- Restarts the stack and waits for
/healthzto return 200.
If a health probe fails or anything looks wrong:
sudo /opt/controlseat/update.sh --rollback
Rollback restores the exact previous image set. Volumes (license, database, configuration, TLS cert) are preserved across updates and rollbacks — they're never touched.
Licensing
Self-hosted deployments use signed license files. Cloud SaaS doesn't run this code path at all — license-related UI in the gateway only appears when a license configuration is present.
Get a license
Contact us for pricing and term. You don't get a license file up front — you'll be set up with a controlseat.com account, and you mint a license against your specific install in the next step.
Activate online (recommended)
- Open the gateway's Platform → License page.
- Click Activate online.
- A popup opens to controlseat.com. Sign in if you aren't already.
- The license is signed against your gateway's install ID + host fingerprint, sent back to the popup, and applied to the gateway with no restart and no copy-paste. The popup closes itself when it's done.
That's the whole flow. The page shows the install ID and host fingerprint for transparency — they're not something you copy anywhere.
Activate offline
For air-gapped installs, mint a license against your gateway from a machine that does have internet access (using your install ID + fingerprint from the License page), then drop the resulting license.json into the install's data directory:
- Native installers — the License page shows the exact path for your platform. On macOS that's
~/Library/Application Support/Control Seat/license.json; on Linux it's/var/lib/controlseat/license.json; on Windows it's%PROGRAMDATA%\Control Seat\license.json. - Docker Compose —
./data/controlseat/license.jsonon the host (the path bound into the gateway container).
If you need help with the offline mint, contact us and we'll generate the file for you.
Re-activation and host-binding
Each license is bound to one gateway by its install ID and host fingerprint (a hash of stable hardware identifiers — machine ID, primary MAC, etc.). Moving the gateway to new hardware invalidates the binding by design. Re-activation from the License page is one click — same flow, fresh license.
Renewal and expiry
Licenses include an expires_at timestamp. Past expiry there's a 24-hour grace period to absorb NTP outages. After grace, the License page shows a renewal prompt and /api/system-info reports mode: expired. The gateway's editor and configuration UI keep working so you can renew — only runtime endpoints (live tag I/O, historian, runtime page rendering) are gated. We don't lock you out of your own data.
TLS
The gateway listens for HTTP on :8090 and HTTPS on :8443. HTTPS only serves traffic once you've uploaded a certificate.
From the gateway's Platform → TLS page:
- Paste a PEM-encoded certificate.
- Paste the matching private key.
- Save.
The cert validates on upload and hot-swaps without restart. Replacing a near-expired cert never requires a maintenance window. Private keys are encrypted at rest with the same vault used for datasource secrets.
If you'd rather run a reverse proxy in front of Control Seat (nginx, Caddy, your existing edge), point it at :8090 and set CS_BASE_URL=https://your-domain so links and redirects use the public URL.
Air-gapped deploys
Air-gapped is supported but takes a few manual steps because the Docker Compose tarball ships compose + scripts only — not the container images themselves. To stage a fully offline install:
# On a host that DOES have internet access:
docker login ghcr.io # using credentials we'll provide
# Pull the five images for the release you're deploying.
for svc in driverhost migrate publish-service gateway-worker editor; do
docker pull ghcr.io/jackgrodnick/${svc}:vX.Y.Z
done
# Save them to a tarball you can move across the air gap.
docker save \
ghcr.io/jackgrodnick/driverhost:vX.Y.Z \
ghcr.io/jackgrodnick/migrate:vX.Y.Z \
ghcr.io/jackgrodnick/publish-service:vX.Y.Z \
ghcr.io/jackgrodnick/gateway-worker:vX.Y.Z \
ghcr.io/jackgrodnick/editor:vX.Y.Z \
-o controlseat-images-vX.Y.Z.tar
Move both controlseat-images-vX.Y.Z.tar and the deploy tarball onto the air-gapped host (USB, internal artifact server, whatever), then:
docker load -i controlseat-images-vX.Y.Z.tar
tar -xzf controlseat-onprem-vX.Y.Z.tar.gz
cd controlseat-onprem-vX.Y.Z
cp .env.example .env && $EDITOR .env
docker compose up -d
Updates follow the same pattern — pull + save the new images on a connected host, ferry the tarball across, docker load, then run update.sh on the air-gapped side. If you maintain an internal Docker registry mirror, point IMAGE_* overrides in .env at it and the updater pulls from there instead.
For larger or fully-offline customers we can build a one-shot bundle that includes the images. Contact us and we'll send a signed bundle for your specific release.
Backup
Two volumes hold all the state worth backing up. Paths depend on your install path:
Native installers
| Platform | Data directory (everything lives under here) |
|---|---|
| macOS | ~/Library/Application Support/Control Seat/ |
| Windows | %PROGRAMDATA%\Control Seat\ |
| Linux | /var/lib/controlseat/ |
The data directory contains the embedded Postgres database (tags, dashboards, flows, alarms, users, license records), the active license.json, and the encrypted TLS key + cert.
Docker Compose
| Path | What's in it |
|---|---|
./data/postgres | Postgres database — tag definitions, dashboards, flows, alarms, users, license records. |
./data/clickhouse (if historian enabled) | Historian time-series data. |
./data/controlseat/license.json | Your signed license file (offline activation only). |
./data/controlseat/tls/ | Uploaded TLS cert + encrypted private key. |
Snapshot these volumes the same way you'd back up any other Postgres + ClickHouse pair (file-system snapshot, pg_dump, ClickHouse BACKUP TABLE). Volumes survive update.sh and --rollback; they don't survive a complete uninstall.
Where to go next
- Tags & Data Sources — connecting tags to MQTT, OPC UA, SQL, and the rest.
- Architecture — what's actually running inside a self-hosted install.
- Get in touch for a hands-on walkthrough of your specific environment.