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:
dotwirein the defaultAllrole, listening on port 8080.postgres, fromtimescale/timescaledb:latest-pg17.nats, fromnats: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
| Setting | What it's for |
|---|---|
ConnectionStrings__PostgresMigrator | The owner role (dotwire) that runs migrations |
ConnectionStrings__PostgresWrite | The runtime role (dotwire_app) for writes |
ConnectionStrings__PostgresRead | The runtime role for reads. Point it at a replica to move reads off the primary. |
Postgres__AppRolePassword | The 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__JwksUrl | Host token verification. See Auth. |
Nats__Url | NATS 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
ApiorAllnodes.Gatewaynodes don't run them. - Run migrations from one node.
compose.multinode.yamlsetsPostgres__Migratetotrueon the API node andfalseon gateways. - Only one audit writer is active at a time. Additional
Apinodes wait in standby. See Audit log. - Retention set with
Dotwire__Retention__MessagesDaysis applied by the node that runs migrations.
Multiple nodes
compose.multinode.yaml is a reference topology:
- two
Gatewaynodes that hold WebSocket and SSE connections, - one
Apinode 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.yamlbinds4222(clients) and8222(monitoring) to127.0.0.1only, andcompose.multinode.yamldoesn't publish4222at 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_checkpointsrows 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.