Internal DNS for Docker Clusters Without K8s
How Temps gives self-hosted Docker clusters stable service discovery and failover-aware internal DNS without running Kubernetes, CoreDNS or a service mesh.
Temps Team · May 17, 2026 · 2mo ago
Temps v0.1.0 ships built-in internal DNS for self-hosted Docker clusters without Kubernetes — a per-node Hickory resolver embedded in temps-agent that gives every managed database a stable *.temps.local hostname, eliminates the IP-env-var churn that broke Postgres HA before v0.1.0, and keeps resolving from a fsynced on-disk snapshot if the control plane is unreachable.
If you run a self-hosted Docker cluster and need service names that survive pod moves and failovers — without standing up Kubernetes — Temps v0.1.0 includes this out of the box. No extra process to run. No CoreDNS Deployment to manage.
How Do You Get Internal DNS for a Self-Hosted Docker Cluster Without Kubernetes?
The short answer: deploy Temps. When you run temps serve and join worker nodes with temps join <url> <token>, each worker's temps-agent automatically starts an embedded Hickory DNS resolver. Every container gets the bridge gateway IP injected as its first nameserver via Docker's --dns flag. Names like pg-orders.temps.local resolve from any node, sub-millisecond, with no separate DNS server to operate.
Three Verified Temps Claims:
-
Per-node full-zone authority, 1-second poll cadence: Every
temps-agentembeds a Hickory DNS resolver and serves the complete*.temps.localzone from a local snapshot — not just its own records. The poll interval defaults to 1 second; the control plane returns immediately on changes (no server-side hold), so propagation is near-real-time. (Source:crates/temps-dns-resolver/src/config.rs) -
5-second failover to DNS flip: A 5-second-tick reconciler in
temps-providersqueries pg_auto_failover, upsertsprimary.<svc>.temps.local(TTL 5s) andreplica.<svc>.temps.local(TTL 30s), and rotates the A record on promotion. libpq withtarget_session_attrs=read-writereconnects transparently. (Source: ADR-011) -
Control-plane-outage resilience from on-disk snapshot: The resolver fsyncs
zone.jsonon every applied generation to/var/lib/temps/dns/. If the control plane is unreachable, containers keep resolving from this snapshot.node_dns_state.healthflips to'stale'so drift is visible; existing app connections are unaffected.
Comparison: Temps internal DNS vs Kubernetes CoreDNS vs Manual IP Management
| Temps v0.1.0 | Kubernetes CoreDNS | Manual env-var IPs | |
|---|---|---|---|
| Requires Kubernetes | No | Yes | No |
| DNS resolver topology | Per-node (embedded in agent) | Centralized Deployment (2+ pods) | N/A |
| Lookup latency | Sub-millisecond (localhost) | 1–5ms (network hop) | N/A |
| Default sync cadence | 1-second poll | Watch events (milliseconds) | Manual redeploy |
| Failover propagation | ~5–7 seconds | Milliseconds (watch events) | Manual redeploy required |
| Control plane outage | Serves stale zone from disk | Depends on NodeLocal DNSCache | No impact |
| App-to-app DNS | Not in v0.1.0 | Yes (all services) | No |
| Setup complexity | Zero config — included in temps-agent | Cluster DNS infra required | Manual maintenance |
| License | Apache 2.0 | Apache 2.0 | N/A |
TL;DR: Temps v0.1.0 ships an internal DNS plane (
*.temps.local) backed by a per-node Hickory resolver, with three tiers of records: container-specific (pg-orders-0.pg-orders.temps.local), service VIP (pg-orders.temps.local), and role-aliased (primary.pg-orders.temps.local). The architecture borrows ideas from Kubernetes CoreDNS but is scoped narrowly to managed-database HA. App-to-app service discovery, DNSSEC, IPv6, and headless-service-style pod DNS are not in this release.
If you've worked with Kubernetes, you know how DNS feels in production: my-service.my-namespace.svc.cluster.local, headless services, ExternalName, kube-dns vs CoreDNS, the occasional "why is this resolving to the old pod" debugging session at 2am. It's powerful and complicated.
Temps v0.1.0 ships an internal DNS plane that does roughly the same job — a stable name that resolves to the right container IP from anywhere in the cluster — but with a deliberately narrower scope and a different set of design choices. This post is an honest comparison: what's supported, what isn't, and where the trade-offs land.
What Problem This Actually Solves
Before v0.1.0, a Postgres HA cluster on Temps had a structural bug. The cluster_connection_string() function built a libpq multi-host string from each member's hostname:
// Before v0.1.0 — broken end-to-end on multi-node clusters
let hosts = members.iter()
.map(|m| m.hostname.unwrap_or_else(|| m.container_name.clone()))
.collect::<Vec<_>>()
.join(",");
Container names only resolve on the host that runs them. So any Postgres cluster with members on more than one node was broken — the connection string contained names that didn't resolve from the consumer's host.
The "fix" was either:
- Hand consumers a list of pre-resolved IPs via env var. Now adding or removing a replica forces redeploying every consumer. That's not HA — that's a kill switch with extra steps.
- Build an internal DNS plane.
Temps v0.1.0 is option 2.
After v0.1.0, the connection string becomes one line:
postgresql://user:pw@pg-orders.temps.local:5432/db?target_session_attrs=read-write
DNS handles host enumeration. Failover flips the primary.* record within ~5 seconds. No consumer redeployment required.
How Kubernetes Does It
For comparison's sake, here's what Kubernetes ships:
- CoreDNS runs as a Deployment in the
kube-systemnamespace. Pods get its ClusterIP as their first nameserver via/etc/resolv.conf(injected by kubelet). <service>.<namespace>.svc.cluster.localresolves to the service's ClusterIP — a stable virtual IP that kube-proxy load-balances across the service's pods.- Headless services (
clusterIP: None) skip the VIP and return all pod IPs as multi-A records. This is how StatefulSets give you per-pod DNS like<pod-name>.<service>.<namespace>.svc.cluster.local. - SRV records are populated for named ports.
_postgres._tcp.<service>.<namespace>.svc.cluster.localreturns the port + target host. - ExternalName services proxy DNS to an external name via CNAME.
autopathplugin (CoreDNS) short-circuits the ndots=5 search-domain explosion for fully-qualified queries.
Kubernetes DNS is essentially "every service has a name, every pod has a name, you compose them via search paths and namespace boundaries." It's been refined over 8 years. It's good.
How Temps Does It
Three tiers, scoped to clusters managed by Temps. Everything under .temps.local.
| Tier | What | Pattern | Example |
|---|---|---|---|
| 3 | Service identity | <svc>.temps.local | pg-orders.temps.local (multi-A) |
| 3 | Role alias (writes) | primary.<svc>.temps.local | primary.pg-orders.temps.local |
| 3 | Role alias (reads) | replica.<svc>.temps.local | replica.pg-orders.temps.local |
| 2 | Container identity | <svc>-<ord>.<svc>.temps.local | pg-orders-0.pg-orders.temps.local |
| 1 | L3 reachability | (IP) | 172.20.5.42 |
Citation Capsule: Temps v0.1.0's internal DNS plane runs a per-node Hickory resolver embedded in
temps-agent, listening on the bridge gateway address (172.x.x.1:53) and127.0.0.53:53. Each node serves the full zone from a localservice_endpointssnapshot synced from the control plane at a 1-second poll interval. The data is persisted to/var/lib/temps/dns/zone.json(fsynced on every generation bump) for failure isolation.node_dns_statetracks each node'sapplied_generationandhealthstatus ('healthy','degraded','stale','unknown').
Per-Node Resolver vs Central CoreDNS
This is the biggest architectural divergence. Kubernetes runs CoreDNS as a centralized Deployment (typically 2 replicas). Temps embeds a Hickory resolver in every temps-agent and runs it locally on each node.
| Property | Kubernetes (CoreDNS) | Temps v0.1.0 (Hickory per-node) |
|---|---|---|
| Topology | Centralized Deployment, 2+ replicas | One resolver per node |
| Failure domain | Cluster-wide (mitigated by replicas + NodeLocal DNSCache) | Per-node (containers on a healthy node aren't affected) |
| Lookup latency | Network hop to CoreDNS pod, typically 1–5ms | Localhost, sub-millisecond |
| Authority | CoreDNS pulls from Kubernetes API | Each node serves the full zone from a local snapshot |
| Failure isolation | Control plane down = cluster DNS works (stale state) | Control plane down = resolution works from on-disk snapshot |
Why per-node? Three reasons from ADR-011:
- Blast radius. A central resolver is a cluster-wide SPOF. Per-node resolvers fail independently.
- Latency. Localhost DNS responds in under 1ms; central DNS over the underlay inherits the overlay's tail latency on every lookup.
- Authority is local. Each node already knows what containers it hosts.
Honestly, Kubernetes has converged on something similar with NodeLocal DNSCache — a DaemonSet that runs a CoreDNS instance on each node specifically to cut latency and reduce conntrack pressure on the central pods. Temps's per-node-by-default is the same idea, made structural.
The "per-node serves the full zone" choice is what makes the Temps approach safer than naïve per-node DNS. Each node has the entire *.temps.local zone in memory. A node losing its agent doesn't lose name resolution — Docker containers can be configured with a peer node's bridge gateway as a secondary nameserver.
Sync Model: Polling vs Kubernetes Informers
Kubernetes CoreDNS reads from the API server via an informer cache. When a Service is created, an EndpointSlice changes, or a pod IP changes, the watch event arrives at CoreDNS within milliseconds and DNS records update.
Temps uses a polling pattern, borrowed from how its existing internal API surfaces (peer lists, route tables) already work:
Agent: GET /internal/nodes/{id}/dns/changes?since=N (1s default poll)
← receives diff with generation > N, or full_snapshot if gap is large
writes records to local zone store
fsyncs to /var/lib/temps/dns/zone.json
ACKs by writing back applied_generation = M
The service_endpoints table has a monotonic generation column bumped on every mutation. Agents pull deltas. The control plane's node_dns_state table tracks each node's applied_generation and last_sync_at, so drift is visible — the ops query is:
SELECT node_id FROM node_dns_state
WHERE last_sync_at < now() - interval '60 seconds';
The control plane currently returns immediately (no server-side hold), so propagation is bounded by the 1-second poll interval — not a 30-second timeout. The maximum backoff on error is 30 seconds. For a system targeting ~6-second failover propagation, polling at 1s is more than sufficient.
In v0.1.0 testing, the median primary-promotion-to-DNS-flip time was 5–7 seconds (5s reconciler tick + up to 1s poll). libpq's reconnect logic handled the gap transparently with target_session_attrs=read-write.
What Temps Does NOT Support in v0.1.0
This is where the comparison gets honest. Kubernetes DNS is feature-rich. Temps v0.1.0 deliberately is not.
App-to-App Service Discovery
In Kubernetes, every Service gets DNS automatically. Your payments service can call http://orders.default.svc.cluster.local without configuration.
In Temps v0.1.0, app deployments don't get auto-generated DNS records. The schema and resolver support it (the service_endpoints table has owner_kind = 'static' for free-form names), but lifecycle hooks for user deployments are deferred until there's a clear demand signal. You still wire app-to-app traffic through env vars or the proxy.
This is the single biggest gap vs Kubernetes. If your mental model is "every service has a name," Temps doesn't give you that for application containers in this release. Only managed databases get first-class DNS.
DNSSEC
Kubernetes CoreDNS supports DNSSEC. Temps v0.1.0 does not.
The reasoning: internal-only zones, authenticated control plane, low value relative to the implementation cost. The threat model is "spoofing inside the cluster," and the mitigation is binding the resolver to <bridge_gateway> and 127.0.0.53 only — never 0.0.0.0. Containers can't reach the control-plane sync API directly; the agent calls it on their behalf with a node token.
If your compliance regime requires DNSSEC for internal queries, this is a gap.
IPv6 (AAAA Records)
The schema column is plain text and Hickory serves AAAA records when present, so the day IPv6 ships in temps-network, records "just appear" with no migration. But no IPv6-specific work landed in v0.1.0. The L3 overlay is v4-only.
SRV Records for Named Ports
Kubernetes auto-populates _<port>._<proto>.<service>.<namespace>.svc.cluster.local SRV records. The Temps schema supports record_type = 'SRV', but the reconciler only writes A records. SRV is plumbed but unused.
Per-Container DNS Customization
Kubernetes lets you customize per-pod dnsConfig with extra search paths, options like ndots:1, custom nameservers. Temps sets the bridge gateway via HostConfig.dns on every container. No per-container DNS customization.
Wildcard Records
CoreDNS supports wildcard plugins. Temps's resolver matches exact FQDNs. No wildcards.
CNAME Chains Across the Boundary
Kubernetes ExternalName services CNAME to external names. Temps's resolver handles *.temps.local in-zone and forwards everything else to upstream resolvers (Cloudflare 1.1.1.1/1.0.0.1, Google 8.8.8.8) — but it doesn't synthesize CNAME records that bridge the two.
What Is Actually Shipped in v0.1.0
| Feature | Status |
|---|---|
| Per-container A records | Yes |
| Multi-A round-robin for service VIPs | Yes |
| Role-aliased records (primary/replica) | Yes (Postgres only) |
| ~5-second failover propagation | Yes |
| Per-node resolver with full zone | Yes |
| On-disk snapshot for control-plane outages | Yes |
| Aggressive low TTLs (5s primary, 30s replica) | Yes |
Upstream forwarder for non-temps.local queries | Yes (Cloudflare + Google) |
Drift detection via node_dns_state | Yes |
| Two-nameserver container config for HA | Yes (configurable) |
| Underlay fallback for non-overlay members | Yes |
| Janitor for orphan record cleanup | Yes (hourly) |
The single most useful operational property has been the "stale-but-serving" behavior. Restart the control plane while a Postgres app is running, and the resolvers serve the last-known zone from disk. Existing connections keep working; only new mutations are blocked. Compare to a freshly-bounced CoreDNS Deployment with no NodeLocal cache, where you can lose name resolution for the duration of the rolling restart.
When to Choose Temps DNS Over Kubernetes
Honest answer: most teams shouldn't pick Temps's DNS over Kubernetes's DNS. They should pick Temps over Kubernetes for the whole deployment story, and the DNS comes along for the ride.
Reasons to be on Temps with this DNS plane:
- You run a small fleet (1–50 services) and don't want to operate Kubernetes.
- You need managed database HA with automatic failover and don't want to glue together AWS RDS or Crunchy.
- You're using Docker, not Kubernetes — the rest of the cluster ecosystem (storage, networking, monitoring) is sized for that.
- You're OK with ~6-second failover latency for databases and don't need millisecond DNS propagation.
Reasons to stay on Kubernetes:
- You already have CoreDNS at scale. Don't migrate.
- You need app-to-app service discovery via DNS. Wait for Temps to ship it.
- You need DNSSEC, headless service patterns, or pod-level DNS customization. Not in v0.1.0.
- You're at 100+ services. The Kubernetes ecosystem is tuned for that scale.
Temps is Apache 2.0, free to self-host, with Temps Cloud — a managed add-on for telemetry retention, offsite backups, and AI credits — coming soon (not yet priced). The internal DNS plane is included — no add-on, no extra config.
What's Coming in Future Releases
The ADR explicitly defers these. They're not commitments, but they're the natural next steps:
- App-to-app DNS (
<app>.apps.temps.local). The schema supports it; missing piece is lifecycle hooks at deploy/scale time. - Redis Sentinel and MongoDB ReplicaSet reconcilers. Same machinery, different upstream APIs to query.
- SRV record reconciliation for services that publish multiple ports.
- Push-based sync instead of polling. Re-evaluate if sub-second propagation is needed.
- DNSSEC for compliance-regulated deployments. Cost vs value question, depends on demand.
FAQ
How do you set up internal DNS for a Docker cluster without Kubernetes?
With Temps, you don't configure anything. When you run temps serve (control plane) and join worker nodes with bunx @temps-sdk/cli join <url> <token>, each worker node's temps-agent automatically starts an embedded Hickory DNS resolver listening on the overlay bridge gateway. Every container you deploy gets the bridge IP injected as its nameserver via --dns, so *.temps.local names resolve immediately. No separate CoreDNS Deployment, no kube-dns, no external DNS server to manage.
Why not just use CoreDNS?
CoreDNS is great. The reason for embedding Hickory in temps-agent is integration: lifecycle hooks, in-process error handling, no separate binary to ship and version, Rust-native error propagation into the existing audit trail. CoreDNS would have meant another process per node with its own configuration, restart policy, and failure mode. Hickory is a Tokio task in a process that already runs.
Can my apps query pg-orders.temps.local from a sidecar?
Yes — the bridge gateway resolver answers any DNS query for *.temps.local from any container attached to the temps-overlay bridge. Sidecars, init containers, anything. Non-temps.local queries forward to Cloudflare and Google.
What happens if the control plane is unreachable?
The resolver keeps serving the last-known zone from /var/lib/temps/dns/zone.json (fsynced on every generation bump). node_dns_state.health flips to 'stale' so ops can tell. Existing apps keep resolving; mutations are paused until the control plane comes back.
Does this work with Kubernetes in a hybrid setup?
Not directly. Temps's DNS is for containers attached to the temps-overlay Docker bridge. If you're running Temps and Kubernetes side-by-side, each has its own DNS plane and the two don't federate. Apps on Kubernetes that need to reach a Temps-managed Postgres cluster would use the cluster's external endpoint, not the internal *.temps.local name.
What is the resource cost per node?
Hickory in-process is approximately 3MB RSS plus a small zone cache (few KB for typical clusters). The agent already runs on every node; the resolver is a Tokio task inside it. No separate process, no separate logs, no separate health check.
Is Temps free to self-host?
Yes. Temps is Apache 2.0 — free to self-host. Temps Cloud, a managed add-on for telemetry retention, offsite backups, and AI credits, is coming soon and not yet priced; there's no per-seat pricing, no bandwidth bills, and no vendor lock-in.
Related: Read the full v0.1.0 release notes