Skip to main contentSkip to navigation
[email protected]
Client AreaSupport
Hosting Mammoth
HostingMammothYour Data, Our Responsibility
Home
Solutions
Hosting Services
Store
Pricing
About
Blog
API
Contact

Stay Ahead of the Curve

Get the latest insights on cybersecurity, AI innovations, and enterprise data solutions delivered to your inbox.

Hosting Mammoth
HostingMammothEnterprise Solutions

Enterprise-grade data solutions. Hosting, recovery, cybersecurity, and AI-powered services for businesses worldwide.

[email protected]
Sun - Fri, 9:00am - 5:00pm

Services

  • Cloud Hosting
  • Data Recovery
  • Cybersecurity
  • Legal Support
  • MSP Services
  • Web Development
  • AI Services
  • Free Server Migration

Hosting

  • VPS Hosting (NVMe SSD)
  • VDS Hosting (NVMe)
  • Storage VPS (High SSD)
  • GPU Servers
  • Managed Services
  • Cloud Firewall
  • Load Balancer
  • One-Click Apps
  • n8n Hosting
  • Object Storage
  • FAQ

Company

  • Store
  • Pricing
  • About Us
  • Locations
  • Blog
  • Testimonials
  • Contact
  • Affiliate Program
  • White-Label
  • Terms of Service
  • Privacy Policy
  • Browser Cookies
  • SLA

Support

  • Client Area
  • Submit Ticket
  • Knowledge Base
  • Server Status
  • API Documentation

© 2026 Hosting Mammoth. All rights reserved.

Knowledge Base
Getting StartedAccount ManagementVPS HostingGPU ServersStorage VPSCloud FirewallLoad BalancerServer ManagementBilling & PaymentsSupport & TicketsAffiliate ProgramReseller ProgramMarketplace & Appsn8n HostingManaged ServicesServer MigrationAPI & DevelopersSecurityTroubleshootingGlossaryInstall Guides
  1. Home
  2. /
  3. Support
  4. /
  5. Install Guides
  6. /
  7. How To Install Nomad Ubuntu
GUIDEInstall Guides

How to Install HashiCorp Nomad on Ubuntu 24.04 VPS: Simpler Alternative to Kubernetes

29 min read

How to Install HashiCorp Nomad on Ubuntu 24.04 VPS: Simpler Alternative to Kubernetes

If you have ever tried to run a production Kubernetes cluster on a handful of VPS nodes, you know the pain: control-plane components eating half your RAM, CNI plugins that break on kernel upgrades, certificate rotations, and a version skew policy that turns every upgrade into a weekend project. HashiCorp Nomad takes a different approach. It is a single 100 MB Go binary that orchestrates containers, VMs, Java applications, and raw executables with a single HCL configuration file. This guide walks you through installing a production-ready Nomad cluster on Ubuntu 24.04 — from apt repository setup to ACLs, autoscaling, and CSI-backed persistent volumes.

Skip the setup? Deploy Ubuntu 24.04 with the CloudCore Professional plan in 60 seconds and have your first Nomad server running in minutes.

