NATS is a lightweight, high-performance messaging system written in Go. The core server delivers messages with publish-subscribe, request-reply and queue groups, and its built-in JetStream engine adds persistence, replay and at-least-once delivery. In this tutorial you will install NATS Server on Ubuntu 24.04, run it as a systemd service with authentication, and use the nats CLI to test each messaging pattern, including a persistent JetStream stream.

Prerequisites

To follow this tutorial you need:

  • A server running Ubuntu 24.04 LTS, for example a CubePath VPS, with at least 1 GB of RAM.
  • A non-root user with sudo privileges.
  • The curl, unzip and jq tools. Install them with sudo apt install -y curl unzip jq if they are missing.

Step 1 - Installing NATS Server

NATS Server is a single static binary published on GitHub. Look up the latest release version with the GitHub API:

NATS_VERSION=$(curl -fsSL https://api.github.com/repos/nats-io/nats-server/releases/latest | jq -r .tag_name)
echo "$NATS_VERSION"
v2.12.x

Download the Linux tarball and the checksum file for that release. Use linux-arm64 instead of linux-amd64 on an ARM server:

cd /tmp
curl -fLO "https://github.com/nats-io/nats-server/releases/download/${NATS_VERSION}/nats-server-${NATS_VERSION}-linux-amd64.tar.gz"
curl -fLO "https://github.com/nats-io/nats-server/releases/download/${NATS_VERSION}/SHA256SUMS"

Verify the tarball against the published checksum:

sha256sum --check --ignore-missing SHA256SUMS
nats-server-v2.12.x-linux-amd64.tar.gz: OK

Extract it and install the binary:

tar -xzf "nats-server-${NATS_VERSION}-linux-amd64.tar.gz"
sudo install -m 0755 "nats-server-${NATS_VERSION}-linux-amd64/nats-server" /usr/local/bin/nats-server
nats-server --version
nats-server: v2.12.x

Step 2 - Installing the NATS CLI

The nats CLI is the tool you use to publish, subscribe, manage streams and generate password hashes. It is released separately:

NATSCLI_VERSION=$(curl -fsSL https://api.github.com/repos/nats-io/natscli/releases/latest | jq -r .tag_name | sed 's/^v//')
cd /tmp
curl -fLO "https://github.com/nats-io/natscli/releases/download/v${NATSCLI_VERSION}/nats-${NATSCLI_VERSION}-linux-amd64.zip"
unzip -o "nats-${NATSCLI_VERSION}-linux-amd64.zip"
sudo install -m 0755 "nats-${NATSCLI_VERSION}-linux-amd64/nats" /usr/local/bin/nats
nats --version

Step 3 - Creating the user and configuration

Run the server as an unprivileged system user with its own data directory for JetStream:

sudo useradd --system --no-create-home --shell /usr/sbin/nologin nats
sudo mkdir -p /etc/nats /var/lib/nats
sudo chown nats:nats /var/lib/nats

Clients authenticate with a username and password. Store bcrypt hashes in the configuration instead of plain passwords. Generate one hash for an admin user and one for an orders application user; the command asks for the password twice and prints the hash:

nats server passwd
? Enter password [? for help] ********************
? Reenter password [? for help] ********************

$2a$11$Qm3...

Run it twice and keep both hashes. Now create the configuration file:

sudo nano /etc/nats/nats-server.conf
server_name: nats-1

# Client connections
listen: 0.0.0.0:4222

# Monitoring endpoint, local only
http: 127.0.0.1:8222

jetstream {
  store_dir: /var/lib/nats
  max_memory_store: 256MB
  max_file_store: 10GB
}

authorization {
  users: [
    {
      user: admin
      password: "$2a$11$REPLACE_WITH_ADMIN_HASH"
    }
    {
      user: orders
      password: "$2a$11$REPLACE_WITH_ORDERS_HASH"
      permissions: {
        publish: ["orders.>"]
        subscribe: ["orders.>", "_INBOX.>"]
      }
    }
  ]
}

Replace the two placeholders with your hashes and keep the double quotes around them. What this configuration does:

  • listen accepts clients on port 4222 on all interfaces. http exposes the monitoring API only on localhost.
  • jetstream enables persistence in /var/lib/nats and caps how much memory and disk streams may use.
  • admin has no permissions block, so it can use any subject. orders can only publish and subscribe under orders., plus _INBOX.>, which it needs to receive replies to its own requests.

Restrict access to the file, since it contains the password hashes:

sudo chown root:nats /etc/nats/nats-server.conf
sudo chmod 640 /etc/nats/nats-server.conf

Check the syntax before starting the server:

sudo nats-server -t -c /etc/nats/nats-server.conf
nats-server: configuration file /etc/nats/nats-server.conf is valid

Step 4 - Running NATS as a systemd service

Create a unit file:

sudo nano /etc/systemd/system/nats-server.service
[Unit]
Description=NATS Server
Documentation=https://docs.nats.io
Wants=network-online.target
After=network-online.target

[Service]
Type=simple
User=nats
Group=nats
ExecStart=/usr/local/bin/nats-server -c /etc/nats/nats-server.conf
ExecReload=/bin/kill -s HUP $MAINPID
Restart=on-failure
RestartSec=5
LimitNOFILE=65536
# JetStream flushes to disk on shutdown; give it time
TimeoutStopSec=60
KillSignal=SIGINT

[Install]
WantedBy=multi-user.target

KillSignal=SIGINT makes NATS perform a clean shutdown, and ExecReload lets you apply configuration changes (users, permissions) with systemctl reload without dropping clients.

Start and enable the service:

sudo systemctl daemon-reload
sudo systemctl enable --now nats-server
sudo systemctl status nats-server --no-pager
● nats-server.service - NATS Server
     Loaded: loaded (/etc/systemd/system/nats-server.service; enabled; preset: enabled)
     Active: active (running) since ...

Query the health endpoint, which also confirms that JetStream started correctly:

curl -s http://127.0.0.1:8222/healthz
{"status":"ok"}

The server logs to the journal. Follow it with sudo journalctl -u nats-server -f.

If applications on other servers must connect, open port 4222 only to them. Replace 10.10.0.0/24 with your application subnet:

sudo ufw allow from 10.10.0.0/24 to any port 4222 proto tcp

Step 5 - Saving CLI contexts

Instead of typing the URL and credentials on every command, store them in nats CLI contexts. Create one for each user and select orders as the default:

nats context save admin --server nats://127.0.0.1:4222 --user admin --password 'admin_password'
nats context save orders --server nats://127.0.0.1:4222 --user orders --password 'orders_password' --select

Replace admin_password and orders_password with the passwords you hashed in step 3. Contexts are saved in ~/.config/nats/context/. Test the connection:

nats server check connection
OK Connection OK:connected to nats://127.0.0.1:4222 in ...

Step 6 - Testing publish-subscribe

In core NATS, a message published to a subject is delivered to every subscriber listening at that moment. Subjects are dot-separated, * matches one token and > matches one or more trailing tokens. Open a subscriber in one terminal:

nats sub 'orders.>'

In a second terminal, publish two messages:

nats pub orders.created '{"id":1001}'
nats pub orders.shipped '{"id":1001}'

The subscriber prints both:

[#1] Received on "orders.created"
{"id":1001}

[#2] Received on "orders.shipped"
{"id":1001}

Now check that permissions are enforced. The orders user is not allowed to publish to billing.invoice:

nats pub billing.invoice 'test'

The server drops the message and the client receives a permissions violation error; the server log (sudo journalctl -u nats-server) records a Publish Violation for user orders on subject billing.invoice.

Queue groups

Subscribers that join the same queue group share the load instead of each getting a copy. Start this command in two terminals:

nats sub 'orders.created' --queue workers

Publish ten messages from a third terminal. Each one is delivered to only one of the two workers:

nats pub orders.created 'new order' --count 10

Step 7 - Testing request-reply

Request-reply is how services call each other over NATS: the requester publishes with a temporary reply subject and waits for one response. Start a responder that answers every request on orders.status:

nats reply orders.status 'shipped'

In another terminal, send a request:

nats request orders.status '{"id":1001}'
Sending request on "orders.status"
Received with rtt 412µs
shipped

If no responder is running, the request fails immediately with no responders available for request instead of waiting for the timeout.

Step 8 - Persisting messages with JetStream

Core NATS messages are lost if no one is subscribed. A JetStream stream captures every message published to its subjects and stores it on disk, so consumers can read it later or replay it. Use the admin context to create a stream for all orders.> subjects, kept for 30 days:

nats --context admin stream add ORDERS \
  --subjects 'orders.>' --storage file --retention limits \
  --max-age 30d --replicas 1 --discard old --defaults

--defaults accepts default values for everything not given on the command line. Publish a few messages, then inspect the stream:

nats pub orders.created 'new order' --count 5
nats --context admin stream info ORDERS
State:

             Messages: 5
                Bytes: 245 B
       First Sequence: 1 @ ...
        Last Sequence: 5 @ ...

Create a durable pull consumer. It remembers which messages have been acknowledged, so a worker can stop and resume where it left off:

nats --context admin consumer add ORDERS billing \
  --pull --deliver all --ack explicit --defaults

Fetch and acknowledge two messages:

nats --context admin consumer next ORDERS billing --count 2
[..] subj: orders.created / tries: 1 / cons seq: 1 / str seq: 1 / pending: 4
new order
Acknowledged message
...

Run the command again and it continues with message 3. That is the key difference from core pub-sub: the stream holds messages until the retention limits remove them, and each consumer tracks its own position.

Step 9 - Connecting from an application

Official client libraries exist for Go, Python, JavaScript, Java, .NET, Rust and more. As an example, a Python publisher uses the nats-py package. Create a virtual environment and install it:

sudo apt install -y python3-venv
python3 -m venv ~/nats-demo
~/nats-demo/bin/pip install nats-py

Create a short script:

nano ~/publish.py
import asyncio
import json

import nats


async def main():
    nc = await nats.connect("nats://127.0.0.1:4222", user="orders", password="orders_password")
    js = nc.jetstream()
    ack = await js.publish("orders.created", json.dumps({"id": 1002}).encode())
    print(f"Stored in stream {ack.stream} with sequence {ack.seq}")
    await nc.drain()


asyncio.run(main())

Run it:

~/nats-demo/bin/python ~/publish.py
Stored in stream ORDERS with sequence 6

js.publish waits for the server to confirm the message is stored, unlike a plain nc.publish.

Step 10 - Clustering for high availability (optional)

A single server is a single point of failure. For production, run three servers and join them with cluster routes. On each node, add a cluster block to /etc/nats/nats-server.conf and give each node a unique server_name (nats-1, nats-2, nats-3):

cluster {
  name: orders-cluster
  listen: 0.0.0.0:6222
  authorization {
    user: route
    password: "your_route_password"
  }
  routes: [
    nats-route://route:[email protected]:6222
    nats-route://route:[email protected]:6222
    nats-route://route:[email protected]:6222
  ]
}

Open port 6222 between the nodes only:

sudo ufw allow from 10.10.0.0/24 to any port 6222 proto tcp

Reload or restart each server, then check the routes on any of them:

curl -s http://127.0.0.1:8222/routez | jq '.num_routes'
2

Each node should report two routes. JetStream forms its own Raft group across the three servers, and new streams can then use --replicas 3 so they survive the loss of one node.

Troubleshooting

nats: error: nats: Authorization Violation. The username or password in the context does not match the configuration. Check the hash was pasted inside double quotes and reload the server with sudo systemctl reload nats-server.

nats: error: nats: no responders available for request on stream add. JetStream is not enabled for the connecting user, or the server failed to start it. Check sudo journalctl -u nats-server for JetStream errors, usually a permission problem on /var/lib/nats.

The service fails to start after an edit. Run sudo nats-server -t -c /etc/nats/nats-server.conf to find the line with the syntax error.

Conclusion

You installed NATS Server on Ubuntu 24.04, secured it with hashed passwords and per-user subject permissions, and tested publish-subscribe, queue groups, request-reply and persistent JetStream streams. Next, enable TLS on the client port before accepting connections over the internet, grow the setup into a three-node cluster with replicated streams, and scrape the monitoring endpoint on port 8222 from your metrics system.