Target environment: RamNode KVM VPS, Ubuntu 24.04 LTS. Covers single-node install, JetStream persistence config, TLS, and a 3-node clustered deployment across RamNode datacenters.
1. Prerequisites
- Any RamNode VPS; NATS itself is lightweight (a single Go binary), but JetStream's file store
needs real disk headroom sized to your expected stream retention — plan storage per stream
(
max_bytes) rather than assuming it'll fit. - For clustering: 3 nodes (odd number, Raft-based like Typesense) for quorum. Cross-region clustering works but write latency scales with the slowest link in the Raft group — same-DC is strongly preferred for anything write-heavy.
- Firewall: client port (
4222), cluster routing port (6222), and monitoring port (8222) each need distinct exposure rules — see section 6.
2. Single-node install
NATS_VERSION=2.10.22
curl -L https://github.com/nats-io/nats-server/releases/download/v${NATS_VERSION}/nats-server-v${NATS_VERSION}-linux-amd64.tar.gz -o nats-server.tar.gz
tar xzf nats-server.tar.gz
sudo mv nats-server-v${NATS_VERSION}-linux-amd64/nats-server /usr/local/bin/
rm -rf nats-server.tar.gz nats-server-v${NATS_VERSION}-linux-amd64
sudo useradd --system --no-create-home --shell /usr/sbin/nologin nats
sudo mkdir -p /etc/nats /var/lib/nats/jetstream
sudo chown -R nats:nats /var/lib/nats3. Base config with JetStream enabled
/etc/nats/nats-server.conf:
server_name: ramnode-nats-01
listen: 0.0.0.0:4222
http: 0.0.0.0:8222
jetstream {
store_dir: /var/lib/nats/jetstream
max_memory_store: 1GB
max_file_store: 20GB
}
authorization {
user: appuser
password: <generate-a-strong-password>
}Adjust max_memory_store / max_file_store to whatever the VPS plan's RAM/disk actually supports
— these are hard caps, not soft targets; JetStream will reject writes once a stream's storage
budget is hit, so also set per-stream max_bytes deliberately rather than relying on the global cap.
4. systemd unit
/etc/systemd/system/nats-server.service:
[Unit]
Description=NATS Server with JetStream
After=network.target
[Service]
Type=simple
User=nats
Group=nats
ExecStart=/usr/local/bin/nats-server -c /etc/nats/nats-server.conf
Restart=on-failure
RestartSec=5
LimitNOFILE=65536
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now nats-server
sudo systemctl status nats-serverVerify:
curl http://localhost:8222/varz | head -20
curl http://localhost:8222/jsz5. TLS
Generate/obtain certs with Let's Encrypt (certbot on
a dedicated hostname, e.g. nats.internal.ramnode.example), then add to the config:
tls {
cert_file: "/etc/letsencrypt/live/nats.internal.ramnode.example/fullchain.pem"
key_file: "/etc/letsencrypt/live/nats.internal.ramnode.example/privkey.pem"
verify: false
}Set verify: true and add ca_file if you want mutual TLS between clients and the server — worth
doing if this NATS instance is reachable from anything outside a fully trusted internal network.
Restart after any config change:
sudo systemctl restart nats-server6. RamNode firewall notes
4222/tcp— client connections. Restrict to known app-server IPs/private subnet, don't expose to the public internet unless you have auth + TLS locked down and a real reason to.6222/tcp— cluster routing, needed only between cluster members. Never expose publicly.8222/tcp— HTTP monitoring endpoint. Restrict to your jump host / monitoring server only; it leaks operational detail (connection counts, stream info) you don't want public.
7. 3-node cluster
On each node, add a cluster block. Example for node 1 (10.0.0.11), assuming nodes at
10.0.0.11/.12/.13:
server_name: ramnode-nats-01
listen: 0.0.0.0:4222
http: 0.0.0.0:8222
jetstream {
store_dir: /var/lib/nats/jetstream
max_file_store: 20GB
}
cluster {
name: ramnode-cluster
listen: 0.0.0.0:6222
routes: [
nats-route://10.0.0.11:6222
nats-route://10.0.0.12:6222
nats-route://10.0.0.13:6222
]
}
authorization {
user: appuser
password: <same-password-all-nodes>
}Repeat on nodes 2 and 3 with matching server_name values (ramnode-nats-02, -03) — everything
else in the cluster block stays identical across all three; the routes list includes every
node including itself, that's expected and normal for NATS.
Bring all three up, then check cluster state:
curl http://localhost:8222/routez
curl http://localhost:8222/jsz?accounts=trueJetStream automatically forms a Raft group per stream/consumer once clustered — you don't separately configure Raft, it rides on top of the cluster routes.
Stream replication
When creating streams on a clustered setup, explicitly set replicas so JetStream actually distributes copies instead of defaulting to 1:
nats stream add ORDERS --subjects "orders.>" --replicas 3 --storage file --max-bytes 10GB--replicas 3 on a 3-node cluster gives full redundancy; you can survive one node going down
without losing the stream, since Raft only needs 2 of 3 for quorum.
8. Backups
nats stream backup ORDERS /opt/nats-backups/orders-$(date +%F).tar.gzCron nightly per stream, same pattern as elsewhere:
0 3 * * * nats stream backup ORDERS /opt/nats-backups/orders-$(date +\%F).tar.gz >> /var/log/nats-backup.log 2>&19. Common issues
| Symptom | Likely cause | Fix |
|---|---|---|
nats: no servers available from client | Firewall blocking 4222 or wrong auth creds | Confirm port open from client IP, re-check authorization block matches client config |
| Cluster nodes not routing to each other | 6222 blocked between nodes, or routes list missing a peer | Re-check firewall + confirm all three nodes list all three routes (including self) |
| Stream stuck with fewer replicas than requested | Not enough healthy nodes to satisfy --replicas | Bring the missing node back up; JetStream will catch it up via Raft once reachable |
| JetStream rejects writes with "no space left" | Global max_file_store or per-stream max_bytes hit | Raise the cap if disk allows, or trim/purge old messages via stream retention policy |
Monitoring endpoint (8222) reachable from unexpected IPs | Firewall rule too broad | Tighten to jump host / monitoring server IP only |
