Kubernetes Is Too Heavy, Swarm Lacks Features — Where Does Nomad Fit?
Two years ago, when I started migrating our e-commerce system to microservices, the first question was: what should we use to orchestrate containers? Kubernetes sounded impressive, but after two weeks of trying to set it up on 3 VPSes, our team was exhausted — dealing with an etcd cluster, control plane, RBAC, CRD… On top of that, I ran into a memory leak in one container back then and it took exactly 2 days to track it down. Debugging on K8s with logs from dozens of overlapping pods — if you haven’t been through it, it’s hard to imagine how painful it can be.
Docker Swarm was simpler, but its scheduling features were pretty rough. No flexible health checks, no detailed resource quotas, no support for non-Docker workloads.
That’s when I tried HashiCorp Nomad — and after more than a year of real-world use, this is the tool I want to share with anyone running small to medium systems.
What Is Nomad and Why Is It a Good Fit for Small Teams?
Nomad is HashiCorp’s workload orchestrator — and it’s not tied to Docker. Regular binaries, Java apps, shell scripts, or containers all work; each is a different “task driver.” It has clear advantages over K8s in several areas:
- Single binary: Both server and agent are a single ~100MB binary file. No separate etcd, no multiple components needed.
- 15-minute setup: A 3-node Nomad cluster can be up and running in 15 minutes, compared to K8s which takes at least a few hours.
- Native integration with Consul and Vault: Service discovery and secret management from the HashiCorp ecosystem — no additional setup if you’re already on this stack.
- Simple job specs: Written in HCL (HashiCorp Configuration Language) — much more readable than K8s YAML.
Core Concepts You Need to Know
No need to read the docs from start to finish. Understand these 4 concepts and you can start deploying right away:
- Job: The largest deployment unit. A job can contain multiple task groups.
- Task Group: A group of tasks that run together on one node (similar to a Pod in K8s).
- Task: The smallest unit — one Docker container, one script, or one binary.
- Allocation: When the Nomad scheduler decides to run a task group on a specific node, that’s called an allocation.
Installing Nomad and Running a Simple Cluster
Install Nomad on Ubuntu/Debian
# Add the HashiCorp repo
wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /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
# Check version
nomad version
Run Nomad in Dev Mode (Quick Local Test)
# Run server + agent on the same machine — for testing only!
sudo nomad agent -dev
# Open another terminal, check node status
nomad node status
Production Server Configuration (3 Nodes)
Create the file /etc/nomad.d/server.hcl for the server node:
datacenter = "dc1"
data_dir = "/opt/nomad/data"
server {
enabled = true
bootstrap_expect = 3 # Need 3 server nodes for quorum
}
client {
enabled = false
}
Create the file /etc/nomad.d/client.hcl for worker nodes:
datacenter = "dc1"
data_dir = "/opt/nomad/data"
client {
enabled = true
servers = ["192.168.1.10:4647", "192.168.1.11:4647", "192.168.1.12:4647"]
}
plugin "docker" {
config {
allow_privileged = false
}
}
Writing Job Specs and Deploying Docker Containers
This is where I think Nomad clearly outperforms Docker Swarm. Job specs are clear, easy to read, and easy to version control.
Basic Job Spec — Deploy Nginx
job "nginx-web" {
datacenters = ["dc1"]
type = "service" # Runs continuously, restarts on failure
group "web" {
count = 2 # 2 instances
network {
port "http" {
static = 80
}
}
task "nginx" {
driver = "docker"
config {
image = "nginx:alpine"
ports = ["http"]
}
resources {
cpu = 200 # MHz
memory = 128 # MB
}
# Built-in health check
service {
name = "nginx-web"
port = "http"
check {
type = "http"
path = "/"
interval = "10s"
timeout = "2s"
}
}
}
}
}
Deploy the job to the cluster:
# Check the plan before applying (like terraform plan)
nomad job plan nginx.nomad
# Deploy
nomad job run nginx.nomad
# View status
nomad job status nginx-web
# View logs for a specific allocation
nomad alloc logs <alloc-id>
Practical Tip: Use Resource Constraints to Prevent OOM
Back when I ran into that memory leak in our e-commerce project, I learned an expensive lesson: always set a memory limit. On K8s, the container gets killed but the logs aren’t clear about why. On Nomad, when a container exceeds its memory limit, Nomad will:
- Log the reason clearly in the allocation log:
OOM killed - Automatically restart according to the configured policy
- Update metrics so you can set up alerts
resources {
cpu = 500
memory = 256
memory_max = 512 # Allow bursting but not beyond this limit
}
restart {
attempts = 3
interval = "5m"
delay = "15s"
mode = "fail" # Fail after 3 attempts instead of restarting indefinitely
}
Deploying an Application with Environment Variables and Secrets
task "api-server" {
driver = "docker"
config {
image = "myapp/api:v1.2.0"
}
env {
APP_ENV = "production"
LOG_LEVEL = "info"
}
# Secret from Vault (if using HashiCorp Vault)
template {
data = <<EOT
{{ with secret "secret/myapp/db" }}
DB_PASSWORD={{ .Data.data.password }}
{{ end }}
EOT
destination = "secrets/env.txt"
env = true
}
resources {
cpu = 300
memory = 256
}
}
Practical Tips from Real-World Operations
1. Use the “batch” Job Type for One-Off Tasks
If you need to run a cronjob or a one-off script, use type = "batch". Nomad will run it and clean up afterward, without keeping the process alive.
job "db-backup" {
type = "batch"
# Nomad also has a periodic block — like cron
periodic {
crons = ["0 3 * * *"] # Run at 3am every day
}
# ... task config
}
2. Canary Deployment Is Built into Nomad
update {
canary = 1 # Deploy 1 new instance first
min_healthy_time = "30s" # Must stay healthy for 30s before proceeding
healthy_deadline = "5m"
auto_promote = false # Requires manual promotion — safer
auto_revert = true # Auto rollback on failure
}
3. Validate JSON Responses from the Nomad API
Nomad has a fairly complete REST API. When debugging or writing automation scripts, I often use JSON Formatter on ToolCraft to format the output for easier reading. The tool runs locally in the browser — no worries about internal data being sent anywhere.
# Get job information in JSON format
curl http://localhost:4646/v1/job/nginx-web | python3 -m json.tool
# Or use the nomad CLI
nomad job inspect nginx-web
4. Converting Between YAML and HCL When Needed
If your team is used to YAML (from K8s), Nomad also accepts JSON job specs. I use ToolCraft’s YAML ↔ JSON Converter when I need to convert configs back and forth — much more convenient than writing a script by hand.
5. Monitoring with the Nomad UI
Nomad comes with a built-in web UI at http://<server-ip>:4646. Not as fancy as the K8s Dashboard, but it’s more than enough: view job status, allocations, logs, and resource usage in real time.
Quick Comparison: Nomad vs Docker Swarm vs Kubernetes
| Criteria | Nomad | Docker Swarm | Kubernetes |
|---|---|---|---|
| Setup complexity | Low | Very low | High |
| Resource overhead | ~100MB RAM | ~50MB RAM | >1GB RAM |
| Non-Docker workload | ✅ Yes | ❌ No | ✅ Yes |
| Canary deployment | ✅ Built-in | ❌ Manual | ✅ Yes |
| Ecosystem | HashiCorp | Docker Inc. | CNCF (very large) |
| Best for | 2–10 person teams | Simple setups | Enterprise |
Conclusion
Nomad isn’t a perfect tool. Need a massive ecosystem, a large community, or building a genuinely large-scale SaaS? K8s is still the right call. But if you’re running 3–20 servers with a small team and need sufficient orchestration without spending an entire week maintaining a control plane — Nomad is absolutely worth a serious look.
Personally, after a year of switching to Nomad: deployments are faster, debugging is easier, and most importantly, the whole team actually understands what the system is running — nobody dreads opening kubectl anymore.
