Deploy Django on HashiCorp Nomad
Deploy Django on HashiCorp Nomad: write a job with web, worker, and migration tasks, pull Docker images, inject secrets from Vault or Nomad Variables with templates, register services, route traffic through Traefik or Consul, and roll out safely.
Problem statement
You need to run Django across several servers with health checks, rolling updates, and secret injection, but Kubernetes feels like more platform than your team wants. HashiCorp Nomad is a single-binary scheduler that runs Docker containers (and other workloads) with a much smaller conceptual surface. A Django deployment on Nomad is one job file: a group for the web tasks, a group for Celery, a prestart task for migrations, and templates that render secrets into environment variables.
- Build and push a Docker image of the app with an immutable tag.
- Write a Nomad job with a
webgroup (count 2+, Docker driver, Gunicorn) and aworkergroup (Celery). - Add a
migratetask withlifecycle { hook = "prestart" }in the web group, or run migrations as a separate batch job. - Render secrets with a
templateblock from Nomad Variables or Vault, usingenv = true. - Register a
servicewith health checks and tags for Traefik or Consul to route traffic; useupdateblocks for rolling deploys.
Step-by-step solution
1. Write the job
job "shop" {
datacenters = ["dc1"]
type = "service"
update {
max_parallel = 1
min_healthy_time = "15s"
healthy_deadline = "3m"
auto_revert = true
}
group "web" {
count = 2
network { port "http" { to = 8000 } }
service {
name = "shop-web"
port = "http"
provider = "nomad"
tags = ["traefik.enable=true", "traefik.http.routers.shop.rule=Host(`shop.example.com`)"]
check {
type = "http"
path = "/healthz/"
interval = "10s"
timeout = "2s"
header { Host = ["shop.example.com"] }
}
}
task "web" {
driver = "docker"
config {
image = "ghcr.io/acme/shop:3f2a1c9"
ports = ["http"]
command = "gunicorn"
args = ["config.wsgi", "--bind", "0.0.0.0:8000", "--workers", "3"]
}
template {
destination = "secrets/app.env"
env = true
data = <<EOT
{{ with nomadVar "nomad/jobs/shop" }}
SECRET_KEY={{ .SECRET_KEY }}
DATABASE_URL={{ .DATABASE_URL }}
REDIS_URL={{ .REDIS_URL }}
{{ end }}
EOT
}
resources { cpu = 500 memory = 512 }
}
}
}
auto_revert = true returns to the last healthy version if a new allocation fails its health checks within the deadline.
2. Run migrations before web starts
Inside the web group:
task "migrate" {
lifecycle { hook = "prestart" sidecar = false }
driver = "docker"
config {
image = "ghcr.io/acme/shop:3f2a1c9"
command = "python"
args = ["manage.py", "migrate", "--noinput"]
}
template { destination = "secrets/app.env" env = true data = "..." }
}
With count = 2, each allocation runs its own prestart task, so migrations run twice; Django's migration lock in PostgreSQL makes the second a no-op, but a separate type = "batch" job run from CI before updating the service job is cleaner.
3. Add the worker group
group "worker" {
count = 1
task "celery" {
driver = "docker"
config {
image = "ghcr.io/acme/shop:3f2a1c9"
command = "celery"
args = ["-A", "config", "worker", "-l", "info", "--concurrency", "4"]
}
template { destination = "secrets/app.env" env = true data = "..." }
kill_timeout = "60s"
resources { cpu = 500 memory = 768 }
}
}
kill_timeout gives Celery time to finish running tasks after SIGTERM before Nomad sends SIGKILL.
4. Store secrets
Write values to Nomad Variables, readable by the job through workload identity:
nomad var put nomad/jobs/shop SECRET_KEY=... DATABASE_URL=postgres://... REDIS_URL=redis://...
With Vault, replace nomadVar with {{ with secret "kv/data/shop" }} and configure the job's Vault role. Rendered files go into the allocation's secrets/ directory, which is not readable by other allocations.
5. Route traffic and deploy
Traefik reads Nomad's native service catalog (providers.nomad) and creates routes from the tags. TLS certificates come from Traefik's ACME resolver. Deploy with:
nomad job plan shop.nomad.hcl
nomad job run shop.nomad.hcl
nomad job status shop
plan shows what will change, similar to terraform plan.
Explanation
Nomad schedules allocations of task groups onto client nodes, restarts failed tasks, and replaces allocations during updates according to the update block. Services registered with Nomad's built-in catalog (or Consul) carry health checks that gate rolling updates and feed load balancers. Django fits naturally: one image, different commands for web, worker, and migrations, and environment-based settings rendered from templates.
Edge cases / notes
- Licensing. Nomad is under the Business Source License since 2023; check that your use fits its terms, or evaluate alternatives.
- Static files. Bake collected static files into the image and serve with WhiteNoise, or put a CDN in front; there is no shared filesystem by default.
- Beat. Run Celery beat as its own group with
count = 1to avoid duplicate schedules.
Next steps
- Use Vault for secrets with Manage Django Secrets With HashiCorp Vault.
- Configure routing with Deploy Django With Traefik and Docker.
- Compare orchestration options in Kubernetes vs Docker Compose for Django.
FAQ
Is Nomad simpler than Kubernetes for Django?
For many teams, yes. One job file covers web, workers, migrations, and rolling updates, with fewer moving parts.
How do I run migrations on Nomad?
As a prestart lifecycle task in the web group, or better, as a separate batch job that CI runs before updating the service.
How do secrets reach Django?
Through template blocks that render Nomad Variables or Vault secrets into an env file loaded into the task's environment.
Does Nomad need Consul?
Not anymore. Nomad has built-in service discovery; Consul adds a service mesh and advanced features.