On-Prem Deployment

Self-hosted Control Seat. Two install paths, both with the same product surface as the cloud SaaS.

Pick an install path

PathUse it when
Native installerYou want the simplest possible install. Single binary, embedded PostgreSQL, no Docker, no .env. Recommended for almost everyone.
Docker ComposeYou'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:

PlatformInstallerWhat it does
macOS 11+.pkgInstalls to /Applications/Control Seat.app, registers a LaunchAgent so the gateway starts at login, opens the browser.
Windows 10 / 11.msiInstalls to C:\Program Files\Control Seat\, registers a Windows Service, adds a Start Menu shortcut.
Linux (Debian / Ubuntu).debInstalls to /usr/lib/controlseat/, creates a systemd unit, auto-starts.
Linux (RHEL / Fedora / Rocky).rpmSame 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:

  1. Stops the running service.
  2. Replaces the binary in /Applications/Control Seat.app/ (macOS), /usr/lib/controlseat/ (Linux), or C:\Program Files\Control Seat\ (Windows).
  3. Runs database migrations against the embedded PostgreSQL.
  4. 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:

  1. Snapshots the currently running image digests.
  2. Re-reads .env so any pinned IMAGE_* overrides are picked up.
  3. Pulls the new images.
  4. Runs database migrations.
  5. Restarts the stack and waits for /healthz to 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)

  1. Open the gateway's Platform → License page.
  2. Click Activate online.
  3. A popup opens to controlseat.com. Sign in if you aren't already.
  4. 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.json on 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:

  1. Paste a PEM-encoded certificate.
  2. Paste the matching private key.
  3. 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

PlatformData 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

PathWhat's in it
./data/postgresPostgres database — tag definitions, dashboards, flows, alarms, users, license records.
./data/clickhouse (if historian enabled)Historian time-series data.
./data/controlseat/license.jsonYour 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.