Messaging
    JetStream

    Deploy NATS + JetStream on a VPS

    Self-host NATS with JetStream persistence on a RamNode VPS — systemd install, TLS, stream configuration, and a 3-node cluster.

    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

    shell
    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/nats

    3. Base config with JetStream enabled

    /etc/nats/nats-server.conf:

    shell
    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:

    shell
    [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.target
    shell
    sudo systemctl daemon-reload
    sudo systemctl enable --now nats-server
    sudo systemctl status nats-server

    Verify:

    shell
    curl http://localhost:8222/varz | head -20
    curl http://localhost:8222/jsz

    5. TLS

    Generate/obtain certs with Let's Encrypt (certbot on a dedicated hostname, e.g. nats.internal.ramnode.example), then add to the config:

    shell
    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:

    shell
    sudo systemctl restart nats-server

    6. 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:

    shell
    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:

    shell
    curl http://localhost:8222/routez
    curl http://localhost:8222/jsz?accounts=true

    JetStream 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:

    shell
    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

    shell
    nats stream backup ORDERS /opt/nats-backups/orders-$(date +%F).tar.gz

    Cron nightly per stream, same pattern as elsewhere:

    shell
    0 3 * * * nats stream backup ORDERS /opt/nats-backups/orders-$(date +\%F).tar.gz >> /var/log/nats-backup.log 2>&1

    9. Common issues

    SymptomLikely causeFix
    nats: no servers available from clientFirewall blocking 4222 or wrong auth credsConfirm port open from client IP, re-check authorization block matches client config
    Cluster nodes not routing to each other6222 blocked between nodes, or routes list missing a peerRe-check firewall + confirm all three nodes list all three routes (including self)
    Stream stuck with fewer replicas than requestedNot enough healthy nodes to satisfy --replicasBring 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 hitRaise the cap if disk allows, or trim/purge old messages via stream retention policy
    Monitoring endpoint (8222) reachable from unexpected IPsFirewall rule too broadTighten to jump host / monitoring server IP only