This repository defines the NATS messaging service with JWT authentication enabled. The setup is fully containerized and designed to be reproducible across environments.
Clone the repository and navigate to the directory.
git clone https://github.com/teyfix/nats-docker-compose-recipe.git
cd nats-docker-compose-recipeAlthough repository has the .env file with default values, you can override
them as needed.
NATS_OPERATOR="my_operator"
NATS_ACCOUNT="my_account"
NATS_VER="2.10"
NATS_BOX_VER="0.19.2"Bootstrap the system and start the NATS server. We are starting nats-export
container so the signing keys will be exported after the NATS server starts.
docker compose up nats-init nats-server nats-push nats-export# 3. Run the example client
bun install
bun run dev- NATS Service
At a high level, this service:
-
Runs a NATS server with:
- Core NATS (TCP clients)
- JetStream for persistence and replay
- WebSocket support for browser-based clients
-
Bootstraps NATS Operator and Account state automatically using
nsc -
Generates and exports signing keys for external services
-
Exposes NATS, WebSocket, and monitoring endpoints through Traefik
-
Stores all state (JetStream + NSC) in Docker volumes
This allows application services to authenticate using JWTs, without NATS needing to consult an external database or authority.
-
nats-init
-
One-time bootstrap container
-
Creates:
- Operator
- System account
- Application account
- Account signing key
-
Generates the JWT resolver configuration
-
Idempotent: exits immediately if state already exists
-
-
nats-server
-
The actual NATS server
-
JetStream enabled
-
Loads resolver configuration generated by
nats-init -
Exposes:
- TCP clients (
4222) - WebSocket clients (
9222) - Monitoring (
8222)
- TCP clients (
-
-
nats-push
- Pushes operator and account JWTs into the running server
- Runs
nsc push -A - Ensures the server has the latest account claims
-
nats-export
- Extracts account signing keys for external use
- Writes them to
.env-style files - Intended for trusted backend services only
-
Client Port:
4222 -
Monitoring:
http://:8222 -
WebSocket:
0.0.0.0:9222(no TLS, TLS handled by Traefik) -
JetStream:
- Memory:
2G - Disk:
10Gi - Data directory:
/data
- Memory:
-
Graceful shutdown:
- Lame duck mode enabled for safe restarts
JWT resolution is handled via an NSC-generated resolver included at startup.
This service uses NATS JWT-based authentication:
- Operator and Account are created automatically
- Clients authenticate using user JWTs
- NATS does not perform per-room or per-subject authorization checks beyond JWT claims
- Authorization logic lives entirely in application services
This means:
- No database tables are required for NATS auth
- Room join/leave enforcement is handled at the app layer
- NATS acts purely as a message router + persistence layer
This setup uses JWT-based authorization, where trust is derived from cryptographic keys rather than server-side state.
-
Operator
- Root authority
- Defines global trust
- Should almost never be used outside bootstrapping
-
Account
- Represents an isolated namespace
- Owns users and permissions
- Signs user JWTs via an account signing key
-
User JWTs
- Short-lived credentials
- Define exact publish/subscribe permissions
- Can be safely issued by application services
-
NATS never stores user secrets
-
Account signing keys are powerful
- Anyone with this key can mint arbitrary users
-
User JWTs should be short-lived
-
nats-exportis intentionally dangerous- It extracts private signing keys
- Intended for CI or trusted backend services only
- Never expose exported keys to browsers or clients
If an attacker obtains:
- Account signing seed → they can create any user
- User credentials → damage is limited by JWT permissions and expiration
This design favors stateless authorization, clear authority, and predictable failure modes.
The service is exposed through Traefik with:
- TCP (TLS):
nats.<domain>→ port4222 - WebSocket (HTTPS):
https://nats.<domain>/ws - Monitoring UI (HTTPS):
https://nats.<domain>/
TLS termination is handled by Traefik using the configured certificate resolver.
nats_data– JetStream persistencensc_data– NSC state (operator, accounts, keys)nsc_config– Generated resolver configuration
These volumes allow the system to restart without losing identity or message state.
Required:
NATS_OPERATOR– Operator nameNATS_ACCOUNT– Account name
Optional / infrastructure-dependent:
NATS_VERNATS_BOX_VERTRAEFIK_*variables
Exported signing keys are written to:
svc/nats/fs/nats-export/secrets/.env.operatorsvc/nats/fs/nats-export/secrets/.env.account
- Real-time messaging between backend services
- Browser clients connecting via WebSockets
- Temporary or long-lived rooms backed by JetStream
- Event-driven workflows with replay and durability
After obtaining the account credentials with nats-export container, you can
generate NATS credentials for users and allow them to connect themselves.
Here is the trimmed version of src/client.ts:
import { connect, credsAuthenticator } from "nats.ws";
import { encodeUser } from "nats-jwt";
import { createUser, fromSeed } from "nkeys.js";
const encodeText = (text: string) => new TextEncoder().encode(text);
const decodeText = (text: Uint8Array) => new TextDecoder().decode(text);
// Can be obtained with `nats-export` container
const accountKey = process.env.NATS_ACCOUNT_KEY!;
const accountSeed = process.env.NATS_ACCOUNT_SECRET!;
// The user we want to create credentials for.
const userId = "john-doe";
// Create user keypair
const userKP = createUser();
const userSeed = decodeText(userKP.getSeed());
// Load account keypair (issuer)
const accountKP = fromSeed(encodeText(accountSeed));
// UNIX timestamp for expiration
const exp = Math.floor(Date.now() / 1000) + 30 * 60;
// Encode & sign user JWT
const jwt = await encodeUser(
`user-${userId}`, //
userKP,
accountKP,
{
issuer_account: accountKey,
pub: { allow: [`users.${userId}.>`], deny: [] },
sub: { allow: [`users.${userId}.>`], deny: [] },
},
{ exp }
);
const creds = `
-----BEGIN NATS USER JWT-----
${jwt}
------END NATS USER JWT------
-----BEGIN USER NKEY SEED-----
${userSeed}
------END USER NKEY SEED------
`;
const client = await connect({
authenticator: credsAuthenticator(encodeText(creds)),
});- Clustering is currently disabled but pre-configured in comments
- WebSocket TLS is intentionally disabled at NATS level
- All authority is derived from JWTs, not server-side state
nats-initinitializes operator/accountnats-serverstarts with resolver confignatspushes NSC statenats-exportoptionally exports signing keys
The NATS server is considered healthy when:
wget -q --spider http://localhost:8222/healthzreturns successfully.
This setup prioritizes stateless authorization, simple operations, and clear separation of concerns between messaging infrastructure and application logic.