Licensing in 2026

HashiCorp moved Nomad to the BSL (Business Source License) in 2023. For internal use, BSL Nomad is free and unchanged in capability. The community fork is OpenNomad (MPL-2.0) where redistribution or competitive offering is a concern. The commands and HCL syntax are identical between the two; pick based on licensing comfort.

The three concepts to know

  • Job — the unit of work. A job has one or more groups; a group has one or more tasks. A task is the actual thing that runs.
  • Task driver — how the task runs: docker, podman, exec (raw process under cgroup isolation), raw_exec, java, qemu, plus community drivers for Firecracker, containerd, etc.
  • Allocation — one running instance of a task on a specific client. Nomad's scheduler decides which client runs which allocation.

Install on Debian / Ubuntu

wget -O- https://apt.releases.hashicorp.com/gpg | \
    sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
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
sudo apt update
sudo apt install nomad

Or grab the binary directly from developer.hashicorp.com/nomad/downloads and drop into /usr/local/bin/.

Single-node dev mode

For a first look:

sudo nomad agent -dev

This runs as both server and client on the local box. UI at http://127.0.0.1:4646/. Don't use -dev for anything beyond exploration — it stores state in memory and disables ACLs.

Production cluster: 3 server + N client

Three servers form the consensus layer (Raft); clients run workloads.

# /etc/nomad.d/nomad.hcl  (on each server)
data_dir   = "/var/lib/nomad"
datacenter = "lab"
log_level  = "INFO"

server {
  enabled          = true
  bootstrap_expect = 3
  encrypt          = "<openssl rand -base64 32>"
}

advertise {
  http = "{{ GetInterfaceIP \"eth0\" }}"
  rpc  = "{{ GetInterfaceIP \"eth0\" }}"
  serf = "{{ GetInterfaceIP \"eth0\" }}"
}

# Optional: integrate with Consul for service discovery
# consul {
#   address = "127.0.0.1:8500"
# }
# /etc/nomad.d/nomad.hcl  (on each client)
data_dir   = "/var/lib/nomad"
datacenter = "lab"

client {
  enabled = true
  servers = ["nomad-server-01:4647", "nomad-server-02:4647", "nomad-server-03:4647"]
  meta {
    role        = "worker"
    has_storage = "true"
  }
}

plugin "docker" { config { allow_privileged = false } }
plugin "raw_exec" { config { enabled = true } }
sudo systemctl enable --now nomad
nomad node status      # from any node
nomad server members

A first job

# nginx.nomad.hcl
job "nginx" {
  datacenters = ["lab"]
  type        = "service"

  group "web" {
    count = 3

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

    service {
      name = "nginx"
      port = "http"
      provider = "nomad"      # or "consul"
      check {
        type     = "http"
        path     = "/"
        interval = "10s"
        timeout  = "2s"
      }
    }

    update {
      max_parallel = 1
      min_healthy_time = "10s"
      healthy_deadline = "5m"
      auto_revert = true
    }

    task "nginx" {
      driver = "docker"

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

      resources {
        cpu    = 200    # MHz
        memory = 128    # MB
      }
    }
  }
}
nomad job plan   nginx.nomad.hcl     # show what would change
nomad job run    nginx.nomad.hcl     # apply
nomad job status nginx                # see allocations + health
nomad alloc logs <alloc-id>          # tail a running task

The scheduler places three nginx allocations on three different clients (subject to constraints), health-checks them, and exposes them via the service registry.

Networking

Three patterns:

  • Host networking — the task uses the client's network namespace. Simple, but ports must be unique per client.
  • Bridge networking (CNI) — each task gets its own veth into a CNI bridge. Standard for containers.
  • Group networks — tasks within a group share a network namespace (like a K8s pod). Useful for sidecars.

Service discovery

Nomad's built-in service catalog (since 1.3) handles "where is service X?" without Consul:

service {
  name = "nginx"
  port = "http"
  provider = "nomad"
}

Other jobs resolve nginx.service.lab.nomad (DNS) or query the API. For complex service-mesh needs, drop in Consul and Consul Connect — Nomad integrates natively.

Templates and secrets

The template stanza renders a file with Go templating at task start. Reading from Vault (see that tutorial):

task "app" {
  template {
    data = <<EOH
DATABASE_URL=postgres://{{ with secret "database/creds/readonly" }}{{ .Data.username }}:{{ .Data.password }}{{ end }}@db.lab:5432/app
EOH
    destination = "secrets/.env"
    env = true
  }
}

Nomad fetches dynamic credentials, writes them into a file scoped to the task, and exports them as env vars. When the lease nears expiry, Nomad re-renders the template and restarts the task with fresh credentials.

Job types

  • service — long-running. Restarts on failure.
  • batch — runs to completion. Like a Kubernetes Job.
  • system — one allocation per matching client. Like a Kubernetes DaemonSet.
  • sysbatch — batch + system (run to completion on every client).

Nomad Pack: the Helm-equivalent

Nomad Pack packages parameterized jobs the way Helm packages Kubernetes manifests. A pack is a directory of templates + variables; nomad-pack run vault installs a pre-built pack from the community registry.

What Nomad isn't

  • Not a full PaaS — no built-in CI/CD, no managed databases. Pair with Argo CD (see that tutorial) idea but for Nomad: Levant or simple nomad job run in CI.
  • Not Kubernetes-API-compatible. Existing Helm charts and operators don't drop in. The ecosystem is smaller but more cohesive.
  • Not opinionated about ingress — pair with Traefik, Caddy, or Fabio (a Nomad-native LB).

When Nomad beats Kubernetes

  • The fleet includes non-container workloads (a Java app, a desktop binary, a QEMU VM).
  • The team can't afford a dedicated platform engineering investment for k8s.
  • Multi-region active-active is a requirement — Nomad has it built in; k8s federation is still painful.
  • You want a single static binary that an operator can read end-to-end. The control-plane source is ~150k lines of Go; k8s is 10x that.