How to Deploy NATS on Vultr Kubernetes Engine

Updated on 11 August, 2026
Learn how to deploy NATS messaging system on Vultr Kubernetes Engine with step-by-step instructions for configuration, installation, and verification.
How to Deploy NATS on Vultr Kubernetes Engine header image

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:

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.

  1. Add the NATS Helm repository.

    console
    $ helm repo add nats https://nats-io.github.io/k8s/helm/charts/
    
  2. Update the repository cache.

    console
    $ helm repo update
    
  3. Install NATS into your cluster.

    console
    $ helm install nats nats/nats
    
  4. View the NATS pods.

    console
    $ kubectl get pods -l=app.kubernetes.io/name=nats
    

    Wait until the pods report a Running status.

    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.

  1. Switch to your home directory.

    console
    $ cd
    
  2. Create the project directory.

    console
    $ mkdir nats-vke
    
  3. Create the consumer application directory inside it.

    console
    $ mkdir nats-vke/nats-consumer
    
  4. Switch to the consumer directory.

    console
    $ cd nats-vke/nats-consumer
    
  5. Initialize a new Go module.

    console
    $ go mod init nats-consumer
    
  6. Create the application source file.

    console
    $ nano consumer.go
    
  7. Add the following code.

    go
    package 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.

  1. Create the Dockerfile.

    console
    $ nano Dockerfile
    
  2. Add the following configuration.

    dockerfile
    FROM 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-buster to compile the consumer binary.
    • The second stage uses gcr.io/distroless/base-debian10 and copies in the binary produced by the first stage.
  3. Resolve the Go modules and create the go.sum file.

    console
    $ go mod tidy
    
  4. Verify the directory contents.

    console
    $ ls
    

    The output lists the source file, the Dockerfile, and both module files.

    consumer.go  Dockerfile  go.mod  go.sum
  5. Log in to your Vultr Container Registry. Replace example with your registry name.

    console
    $ docker login https://sjc.vultrcr.com/example
    

    Enter your registry username and password when prompted.

  6. Build the consumer image. Replace example with your registry name.

    console
    $ docker build -t sjc.vultrcr.com/example/nats-consumer:latest .
    
  7. Push the image to the registry. Replace example with 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.

  1. Create the deployment manifest.

    console
    $ nano consumer.yaml
    
  2. Add the following configuration. Replace sjc.vultrcr.com/example with your registry URL.

    yaml
    apiVersion: 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 the nats Service 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.
  3. Apply the manifest.

    console
    $ kubectl apply -f consumer.yaml
    
  4. Verify that the consumer pod is running.

    console
    $ kubectl get pods -l=app=nats-consumer
    

    Verify that the pod reports a Running status.

    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.

  1. Switch to the project directory.

    console
    $ cd ~/nats-vke
    
  2. Create the producer application directory.

    console
    $ mkdir nats-producer
    
  3. Switch to the directory.

    console
    $ cd nats-producer
    
  4. Initialize a new Go module.

    console
    $ go mod init nats-producer
    
  5. Create the application source file.

    console
    $ nano producer.go
    
  6. Add the following code.

    go
    package 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 SIGTERM signal.

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.

  1. Create the Dockerfile.

    console
    $ nano Dockerfile
    
  2. Add the following configuration.

    dockerfile
    FROM 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-buster to compile the producer binary.
    • The second stage uses gcr.io/distroless/base-debian10 and copies in the binary produced by the first stage.
  3. Resolve the Go modules and create the go.sum file.

    console
    $ go mod tidy
    
  4. Build the producer image. Replace example with your registry name.

    console
    $ docker build -t sjc.vultrcr.com/example/nats-producer-app:latest .
    
  5. Push the image to the registry. Replace example with 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.

  1. Create the deployment manifest.

    console
    $ nano producer.yaml
    
  2. Add the following configuration. Replace sjc.vultrcr.com/example with your registry URL.

    yaml
    apiVersion: 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.

  3. Apply the manifest.

    console
    $ kubectl apply -f producer.yaml
    
  4. 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           6h5m
  5. Verify that the producer pod is running.

    console
    $ kubectl get pods -l=app=nats-producer
    

    Verify that the pod reports a Running status.

    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.

  1. 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-12

    Press Ctrl + C to stop following the log.

  2. 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-12

    Press Ctrl + C to stop following the log.

  3. Scale the consumer to two replicas.

    console
    $ kubectl scale deployment/nats-consumer --replicas=2
    
  4. 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          6h33m
  5. Follow 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-23

    Press Ctrl + C to stop following the log.

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

Comments