How to Deploy NATS on Vultr Kubernetes Engine

NATS is an open-source, lightweight, high-performance messaging system for building distributed and scalable applications. It supports publish-subscribe, queuing, and request-reply patterns commonly used in cloud-native and microservice architectures. In the publish-subscribe pattern, a publisher sends a message on a subject and every active subscriber listening on that subject receives it. In the queuing pattern, subscribers register as part of a queue group, and only one randomly selected member of the group consumes each message.
This guide explains how to deploy a NATS cluster on a Vultr Kubernetes Engine (VKE) cluster using Helm. It covers installing the NATS chart, building Go producer and consumer applications into container images, storing those images in a Vultr Container Registry, deploying both applications to the cluster, and scaling the consumer to demonstrate how queue-based messaging distributes load.
Prerequisites
Before you begin, you need to:
- Deploy a Vultr Kubernetes Engine cluster with at least 3 nodes.
- Deploy a Docker One-Click instance to use as the management server, and access it over SSH as a non-root user with sudo privileges.
- Create a Vultr Container Registry to store the private application images.
- Install and configure kubectl on the management server with access to the cluster.
- Install the Helm CLI on the management server.
- Install Go on the management server.
Install NATS
The NATS project publishes an official Helm chart that deploys the server together with a nats-box utility pod for administrative commands. Installing the chart creates the messaging backbone that both application deployments connect to.
Add the NATS Helm repository.
console$ helm repo add nats https://nats-io.github.io/k8s/helm/charts/
Update the repository cache.
console$ helm repo update
Install NATS into your cluster.
console$ helm install nats nats/nats
View the NATS pods.
console$ kubectl get pods -l=app.kubernetes.io/name=nats
Wait until the pods report a
Runningstatus.NAME READY STATUS RESTARTS AGE nats-0 2/2 Running 0 25s nats-box-7ffb855bbb-dhtvk 1/1 Running 0 25s
Create the NATS Consumer Application
The consumer subscribes to a subject as part of a queue group. Running more than one replica of this application later demonstrates how NATS distributes messages across a group instead of delivering every message to every subscriber.
Switch to your home directory.
console$ cd
Create the project directory.
console$ mkdir nats-vke
Create the consumer application directory inside it.
console$ mkdir nats-vke/nats-consumer
Switch to the consumer directory.
console$ cd nats-vke/nats-consumer
Initialize a new Go module.
console$ go mod init nats-consumer
Create the application source file.
console$ nano consumer.go
Add the following code.
gopackage main import ( "fmt" "log" "os" "os/signal" "syscall" "github.com/nats-io/nats.go" ) func main() { natsServer := os.Getenv("NATS_SERVER") if natsServer == "" { log.Fatal("missing NATS_SERVER env variable") } subject := os.Getenv("NATS_SUBJECT") if subject == "" { log.Fatal("missing NATS_SUBJECT env variable") } queueGroup := os.Getenv("NATS_QUEUE_GROUP") if queueGroup == "" { log.Fatal("missing NATS_QUEUE_GROUP env variable") } nc, err := nats.Connect(natsServer) if err != nil { log.Fatalf("Error connecting to NATS: %v", err) } fmt.Println("successfully connected to", natsServer) defer nc.Close() _, err = nc.QueueSubscribe(subject, queueGroup, func(msg *nats.Msg) { log.Printf("Received message on subject %s: %s", msg.Subject, string(msg.Data)) }) if err != nil { log.Fatalf("Error subscribing to subject: %v", err) } log.Printf("Subscribed to subject %s within queue group %s", subject, queueGroup) waitForSignal() } func waitForSignal() { sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) <-sigCh log.Println("Received termination signal. Shutting down...") }
Save and close the file.
The application performs the following actions in order:
- Reads the required environment variables for the NATS server, subject, and queue group.
- Connects to the NATS server.
- Subscribes to the subject as a member of the queue group.
- Logs each received message to the console through the message handler.
Build the Consumer Container Image
A two-stage Dockerfile compiles the Go binary in a build image and copies it into a minimal distroless runtime image, which keeps the published image small and free of a shell and package manager.
Create the Dockerfile.
console$ nano Dockerfile
Add the following configuration.
dockerfileFROM golang:1.18-buster AS build WORKDIR /app COPY go.mod ./ COPY go.sum ./ RUN go mod download COPY consumer.go ./ RUN go build -o /nats-consumer-app FROM gcr.io/distroless/base-debian10 WORKDIR / COPY --from=build /nats-consumer-app /nats-consumer-app EXPOSE 8080 USER nonroot:nonroot ENTRYPOINT ["/nats-consumer-app"]
Save and close the file.
- The first stage uses
golang:1.18-busterto compile the consumer binary. - The second stage uses
gcr.io/distroless/base-debian10and copies in the binary produced by the first stage.
- The first stage uses
Resolve the Go modules and create the
go.sumfile.console$ go mod tidy
Verify the directory contents.
console$ lsThe output lists the source file, the Dockerfile, and both module files.
consumer.go Dockerfile go.mod go.sumLog in to your Vultr Container Registry. Replace
examplewith your registry name.console$ docker login https://sjc.vultrcr.com/example
Enter your registry username and password when prompted.
Build the consumer image. Replace
examplewith your registry name.console$ docker build -t sjc.vultrcr.com/example/nats-consumer:latest .
Push the image to the registry. Replace
examplewith your registry name.console$ docker push sjc.vultrcr.com/example/nats-consumer:latest
The output confirms each layer is pushed and reports the image digest.
The push refers to repository [sjc.vultrcr.com/example/nats-consumer] 5adb57ca5a3c: Pushed 91f7bcfdfda8: Pushed 05ef21d76315: Pushed latest: digest: sha256:1ee56100e7ba4274a8c33b4c49740bbd2f69e4f7f75461208b7d2854c07c63c5 size: 949
Deploy the Consumer Application
The Deployment passes the NATS server address, subject, and queue group to the container as environment variables, which the application reads at startup. The server address uses the nats Service that the Helm chart created.
Create the deployment manifest.
console$ nano consumer.yaml
Add the following configuration. Replace
sjc.vultrcr.com/examplewith your registry URL.yamlapiVersion: apps/v1 kind: Deployment metadata: name: nats-consumer spec: replicas: 1 selector: matchLabels: app: nats-consumer template: metadata: labels: app: nats-consumer spec: containers: - name: nats-consumer image: sjc.vultrcr.com/example/nats-consumer:latest imagePullPolicy: Always env: - name: NATS_SERVER value: nats://nats:4222 - name: NATS_SUBJECT value: vke-nats-demo-subject - name: NATS_QUEUE_GROUP value: vke-nats-demo-queue
Save and close the file.
NATS_SERVER: Points at thenatsService created by the Helm chart on the default client port.NATS_QUEUE_GROUP: Places every replica of this Deployment in the same queue group so that NATS delivers each message to only one of them.
Apply the manifest.
console$ kubectl apply -f consumer.yaml
Verify that the consumer pod is running.
console$ kubectl get pods -l=app=nats-consumer
Verify that the pod reports a
Runningstatus.NAME READY STATUS RESTARTS AGE nats-consumer-746f5ddf75-tzmxs 1/1 Running 0 12s
Create the NATS Producer Application
The producer publishes a numbered message to the same subject every three seconds, which provides a steady stream to observe when testing how the consumers share the load.
Switch to the project directory.
console$ cd ~/nats-vke
Create the producer application directory.
console$ mkdir nats-producer
Switch to the directory.
console$ cd nats-producer
Initialize a new Go module.
console$ go mod init nats-producer
Create the application source file.
console$ nano producer.go
Add the following code.
gopackage main import ( "fmt" "log" "os" "os/signal" "syscall" "time" "github.com/nats-io/nats.go" ) func main() { natsServer := os.Getenv("NATS_SERVER") if natsServer == "" { log.Fatal("missing NATS_SERVER env variable") } subject := os.Getenv("NATS_SUBJECT") if subject == "" { log.Fatal("missing NATS_SUBJECT env variable") } nc, err := nats.Connect(natsServer) if err != nil { log.Fatalf("Error connecting to NATS: %v", err) } fmt.Println("successfully connected to", natsServer) defer nc.Close() c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { <-c fmt.Println("\nReceived termination signal. Exiting...") os.Exit(0) }() index := 0 for { message := fmt.Sprintf("message-%d", index) if err := nc.Publish(subject, []byte(message)); err != nil { log.Printf("Error publishing message: %v", err) } else { log.Printf("Published message: %s", message) } index++ time.Sleep(3 * time.Second) } }
Save and close the file.
The application performs the following actions in order:
- Reads the required environment variables for the NATS server and subject.
- Connects to the NATS server.
- Publishes messages to the subject in an infinite loop, waiting three seconds between each message.
- Shuts down gracefully in response to a
SIGTERMsignal.
Build the Producer Container Image
The producer uses the same two-stage build as the consumer, differing only in the source file it compiles and the resulting binary name.
Create the Dockerfile.
console$ nano Dockerfile
Add the following configuration.
dockerfileFROM golang:1.18-buster AS build WORKDIR /app COPY go.mod ./ COPY go.sum ./ RUN go mod download COPY producer.go ./ RUN go build -o /nats-producer-app FROM gcr.io/distroless/base-debian10 WORKDIR / COPY --from=build /nats-producer-app /nats-producer-app EXPOSE 8080 USER nonroot:nonroot ENTRYPOINT ["/nats-producer-app"]
Save and close the file.
- The first stage uses
golang:1.18-busterto compile the producer binary. - The second stage uses
gcr.io/distroless/base-debian10and copies in the binary produced by the first stage.
- The first stage uses
Resolve the Go modules and create the
go.sumfile.console$ go mod tidy
Build the producer image. Replace
examplewith your registry name.console$ docker build -t sjc.vultrcr.com/example/nats-producer-app:latest .
Push the image to the registry. Replace
examplewith your registry name.console$ docker push sjc.vultrcr.com/example/nats-producer-app:latest
Deploy the Producer Application
The producer Deployment reads the same NATS server address and subject as the consumer, but takes no queue group because it publishes rather than subscribes.
Create the deployment manifest.
console$ nano producer.yaml
Add the following configuration. Replace
sjc.vultrcr.com/examplewith your registry URL.yamlapiVersion: apps/v1 kind: Deployment metadata: name: nats-producer spec: replicas: 1 selector: matchLabels: app: nats-producer template: metadata: labels: app: nats-producer spec: containers: - name: nats-producer image: sjc.vultrcr.com/example/nats-producer-app:latest imagePullPolicy: Always env: - name: NATS_SERVER value: nats://nats:4222 - name: NATS_SUBJECT value: vke-nats-demo-subject
Save and close the file.
Apply the manifest.
console$ kubectl apply -f producer.yaml
Verify that the deployments are available.
console$ kubectl get deployments
The output lists the NATS utility pod alongside both application deployments.
NAME READY UP-TO-DATE AVAILABLE AGE nats-box 1/1 1 1 6h28m nats-consumer 1/1 1 1 6h15m nats-producer 1/1 1 1 6h5mVerify that the producer pod is running.
console$ kubectl get pods -l=app=nats-producer
Verify that the pod reports a
Runningstatus.NAME READY STATUS RESTARTS AGE nats-producer-842f5eef42-dfgz 1/1 Running 0 20s
Test the Messaging Operations
Following both application logs confirms that messages published by the producer reach the consumer through NATS. Scaling the consumer afterward shows the queue group distributing those messages instead of duplicating them.
Follow the producer logs.
console$ kubectl logs -f $(kubectl get pod -l=app=nats-producer -o jsonpath='{.items[0].metadata.name}')
The output displays a new published message every three seconds.
Published message: message-10 Published message: message-11 Published message: message-12Press
Ctrl+Cto stop following the log.Follow the consumer logs.
console$ kubectl logs -f $(kubectl get pod -l=app=nats-consumer -o jsonpath='{.items[0].metadata.name}')
The output displays the matching received messages.
Received message on subject vke-nats-demo-subject: message-10 Received message on subject vke-nats-demo-subject: message-11 Received message on subject vke-nats-demo-subject: message-12Press
Ctrl+Cto stop following the log.Scale the consumer to two replicas.
console$ kubectl scale deployment/nats-consumer --replicas=2
Verify that both consumer pods are running.
console$ kubectl get pods -l=app=nats-consumer
NAME READY STATUS RESTARTS AGE nats-consumer-6fb9d66968-bclj7 1/1 Running 0 6h19m nats-consumer-6fb9d66968-cgr95 1/1 Running 0 6h33mFollow the logs of the first consumer replica.
console$ kubectl logs -f $(kubectl get pod -l=app=nats-consumer -o jsonpath='{.items[0].metadata.name}')
The output displays only a subset of the message sequence.
Received message on subject vke-nats-demo-subject: message-17 Received message on subject vke-nats-demo-subject: message-20 Received message on subject vke-nats-demo-subject: message-23Press
Ctrl+Cto stop following the log.Follow the logs of the second consumer replica.
console$ kubectl logs -f $(kubectl get pod -l=app=nats-consumer -o jsonpath='{.items[1].metadata.name}')
The output displays the remaining messages in the sequence.
Received message on subject vke-nats-demo-subject: message-18 Received message on subject vke-nats-demo-subject: message-19 Received message on subject vke-nats-demo-subject: message-21
Each message reaches exactly one replica rather than both, because both pods belong to the same queue group. Distributing messages this way lets you scale message processing horizontally by adding replicas.
Conclusion
You have deployed NATS on a Vultr Kubernetes Engine cluster, published messages from a Go producer application, and consumed them with a scalable consumer application that shares work through a queue group. Extend the deployment by adding more subjects, enabling JetStream for persistent streams, or scaling the consumer further to match your processing needs. For more information, visit the official NATS documentation and the Vultr Kubernetes Engine documentation.