TempsTemps
  • Docs
  • Blog
  • Roadmap
  • Pricing
  • Enterprise
  • Security
  • Contact
Star—
TempsTemps

Open-source deployment platform with built-in error tracking, analytics, and monitoring. Runs on any VPS. No surprise bills, no data leaving your infrastructure.

  • Product
  • Features
  • AI Agents
  • Error Tracking
  • Observability
  • Analytics
  • Documentation
  • Changelog
  • Enterprise
  • Contact
  • Resources
  • Getting Started
  • Upgrade
  • GitHub
  • Reddit
  • Tools
  • VPS Security Scanner
  • PaaS Tax Calculator
  • Compare
  • vs Vercel
  • vs Netlify
  • vs Coolify
  • All Platforms
  • Deploy
  • Next.js
  • Node.js
  • Django
  • Laravel
  • Go
  • Rust
  • All Frameworks →
  • Legal & Compliance
  • Security & Trust
  • Data Ownership & Privacy
  • GDPR Compliance

© 2026 Temps. All rights reserved.

GitHubDocs
Back to all posts

Internal DNS for Self-Hosted Docker Clusters Without Kubernetes

How Temps provides stable service discovery and failover-aware DNS for self-hosted Docker clusters without operating Kubernetes or CoreDNS.

Temps Team

Temps Team

May 17, 2026 · 2mo ago

Free guide

Self-Hosting Starter Kit

Everything you need to go from zero to a production-ready self-hosted server in an afternoon. No Kubernetes required.

  • VPS provider comparison (Hetzner, DO, Vultr — real 2026 prices)
  • Security hardening checklist (SSH, firewall, fail2ban)
  • Docker production setup with automated backups
  • SSL, DNS, and monitoring in 15 minutes

No spam. Unsubscribe anytime. Privacy policy

#internal dns#service discovery#self-hosted docker#coredns alternative#postgres failover#hickory dns
Back to all posts

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:

  1. Per-node full-zone authority, 1-second poll cadence: Every temps-agent embeds a Hickory DNS resolver and serves the complete *.temps.local zone 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)

  2. 5-second failover to DNS flip: A 5-second-tick reconciler in temps-providers queries pg_auto_failover, upserts primary.<svc>.temps.local (TTL 5s) and replica.<svc>.temps.local (TTL 30s), and rotates the A record on promotion. libpq with target_session_attrs=read-write reconnects transparently. (Source: ADR-011)

  3. Control-plane-outage resilience from on-disk snapshot: The resolver fsyncs zone.json on every applied generation to /var/lib/temps/dns/. If the control plane is unreachable, containers keep resolving from this snapshot. node_dns_state.health flips 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.0Kubernetes CoreDNSManual env-var IPs
Requires KubernetesNoYesNo
DNS resolver topologyPer-node (embedded in agent)Centralized Deployment (2+ pods)N/A
Lookup latencySub-millisecond (localhost)1–5ms (network hop)N/A
Default sync cadence1-second pollWatch events (milliseconds)Manual redeploy
Failover propagation~5–7 secondsMilliseconds (watch events)Manual redeploy required
Control plane outageServes stale zone from diskDepends on NodeLocal DNSCacheNo impact
App-to-app DNSNot in v0.1.0Yes (all services)No
Setup complexityZero config — included in temps-agentCluster DNS infra requiredManual maintenance
LicenseApache 2.0Apache 2.0N/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.

Free guide

Self-Hosting Starter Kit

Everything you need to go from zero to a production-ready self-hosted server in an afternoon. No Kubernetes required.

  • VPS provider comparison (Hetzner, DO, Vultr — real 2026 prices)
  • Security hardening checklist (SSH, firewall, fail2ban)
  • Docker production setup with automated backups
  • SSL, DNS, and monitoring in 15 minutes

No spam. Unsubscribe anytime. Privacy policy

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:

  1. 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.
  2. Build an internal DNS plane.

Temps v0.1.0 is option 2.

