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?
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
PodSecurityPolicyor 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 logsandnomad alloc statuscover 95% of cases. Kubernetes debugging frequently involves chasing across CNI, kube-proxy, CoreDNS, ingress controllers, and three layers of admission webhooks.
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/hostsfile 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:
ssh root@your-server-ipFor 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:
sudo apt update && sudo apt upgrade -y
sudo apt install -y gnupg software-properties-common curl ca-certificatesExpected output (abbreviated):
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:
sudo rebootStep 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:
wget -O- https://apt.releases.hashicorp.com/gpg | \
sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpgAdd the repository to your sources list:
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.listUpdate the package index to pick up the new repository:
sudo apt updateExpected output includes a line like:
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:
sudo apt install -y nomadExpected output:
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:
nomad --versionExpected output:
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
nomadsystem user
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:
curl -fsSL https://get.docker.com | sudo sh
sudo systemctl enable --now docker
sudo usermod -aG docker nomadVerify Docker works:
sudo docker run --rm hello-worldInstall Podman and the Nomad Podman Driver
Podman is distributed in the Ubuntu universe repo:
sudo apt install -y podmanEnable the Podman socket, which the Nomad driver connects to:
sudo systemctl enable --now podman.socketDownload the Nomad Podman driver plugin:
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-podmanNomad 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:
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. Use5for a 5-server cluster; never use even numbers.retry_join— Gossip bootstrap targets. Resolvenomad-1etc. via DNS or/etc/hosts. Nomad will keep retrying forever (retry_max = 0) until all servers are reachable.advertisewith{{ 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 athttp://<node>:4646/ui.
sudo mkdir -p /opt/nomad/data /srv/nomad-volumes/app-data
sudo chown -R nomad:nomad /opt/nomad /srv/nomad-volumesStep 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:
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:
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:
sudo systemctl enable --now nomadCheck the service status:
sudo systemctl status nomadExpected output:
● 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.872sOnce all three servers are up, verify the Raft quorum from any node:
nomad server membersExpected output:
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 globalOne server should show Leader: true. Check the client nodes:
nomad node statusExpected output:
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 readyAll 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:
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:
nomad job run web.nomad.hclExpected output:
==> 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:
nomad job status webBatch Job: One-Off Task
Batch jobs run to completion. Create migrate.nomad.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:
nomad job run migrate.nomad.hclWhen 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:
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:
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:
sudo apt install -y consulMinimal Consul server config at /etc/consul.d/consul.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:
sudo systemctl enable --now consul
consul membersNow tell Nomad to talk to Consul. Add to /etc/nomad.d/nomad.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:
sudo systemctl restart nomadUpdate job service stanzas to use the Consul provider:
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:
sudo apt install -y vaultSee 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:
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:
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:
acl {
enabled = true
}Restart Nomad on each server one at a time (rolling):
sudo systemctl restart nomadBootstrap the root ACL token on the leader:
nomad acl bootstrapExpected output:
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:
export NOMAD_TOKEN="8888bbbb-9999-0000-cccc-ddddeeeeffff"Create a scoped policy for developers (dev-policy.hcl):
namespace "default" { policy = "write" capabilities = ["submit-job", "dispatch-job", "read-logs"] }agent { policy = "read" }
node { policy = "read" }
Apply and mint a token:
nomad acl policy apply -description "Developer policy" dev dev-policy.hcl
nomad acl token create -name="alice" -policy=dev -type=clientDistribute 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:
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-autoscalerCreate /etc/nomad-autoscaler/config.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:
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:
group "frontend" { count = 3scaling { 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):
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:
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):
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:
nomad volume create postgres-volume.hclMount it in a job:
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
| Problem | Cause | Solution |
|---|---|---|
No cluster leader error for more than 30 seconds | Raft quorum not met — fewer than bootstrap_expect servers reachable | Check nomad server members. Verify network connectivity on port 4648 (serf) and 4647 (rpc) between servers. |
Client shows down in nomad node status | Agent crashed or lost connectivity to servers | sudo journalctl -u nomad -n 100. Check the client's servers list resolves correctly. |
driver "docker" healthy but no allocations run | Nomad user is not in the docker group | sudo usermod -aG docker nomad && sudo systemctl restart nomad. |
Permission denied from Podman driver | Podman socket not running or wrong path | sudo systemctl status podman.socket. Verify socket_path in the plugin config matches the actual socket. |
Job stuck in pending forever | No client satisfies constraints | nomad 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 restart | ACLs enabled but no token provided | Export NOMAD_TOKEN in your shell or pass -token on the CLI. |
CSI volume never becomes available | Controller plugin can't reach storage backend | nomad plugin status <plugin-id>. Check controller logs: nomad alloc logs <alloc-id>. |
Viewing Logs
Nomad logs through systemd-journald:
sudo journalctl -u nomad -fPer-allocation logs:
nomad alloc logs -f <alloc-id>
nomad alloc logs -f -stderr <alloc-id>Detailed job placement diagnostics:
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/metricsendpoint. 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.