Docs menu

Deployment

dotwire needs three things: the dotwire binary or image, PostgreSQL with the Timescale extension, and NATS with JetStream enabled. The repository ships two Compose files: compose.yaml for a single node and compose.multinode.yaml for a split deployment.

The published image is ghcr.io/pasztor-mark/dotwire:latest, for linux/amd64 and linux/arm64.

Single node

compose.yaml runs three services:

  • dotwire in the default All role, listening on port 8080.
  • postgres, from timescale/timescaledb:latest-pg17.
  • nats, from nats:2.10-alpine, started with JetStream (-js) and file storage on a volume.

See the Quickstart for the .env values it needs.

The dotwire service is hardened by default: it runs as a non-root user with a read-only filesystem, drops all Linux capabilities, sets no-new-privileges, and raises the open-file limit to 65,536. It also turns on server GC with dynamic adaptation (DOTNET_gcServer=1, DOTNET_GCDynamicAdaptationMode=1), which suits connection-dense workloads. Keep these settings when you write your own deployment.

Required settings

SettingWhat it's for
ConnectionStrings__PostgresMigratorThe owner role (dotwire) that runs migrations
ConnectionStrings__PostgresWriteThe runtime role (dotwire_app) for writes
ConnectionStrings__PostgresReadThe runtime role for reads. Point it at a replica to move reads off the primary.
Postgres__AppRolePasswordThe password dotwire sets for dotwire_app when it runs migrations
Dotwire__Encryption__ActiveKeyId, Dotwire__Encryption__Keys__<id>At-rest encryption key. See Encryption and retention.
Auth__Issuer, Auth__Audience, and Auth__Keys__<kid> or Auth__JwksUrlHost token verification. See Auth.
Nats__UrlNATS server URL. Separate several URLs with commas.

Every option is listed in the configuration reference.

Migrations

With Postgres__Migrate=true (the default), dotwire runs its embedded SQL migrations at startup through the migrator connection, then sets the dotwire_app password from Postgres__AppRolePassword. With Nats__Enabled=true (the default), it also provisions its JetStream streams at startup.

The runtime role holds only the grants it needs. In particular, it can't update or delete audit rows. See Audit log.

Health check

GET /healthz returns 200 on every node role. The Compose files and the sample HAProxy config use it.

Node roles

Set Dotwire__Role to All, Api, or Gateway. See Node roles for what each one serves.

  • Run the Postgres writer and audit writer on Api or All nodes. Gateway nodes don't run them.
  • Run migrations from one node. compose.multinode.yaml sets Postgres__Migrate to true on the API node and false on gateways.
  • Only one audit writer is active at a time. Additional Api nodes wait in standby. See Audit log.
  • Retention set with Dotwire__Retention__MessagesDays is applied by the node that runs migrations.

Multiple nodes

compose.multinode.yaml is a reference topology:

  • two Gateway nodes that hold WebSocket and SSE connections,
  • one Api node that serves REST and admin traffic and runs migrations,
  • a three-node NATS cluster with JetStream,
  • a Postgres primary for writes and a streaming replica for reads,
  • HAProxy in front, configured by haproxy.cfg.

Routing

Gateway nodes serve the hub (/hub/rooms) and SSE (/rooms/{roomId}/events). Api nodes serve everything else. Route requests to match.

The sample haproxy.cfg sends WebSocket upgrades, /hub/rooms, and /rooms/{roomId}/events to the gateways, and everything else to the API node.

Load balancer

Connections are WebSockets only, with negotiation skipped, so you don't need sticky sessions. An L4 (TCP passthrough) load balancer adds the least per-connection overhead. The sample haproxy.cfg runs in HTTP mode only because it serves both roles from one listener and routes by path. If you give each role its own listener or hostname, a plain TCP proxy works.

Terminate TLS at your load balancer or ingress. dotwire listens on plain HTTP on port 8080.

Rate limits across nodes

Rate limits are enforced in each process and nodes don't share counters. With N nodes behind a load balancer, a user's effective limit is N times the configured value. See Rate limits.

Securing NATS

Anyone who can connect to NATS can bypass dotwire's token and membership checks at the broker: read ciphertext and metadata, publish forged events, or administer JetStream. Treat NATS as an internal service.

  • Don't publish NATS ports outside your private network. compose.yaml binds 4222 (clients) and 8222 (monitoring) to 127.0.0.1 only, and compose.multinode.yaml doesn't publish 4222 at all.
  • Turn on NATS authentication anywhere other machines share the network. The bundled Compose files run NATS without it.

To connect to an authenticated NATS server, put the credentials in the URL:

Nats__Url=nats://<user>:<password>@nats:4222

A URL with only a user part, nats://<token>@nats:4222, sends it as a token. With a cluster, list every server, comma-separated, with the same credentials in each URL. A wrong or missing password fails at startup.

dotwire has no settings for NATS client certificates, NKeys, or credential files.

The same applies to Postgres: compose.yaml binds 5432 to 127.0.0.1.

Before you go further

dotwire v0.2 is an early release and isn't production-hardened. If you deploy it anyway:

  • Keep the encryption key in a secret store and back it up. Losing it makes history unreadable.
  • Archive audit_checkpoints rows to independent storage on a regular schedule.
  • Back up and encrypt Postgres and the NATS volumes. dotwire's at-rest encryption covers message content, not your backup storage.
  • Review the shared responsibility matrix.