After v0.1.0, the connection string becomes one line:

postgresql://user:[email protected]:5432/db?target_session_attrs=read-write

DNS handles host enumeration. Failover flips the primary.* record within ~5 seconds. No consumer redeployment required.

Free guide

Self-Hosting Starter Kit

Everything you need to go from zero to a production-ready self-hosted server in an afternoon. No Kubernetes required.

  • VPS provider comparison (Hetzner, DO, Vultr — real 2026 prices)
  • Security hardening checklist (SSH, firewall, fail2ban)
  • Docker production setup with automated backups
  • SSL, DNS, and monitoring in 15 minutes

No spam. Unsubscribe anytime. Privacy policy

How Kubernetes Does It

For comparison's sake, here's what Kubernetes ships:

  • CoreDNS runs as a Deployment in the kube-system namespace. Pods get its ClusterIP as their first nameserver via /etc/resolv.conf (injected by kubelet).
  • <service>.<namespace>.svc.cluster.local resolves 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.local returns the port + target host.
  • ExternalName services proxy DNS to an external name via CNAME.
  • autopath plugin (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.

TierWhatPatternExample
3Service identity<svc>.temps.localpg-orders.temps.local (multi-A)
3Role alias (writes)primary.<svc>.temps.localprimary.pg-orders.temps.local
3Role alias (reads)replica.<svc>.temps.localreplica.pg-orders.temps.local
2Container identity<svc>-<ord>.<svc>.temps.localpg-orders-0.pg-orders.temps.local
1L3 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) and 127.0.0.53:53. Each node serves the full zone from a local service_endpoints snapshot 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_state tracks each node's applied_generation and health status ('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.

PropertyKubernetes (CoreDNS)Temps v0.1.0 (Hickory per-node)
TopologyCentralized Deployment, 2+ replicasOne resolver per node
Failure domainCluster-wide (mitigated by replicas + NodeLocal DNSCache)Per-node (containers on a healthy node aren't affected)
Lookup latencyNetwork hop to CoreDNS pod, typically 1–5msLocalhost, sub-millisecond
AuthorityCoreDNS pulls from Kubernetes APIEach node serves the full zone from a local snapshot
Failure isolationControl plane down = cluster DNS works (stale state)Control plane down = resolution works from on-disk snapshot

Why per-node? Three reasons from ADR-011:

  1. Blast radius. A central resolver is a cluster-wide SPOF. Per-node resolvers fail independently.
  2. Latency. Localhost DNS responds in under 1ms; central DNS over the underlay inherits the overlay's tail latency on every lookup.
  3. 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

FeatureStatus
Per-container A recordsYes
Multi-A round-robin for service VIPsYes
Role-aliased records (primary/replica)Yes (Postgres only)
~5-second failover propagationYes
Per-node resolver with full zoneYes
On-disk snapshot for control-plane outagesYes
Aggressive low TTLs (5s primary, 30s replica)Yes
Upstream forwarder for non-temps.local queriesYes (Cloudflare + Google)
Drift detection via node_dns_stateYes
Two-nameserver container config for HAYes (configurable)
Underlay fallback for non-overlay membersYes
Janitor for orphan record cleanupYes (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, self-host free, or ~$6/mo on Temps Cloud (Hetzner cost + 30%, no per-seat fees). The internal DNS plane is included — no add-on, no extra config.

Free guide

Self-Hosting Starter Kit

Everything you need to go from zero to a production-ready self-hosted server in an afternoon. No Kubernetes required.

  • VPS provider comparison (Hetzner, DO, Vultr — real 2026 prices)
  • Security hardening checklist (SSH, firewall, fail2ban)
  • Docker production setup with automated backups
  • SSL, DNS, and monitoring in 15 minutes

No spam. Unsubscribe anytime. Privacy policy

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 is ~$6/mo (Hetzner server cost + 30% margin), with no per-seat pricing, no bandwidth bills, and no vendor lock-in.

Related: Read the full v0.1.0 release notes