Table of Contents

  • What is HashiCorp Nomad?
  • Why Choose Nomad Over Kubernetes?
  • Prerequisites
  • Step 1: Update System and Install Prerequisites
  • Step 2: Add the HashiCorp apt Repository
  • Step 3: Install Nomad
  • Step 4: Install Docker and Podman Task Drivers
  • Step 5: Write the Server nomad.hcl
  • Step 6: Write the Client nomad.hcl
  • Step 7: Start the Cluster and Verify Members
  • Step 8: Run Your First Jobs (Service, Batch, System)
  • Step 9: Add Consul for Service Discovery
  • Step 10: Integrate Vault for Secrets
  • Step 11: Enable ACLs
  • Step 12: Install the Nomad Autoscaler
  • Step 13: Register a CSI Plugin for Persistent Volumes
  • Troubleshooting
  • FAQ
  • Next Steps
  • What is HashiCorp Nomad?

    HashiCorp Nomad is a flexible workload orchestrator developed by the same team behind Terraform, Consul, and Vault. It schedules applications across a cluster of machines, making sure they run where they should, restart when they crash, and scale when demand changes. Unlike Kubernetes — which is container-centric — Nomad treats containers as just one of many possible workload types. Its pluggable task driver system means you can run Docker containers, Podman rootless containers, Java jars, QEMU virtual machines, raw binaries, and even Windows services through a single scheduler and API.

    Nomad uses HashiCorp's Raft consensus algorithm to maintain a highly available control plane across 3 or 5 server nodes, and gossip (SWIM) for membership and failure detection among clients. Jobs are declared in HCL (HashiCorp Configuration Language), the same syntax Terraform uses, and submitted through a REST API, CLI, or web UI. The scheduler evaluates constraints (which datacenters, which CPU architectures, which hardware features) and affinities (soft preferences) to place tasks on the best-fitting clients, then bin-packs containers to maximize utilization.

    A single Nomad cluster can span multiple datacenters and regions, and federation is built in — no service mesh tax, no extra control-plane components. This makes Nomad particularly attractive for edge deployments, multi-region SaaS, and hybrid-cloud setups where a single pane of glass across VPS providers matters more than the bells and whistles of CNCF-flavored Kubernetes.

    Why Choose Nomad Over Kubernetes?

    Kubernetes is the default answer in most conversations about container orchestration, but for small-to-medium teams running on VPS infrastructure, Nomad often delivers more value with far less operational cost:

    • One binary, one config file — Nomad ships as a single static binary (~100 MB). No etcd to manage separately, no API server, scheduler, and controller-manager processes to tune individually. The operational surface area is an order of magnitude smaller.
    • Runs anywhere — A fresh 2 GB VPS is enough to run a Nomad server. A Kubernetes control plane on 2 GB fights itself for RAM before you schedule a single pod.
    • Beyond containers — Need to run a Java application without a Dockerfile? A QEMU VM? A legacy Windows service? Nomad's task drivers handle all of them natively.
    • Predictable upgrades — Nomad has strong backward compatibility and a straightforward rolling-upgrade procedure. You will not find yourself deprecating PodSecurityPolicy or chasing CRD migrations across minor versions.
    • First-class HashiCorp stack integration — Consul for service discovery and mTLS, Vault for dynamic secrets, Terraform for infrastructure — everything speaks the same HCL and shares identity primitives.
    • Cost — A 3-server Nomad cluster fits comfortably on three CloudCore Professional VPS instances at EUR 19.99/month each. The equivalent Kubernetes managed offering from major clouds is rarely below EUR 70/month just for the control plane.
    • Simpler debugging — When something breaks in Nomad, nomad alloc logs and nomad alloc status cover 95% of cases. Kubernetes debugging frequently involves chasing across CNI, kube-proxy, CoreDNS, ingress controllers, and three layers of admission webhooks.
    If you have genuinely cluster-scale challenges — stateful distributed databases, hundreds of developers pushing services, or a mandate for a specific CNCF toolchain — Kubernetes is probably still the right choice. See our K3s install guide for a lightweight Kubernetes alternative. For everyone else, Nomad is the pragmatic pick.

    Prerequisites

    Before starting, make sure you have:

    • Three Ubuntu 24.04 LTS VPS instances with root or sudo access (for a production-grade Raft quorum)
    • SSH access to each server
    • At least 2 GB of RAM per server node (4 GB+ recommended for clients running real workloads)
    • A private network between nodes is strongly recommended — Nomad uses ports 4646 (HTTP), 4647 (RPC), and 4648 (Serf gossip)
    • Fully resolvable hostnames or a /etc/hosts file that points each node at the others
    Recommended Plan: CloudCore Professional
    >
    For a production 3-server Nomad cluster plus a couple of client nodes, we recommend the CloudCore Professional plan on each node:
    >
    - 6 vCPU cores
    - 12 GB RAM
    - 100 GB NVMe SSD
    - Private networking between instances
    - EUR 19.99/month per node
    >
    This gives you enough headroom for the control plane plus meaningful workloads on each client.

    Connect to each server via SSH to get started:

    bash
    ssh root@your-server-ip

    For this guide, we use three hosts: nomad-1, nomad-2, and nomad-3. All three will run both the server and client roles — a common topology for small clusters. For larger setups, separate roles onto dedicated machines.

    Step 1: Update System and Install Prerequisites

    Start by updating your package index and installing the tools needed to add the HashiCorp repository.

    Run this on every node:

    bash
    sudo apt update && sudo apt upgrade -y
    sudo apt install -y gnupg software-properties-common curl ca-certificates

    Expected output (abbreviated):

    text
    Hit:1 http://archive.ubuntu.com/ubuntu noble InRelease
    Reading package lists... Done
    Building dependency tree... Done
    gnupg is already the newest version (2.4.4-2ubuntu17).
    0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.

    If a new kernel was installed, reboot and reconnect:

    bash
    sudo reboot

    Step 2: Add the HashiCorp apt Repository

    HashiCorp maintains an official Debian/Ubuntu repository that serves signed Nomad, Consul, Vault, and Terraform packages. This is the recommended install method because it handles security updates through normal apt upgrade.

    Import the HashiCorp GPG key:

    bash
    wget -O- https://apt.releases.hashicorp.com/gpg | \
      sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg

    Add the repository to your sources list:

    bash
    echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
      https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \
      sudo tee /etc/apt/sources.list.d/hashicorp.list

    Update the package index to pick up the new repository:

    bash
    sudo apt update

    Expected output includes a line like:

    text
    Get:5 https://apt.releases.hashicorp.com noble InRelease [12.9 kB]

    Run these steps on every node.

    Step 3: Install Nomad

    With the repository in place, install Nomad itself:

    bash
    sudo apt install -y nomad

    Expected output:

    text
    The following NEW packages will be installed:
      nomad
    ...
    Setting up nomad (1.9.3-1) ...
    Created symlink /etc/systemd/system/multi-user.target.wants/nomad.service -> /usr/lib/systemd/system/nomad.service.

    Verify the installation:

    bash
    nomad --version

    Expected output:

    text
    Nomad v1.9.3
    BuildDate 2026-01-14T18:04:12Z
    Revision abcd1234...

    The package creates:

    • /usr/bin/nomad — the Nomad binary
    • /etc/nomad.d/ — configuration directory
    • /opt/nomad/ — data directory
    • /etc/systemd/system/nomad.service — systemd unit
    • A nomad system user
    Do not start Nomad yet — we need to write a proper configuration first.

    Step 4: Install Docker and Podman Task Drivers

    Nomad runs jobs through task drivers. The docker driver is built in; the podman driver is a separate plugin. Installing both lets workloads pick the right runtime per job.

    Install Docker Engine

    On every client node, install the official Docker packages:

    bash
    curl -fsSL https://get.docker.com | sudo sh
    sudo systemctl enable --now docker
    sudo usermod -aG docker nomad

    Verify Docker works:

    bash
    sudo docker run --rm hello-world

    Install Podman and the Nomad Podman Driver

    Podman is distributed in the Ubuntu universe repo:

    bash
    sudo apt install -y podman

    Enable the Podman socket, which the Nomad driver connects to:

    bash
    sudo systemctl enable --now podman.socket

    Download the Nomad Podman driver plugin:

    bash
    NOMAD_PODMAN_VERSION=0.6.2
    curl -Lo /tmp/nomad-driver-podman.zip \
      "https://releases.hashicorp.com/nomad-driver-podman/${NOMAD_PODMAN_VERSION}/nomad-driver-podman_${NOMAD_PODMAN_VERSION}_linux_amd64.zip"
    sudo unzip -o /tmp/nomad-driver-podman.zip -d /opt/nomad/plugins
    sudo chmod +x /opt/nomad/plugins/nomad-driver-podman

    Nomad will auto-discover any executable in /opt/nomad/plugins on startup.

    Step 5: Write the Server nomad.hcl

    Every Nomad node needs a configuration file. The server config enables the Raft-backed control plane. Create /etc/nomad.d/nomad.hcl on all three server nodes:

    bash
    sudo tee /etc/nomad.d/nomad.hcl > /dev/null <<'EOF'
    datacenter = "dc1"
    region     = "global"
    data_dir   = "/opt/nomad/data"
    bind_addr  = "0.0.0.0"

    advertise { http = "{{ GetPrivateIP }}" rpc = "{{ GetPrivateIP }}" serf = "{{ GetPrivateIP }}" }

    server { enabled = true bootstrap_expect = 3

    server_join { retry_join = ["nomad-1", "nomad-2", "nomad-3"] retry_max = 0 retry_interval = "15s" } }

    client { enabled = true

    host_volume "app-data" { path = "/srv/nomad-volumes/app-data" read_only = false } }

    plugin "docker" { config { allow_privileged = false volumes { enabled = true } } }

    plugin "nomad-driver-podman" { config { socket_path = "unix:///run/podman/podman.sock" } }

    telemetry { collection_interval = "10s" disable_hostname = true prometheus_metrics = true publish_allocation_metrics = true publish_node_metrics = true }

    ui_config { enabled = true } EOF

    Key settings explained:

    • bootstrap_expect = 3 — Tells Nomad to wait until exactly three servers have joined before electing a Raft leader. Critical to avoid split-brain during initial cluster bootstrap. Use 5 for a 5-server cluster; never use even numbers.
    • retry_join — Gossip bootstrap targets. Resolve nomad-1 etc. via DNS or /etc/hosts. Nomad will keep retrying forever (retry_max = 0) until all servers are reachable.
    • advertise with {{ GetPrivateIP }} — Go template helper that resolves to the private network IP. Essential on multi-NIC VPS servers where you do not want RPC traffic on the public interface.
    • host_volume — Declares a static mount path clients can expose to jobs without needing a CSI plugin.
    • ui_config { enabled = true } — Turns on the built-in web UI at http://<node>:4646/ui.
    Create the data and volume directories:

    bash
    sudo mkdir -p /opt/nomad/data /srv/nomad-volumes/app-data
    sudo chown -R nomad:nomad /opt/nomad /srv/nomad-volumes

    Step 6: Write the Client nomad.hcl

    If you have dedicated client-only nodes (no server role), the config is simpler. Create /etc/nomad.d/nomad.hcl on each client:

    bash
    sudo tee /etc/nomad.d/nomad.hcl > /dev/null <<'EOF'
    datacenter = "dc1"
    region     = "global"
    data_dir   = "/opt/nomad/data"
    bind_addr  = "0.0.0.0"

    advertise { http = "{{ GetPrivateIP }}" rpc = "{{ GetPrivateIP }}" serf = "{{ GetPrivateIP }}" }

    client { enabled = true

    servers = ["nomad-1:4647", "nomad-2:4647", "nomad-3:4647"]

    meta { workload_class = "general" gpu = "false" }

    host_volume "app-data" { path = "/srv/nomad-volumes/app-data" read_only = false } }

    plugin "docker" { config { allow_privileged = false } }

    plugin "nomad-driver-podman" { config { socket_path = "unix:///run/podman/podman.sock" } } EOF

    The meta block lets jobs target specific nodes using constraints:

    hcl
    constraint {
      attribute = "${meta.workload_class}"
      value     = "general"
    }

    This is the Nomad equivalent of Kubernetes node labels and nodeSelectors.

    Step 7: Start the Cluster and Verify Members

    On every server node, enable and start the systemd unit:

    bash
    sudo systemctl enable --now nomad

    Check the service status:

    bash
    sudo systemctl status nomad

    Expected output:

    text
    ● nomad.service - Nomad
         Loaded: loaded (/usr/lib/systemd/system/nomad.service; enabled; preset: enabled)
         Active: active (running) since Thu 2026-04-16 10:05:00 UTC; 15s ago
       Main PID: 2345 (nomad)
          Tasks: 12
         Memory: 68.3M
            CPU: 1.872s

    Once all three servers are up, verify the Raft quorum from any node:

    bash
    nomad server members

    Expected output:

    text
    Name              Address         Port  Status  Leader  Raft Version  Build  Datacenter  Region
    nomad-1.global    10.0.0.1        4648  alive   true    3             1.9.3  dc1         global
    nomad-2.global    10.0.0.2        4648  alive   false   3             1.9.3  dc1         global
    nomad-3.global    10.0.0.3        4648  alive   false   3             1.9.3  dc1         global

    One server should show Leader: true. Check the client nodes:

    bash
    nomad node status

    Expected output:

    text
    ID        DC   Name      Class   Drain  Eligibility  Status
    a1b2c3d4  dc1  nomad-1   <none>  false  eligible     ready
    e5f6g7h8  dc1  nomad-2   <none>  false  eligible     ready
    i9j0k1l2  dc1  nomad-3   <none>  false  eligible     ready

    All three nodes show ready. Your cluster is up. Open the UI at http://nomad-1:4646/ui to see the dashboard.

    Step 8: Run Your First Jobs (Service, Batch, System)

    Nomad supports three main job types, each with different lifecycle semantics.

    Service Job: Long-Running Web Application

    Create web.nomad.hcl:

    hcl
    job "web" {
      datacenters = ["dc1"]
      type        = "service"

    group "frontend" { count = 3

    network { port "http" { to = 80 } }

    service { name = "web" port = "http" provider = "nomad"

    check { type = "http" path = "/" interval = "10s" timeout = "2s" } }

    task "nginx" { driver = "docker"

    config { image = "nginx:1.27-alpine" ports = ["http"] }

    resources { cpu = 200 memory = 128 } } } }

    Submit the job:

    bash
    nomad job run web.nomad.hcl

    Expected output:

    text
    ==> 2026-04-16T10:10:00Z: Monitoring evaluation "abc123"
        2026-04-16T10:10:00Z: Evaluation triggered by job "web"
        2026-04-16T10:10:01Z: Evaluation within deployment: "def456"
        2026-04-16T10:10:01Z: Allocation "xyz789" created: node "a1b2c3d4", group "frontend"
        ...
    ==> 2026-04-16T10:10:03Z: Evaluation "abc123" finished with status "complete"

    Three nginx containers are now spread across the cluster. Check their status:

    bash
    nomad job status web

    Batch Job: One-Off Task

    Batch jobs run to completion. Create migrate.nomad.hcl:

    hcl
    job "db-migrate" {
      datacenters = ["dc1"]
      type        = "batch"

    group "migrate" { task "flyway" { driver = "docker"

    config { image = "flyway/flyway:10" command = "migrate" }

    env { FLYWAY_URL = "jdbc:postgresql://db.service.consul:5432/app" FLYWAY_USER = "app" FLYWAY_PASSWORD = "changeme" }

    resources { cpu = 100 memory = 256 } } } }

    Run it:

    bash
    nomad job run migrate.nomad.hcl

    When the task finishes, the allocation transitions to complete.

    System Job: One Allocation Per Client

    System jobs run exactly one allocation on every eligible client — perfect for log shippers and node-level agents. Create promtail.nomad.hcl:

    hcl
    job "promtail" {
      datacenters = ["dc1"]
      type        = "system"

    group "agent" { task "promtail" { driver = "docker"

    config { image = "grafana/promtail:3.2.0" args = ["-config.file=/etc/promtail/config.yaml"] volumes = [ "/var/log:/var/log:ro" ] }

    resources { cpu = 100 memory = 128 } } } }

    Constraints: Pinning Jobs to Specific Nodes

    Add a constraint block to target nodes by attribute:

    hcl
    constraint {
      attribute = "${attr.kernel.name}"
      value     = "linux"
    }

    constraint { attribute = "${meta.workload_class}" operator = "=" value = "gpu" }

    constraint { attribute = "${attr.unique.hostname}" operator = "regexp" value = "^nomad-[1-3]$" }

    Useful attributes: attr.cpu.arch, attr.kernel.version, attr.memory.totalbytes, plus every meta.* value you set in the client config. Run nomad node status -verbose <node-id> to see all attributes.

    Step 9: Add Consul for Service Discovery

    Nomad's built-in nomad service provider is fine for simple clusters, but Consul is the production-grade choice — it adds DNS resolution, cross-datacenter federation, mTLS service mesh, and a richer health-checking model.

    Install Consul from the same HashiCorp repo you already added in Step 2:

    bash
    sudo apt install -y consul

    Minimal Consul server config at /etc/consul.d/consul.hcl:

    hcl
    datacenter  = "dc1"
    data_dir    = "/opt/consul"
    server      = true
    bootstrap_expect = 3
    client_addr = "0.0.0.0"
    bind_addr   = "{{ GetPrivateIP }}"

    retry_join = ["nomad-1", "nomad-2", "nomad-3"]

    ui_config { enabled = true }

    Start Consul:

    bash
    sudo systemctl enable --now consul
    consul members

    Now tell Nomad to talk to Consul. Add to /etc/nomad.d/nomad.hcl:

    hcl
    consul {
      address = "127.0.0.1:8500"

    server_service_name = "nomad" client_service_name = "nomad-client" auto_advertise = true server_auto_join = true client_auto_join = true }

    Restart Nomad:

    bash
    sudo systemctl restart nomad

    Update job service stanzas to use the Consul provider:

    hcl
    service {
      name     = "web"
      port     = "http"
      provider = "consul"

    check { type = "http" path = "/health" interval = "10s" timeout = "2s" } }

    Now every registered service is DNS-resolvable from inside the cluster as <name>.service.consul. A job connecting to PostgreSQL can simply use postgres.service.consul:5432 and Consul will return a healthy instance via DNS round-robin.

    For a deep dive, see our companion Consul install guide.

    Step 10: Integrate Vault for Secrets

    Shipping secrets in plaintext env vars is a non-starter in production. HashiCorp Vault provides dynamic, short-lived credentials (database passwords, cloud API tokens, TLS certificates) that Nomad can inject into tasks on the fly.

    Install Vault from the HashiCorp repo:

    bash
    sudo apt install -y vault

    See our dedicated Vault install guide for the full secure bootstrap. Once Vault is unsealed and you have a token with policies to create child tokens, configure Nomad to use it.

    Add to /etc/nomad.d/nomad.hcl:

    hcl
    vault {
      enabled          = true
      address          = "http://vault.service.consul:8200"
      create_from_role = "nomad-cluster"
      token            = "<nomad-bootstrap-token>"
    }

    Restart Nomad. Now jobs can request secrets using a template stanza:

    hcl
    task "app" {
      driver = "docker"

    vault { policies = ["app-read"] change_mode = "restart" }

    template { data = <<-EOT {{ with secret "database/creds/app-role" }} DB_USER={{ .Data.username }} DB_PASS={{ .Data.password }} {{ end }} EOT

    destination = "secrets/env" env = true change_mode = "restart" }

    config { image = "myapp:v1.2.0" } }

    Nomad fetches a unique database credential from Vault, writes it to the task's secrets/ directory (tmpfs-backed, encrypted at rest), and restarts the task when the lease is about to expire. Secrets never appear in the job file, in Nomad's state store, or in container images.

    Step 11: Enable ACLs

    By default, any API call to Nomad is unauthenticated. For production, enable ACLs to require tokens for every operation.

    Add to /etc/nomad.d/nomad.hcl on all servers:

    hcl
    acl {
      enabled = true
    }

    Restart Nomad on each server one at a time (rolling):

    bash
    sudo systemctl restart nomad

    Bootstrap the root ACL token on the leader:

    bash
    nomad acl bootstrap

    Expected output:

    text
    Accessor ID  = 1111aaaa-2222-3333-4444-555566667777
    Secret ID    = 8888bbbb-9999-0000-cccc-ddddeeeeffff
    Name         = Bootstrap Token
    Type         = management
    Global       = true
    Policies     = n/a
    Create Time  = 2026-04-16 10:30:00 +0000 UTC
    Expiry Time  = <none>

    Save the Secret ID somewhere safe — this is your root token. Losing it requires a cluster reset.

    Export it for CLI use:

    bash
    export NOMAD_TOKEN="8888bbbb-9999-0000-cccc-ddddeeeeffff"

    Create a scoped policy for developers (dev-policy.hcl):

    hcl
    namespace "default" {
      policy       = "write"
      capabilities = ["submit-job", "dispatch-job", "read-logs"]
    }

    agent { policy = "read" }

    node { policy = "read" }

    Apply and mint a token:

    bash
    nomad acl policy apply -description "Developer policy" dev dev-policy.hcl
    nomad acl token create -name="alice" -policy=dev -type=client

    Distribute the token's Secret ID to the developer. They set NOMAD_TOKEN in their shell and can submit jobs but cannot modify ACLs or drain nodes.

    Step 12: Install the Nomad Autoscaler

    The Nomad Autoscaler handles both horizontal job scaling (adding allocations when load rises) and cluster scaling (adding or removing client nodes via cloud APIs).

    Download the latest release:

    bash
    NOMAD_AUTOSCALER_VERSION=0.4.6
    curl -Lo /tmp/nomad-autoscaler.zip \
      "https://releases.hashicorp.com/nomad-autoscaler/${NOMAD_AUTOSCALER_VERSION}/nomad-autoscaler_${NOMAD_AUTOSCALER_VERSION}_linux_amd64.zip"
    sudo unzip -o /tmp/nomad-autoscaler.zip -d /usr/local/bin/
    sudo chmod +x /usr/local/bin/nomad-autoscaler

    Create /etc/nomad-autoscaler/config.hcl:

    hcl
    nomad {
      address = "http://127.0.0.1:4646"
      token   = "<autoscaler-acl-token>"
    }

    apm "prometheus" { driver = "prometheus" config = { address = "http://prometheus.service.consul:9090" } }

    strategy "target-value" { driver = "target-value" }

    Run the autoscaler as a Nomad job so Nomad manages Nomad:

    hcl
    job "autoscaler" {
      datacenters = ["dc1"]
      type        = "service"

    group "autoscaler" { count = 1

    task "autoscaler" { driver = "exec"

    config { command = "/usr/local/bin/nomad-autoscaler" args = ["agent", "-config=/etc/nomad-autoscaler/config.hcl"] }

    resources { cpu = 200 memory = 256 } } } }

    Then add a scaling block to any job you want autoscaled:

    hcl
    group "frontend" {
      count = 3

    scaling { enabled = true min = 2 max = 20

    policy { cooldown = "1m" evaluation_interval = "30s"

    check "avg_cpu" { source = "prometheus" query = "avg(nomad_client_allocs_cpu_total_percent{task_group=\"frontend\"})"

    strategy "target-value" { target = 70 } } } } }

    This keeps average CPU utilization across the frontend group at 70%, scaling between 2 and 20 allocations.

    Step 13: Register a CSI Plugin for Persistent Volumes

    Stateful workloads — databases, queues, file stores — need persistent storage that outlives any single allocation. Nomad supports the CSI (Container Storage Interface) standard used by Kubernetes, so most cloud and on-prem storage backends work out of the box.

    For VPS deployments, Democratic CSI with NFS or iSCSI is a popular choice. Here is a minimal AWS EBS example to show the pattern:

    Register the CSI controller plugin (one instance cluster-wide):

    hcl
    job "ebs-controller" {
      datacenters = ["dc1"]
      type        = "service"

    group "controller" { task "plugin" { driver = "docker"

    config { image = "public.ecr.aws/ebs-csi-driver/aws-ebs-csi-driver:v1.35.0" args = [ "controller", "--endpoint=${CSI_ENDPOINT}", "--logtostderr", "--v=5" ] privileged = true }

    csi_plugin { id = "aws-ebs0" type = "controller" mount_dir = "/csi" }

    resources { cpu = 200 memory = 256 } } } }

    Register the node plugin as a system job so every client can attach volumes:

    hcl
    job "ebs-nodes" {
      datacenters = ["dc1"]
      type        = "system"

    group "nodes" { task "plugin" { driver = "docker"

    config { image = "public.ecr.aws/ebs-csi-driver/aws-ebs-csi-driver:v1.35.0" args = ["node", "--endpoint=${CSI_ENDPOINT}"] privileged = true }

    csi_plugin { id = "aws-ebs0" type = "node" mount_dir = "/csi" }

    resources { cpu = 100 memory = 128 } } } }

    Create a volume spec (postgres-volume.hcl):

    hcl
    id           = "postgres-data"
    name         = "postgres-data"
    type         = "csi"
    plugin_id    = "aws-ebs0"
    capacity_min = "10GiB"
    capacity_max = "20GiB"

    capability { access_mode = "single-node-writer" attachment_mode = "file-system" }

    Create the volume:

    bash
    nomad volume create postgres-volume.hcl

    Mount it in a job:

    hcl
    group "db" {
      volume "data" {
        type            = "csi"
        source          = "postgres-data"
        read_only       = false
        attachment_mode = "file-system"
        access_mode     = "single-node-writer"
      }

    task "postgres" { driver = "docker"

    volume_mount { volume = "data" destination = "/var/lib/postgresql/data" }

    config { image = "postgres:16-alpine" } } }

    Nomad coordinates with the CSI controller to attach the EBS volume to whichever client receives the allocation — and re-attaches it during rescheduling, so the data follows the workload.

    Troubleshooting

    ProblemCauseSolution
    No cluster leader error for more than 30 secondsRaft quorum not met — fewer than bootstrap_expect servers reachableCheck nomad server members. Verify network connectivity on port 4648 (serf) and 4647 (rpc) between servers.
    Client shows down in nomad node statusAgent crashed or lost connectivity to serverssudo journalctl -u nomad -n 100. Check the client's servers list resolves correctly.
    driver "docker" healthy but no allocations runNomad user is not in the docker groupsudo usermod -aG docker nomad && sudo systemctl restart nomad.
    Permission denied from Podman driverPodman socket not running or wrong pathsudo systemctl status podman.socket. Verify socket_path in the plugin config matches the actual socket.
    Job stuck in pending foreverNo client satisfies constraintsnomad job status <job> and then nomad eval status <eval-id> to see placement failures. Relax constraints or add a matching client.
    ACL token not found after restartACLs enabled but no token providedExport NOMAD_TOKEN in your shell or pass -token on the CLI.
    CSI volume never becomes availableController plugin can't reach storage backendnomad plugin status <plugin-id>. Check controller logs: nomad alloc logs <alloc-id>.

    Viewing Logs

    Nomad logs through systemd-journald:

    bash
    sudo journalctl -u nomad -f

    Per-allocation logs:

    bash
    nomad alloc logs -f <alloc-id>
    nomad alloc logs -f -stderr <alloc-id>

    Detailed job placement diagnostics:

    bash
    nomad job status -verbose <job-name>
    nomad eval status -verbose <eval-id>

    FAQ

    Is Nomad a good alternative to Kubernetes?

    For the majority of small-to-medium workloads running on VPS infrastructure, yes. Nomad is a single 100 MB binary that orchestrates containers, VMs, Java jars, and raw executables with a fraction of the operational overhead of Kubernetes. A 3-node Nomad cluster runs comfortably on VPS-sized servers, while a comparable Kubernetes cluster typically needs dedicated control plane resources, CNI plugins, and constant upgrade churn. Kubernetes makes sense when you need the sprawling CNCF ecosystem (Istio, Knative, CRDs for every concern) or when your organization already runs Kubernetes elsewhere and consolidation beats diversity. For everyone else, Nomad delivers 90% of the orchestration value at 10% of the operational cost.

    What are the minimum requirements for a Nomad server?

    Each Nomad server needs about 2 GB RAM, 2 vCPUs, and 20 GB disk for the Raft state and event logs. In production, run 3 or 5 servers to form a proper Raft quorum (never use even numbers — they provide no additional fault tolerance and increase the risk of split brain). Clients (worker nodes) scale with the workloads they run: plan 4-8 GB RAM per client for typical microservice stacks, more for memory-heavy databases or JVM applications. The CloudCore Professional plan at 6 vCPU and 12 GB RAM handles both roles comfortably for small clusters.

    How does Nomad handle service discovery?

    Nomad has two options. The built-in native service discovery (set provider = "nomad" on a service stanza) is simple and requires no extra software — fine for small clusters. For production, most operators integrate HashiCorp Consul, which adds DNS-based service resolution (myservice.service.consul), richer health checks, cross-datacenter federation, and optional mTLS service mesh via Consul Connect. Consul's DNS interface means any application — even one without Nomad integration — can discover services just by resolving a hostname.

    Can Nomad run Docker and Podman containers on the same node?

    Yes. Nomad's task driver system is pluggable and drivers coexist peacefully. The docker driver ships built-in, and the community nomad-driver-podman plugin runs alongside it. This is useful when you want rootless Podman containers for security-sensitive workloads while keeping Docker for convenience or legacy images. Each job specifies its preferred driver, and the scheduler only places tasks on clients where that driver is healthy.

    Does Nomad support persistent storage for stateful workloads?

    Yes. Nomad supports the CSI (Container Storage Interface) standard — the same abstraction Kubernetes uses — so most cloud block storage (AWS EBS, GCP Persistent Disk, Azure Disk) and on-prem solutions (Ceph, Longhorn, Democratic CSI, NFS) plug in with minimal configuration. For simpler setups, host volumes declared in the client config expose a static filesystem path to jobs without needing a CSI plugin. The combination covers everything from single-node PostgreSQL deployments to dynamically provisioned replicated volumes for HA databases.

    How do Nomad ACLs compare to Kubernetes RBAC?

    Nomad's ACL system is policy-driven, using HCL policies that scope capabilities by namespace, node, agent, quota, plugin, and more. Tokens are issued against policies and can be management (root-like) or client (policy-scoped). It is simpler than Kubernetes RBAC — no separate Role, RoleBinding, ClusterRole, ClusterRoleBinding, and ServiceAccount primitives — while covering the same fundamental use cases. Combined with Vault integration for workload identity, you get end-to-end authentication and authorization without stitching together half a dozen CNCF projects.

    Can I run Nomad alongside an existing Kubernetes cluster?

    Yes — many organizations do exactly this. Nomad excels at batch workloads, scheduled jobs, edge deployments, and legacy non-containerized applications. Kubernetes handles the rest. Both can share Consul for service discovery and Vault for secrets, giving developers a unified identity and networking story across both platforms.

    Next Steps

    Now that you have a working Nomad cluster, here is how to build on it:

    • Layer on Consul service mesh — Follow our Consul install guide and turn on Consul Connect for zero-trust mTLS between every job in the cluster without changing a line of application code.
    • Add Vault for dynamic secrets — Our Vault install guide walks through unsealing, auth methods, and the database secrets engine so your jobs get short-lived credentials minted on demand.
    • Compare with lightweight Kubernetes — Curious how K3s stacks up? The K3s install guide shows the same cluster pattern on Rancher's minimal Kubernetes distribution so you can pick the right tool for each workload.
    • Wire up observability — Deploy Prometheus and Grafana as system jobs to scrape Nomad's /v1/metrics endpoint. With the telemetry block enabled, you get detailed scheduler, Raft, and allocation metrics out of the box.
    • Enable Sentinel policies (Enterprise) — On Nomad Enterprise, Sentinel lets you codify governance ("no job may run as root", "all jobs must declare resource limits") as policies enforced at submission time.
    • Explore the ecosystem — The HashiCorp Nomad documentation has deep dives on task drivers, autoscaling strategies, multi-region federation, and gossip encryption that go well beyond this install guide.

    Deploy Nomad on CloudCore Professional
    >
    Spin up three Ubuntu 24.04 VPS instances and have a production-ready Nomad cluster running in under an hour.
    >
    - 6 vCPU, 12 GB RAM, 100 GB NVMe SSD per node
    - Private networking for secure Raft and gossip traffic
    - Root access and full control over the stack
    - EUR 19.99/month per node — unmetered bandwidth included
    >
    Launch your Nomad cluster now on CloudCore Professional.

    Was this article helpful?

    ← Back to Install GuidesBrowse all categories →

    Still have questions?

    Contact Support →Submit a Ticket