Dynamic DNS proxy that maps Docker Swarm service replicas to predictable FQDNs via configurable templates.
Watches Docker Swarm through the socket and automatically maintains DNS A-records for every replica based on a user-defined FQDN template.
In Docker Swarm, replica IP addresses change on every deploy. The built-in Swarm DNS resolves only the service name → VIP (a single address) or DNSRR (a list with no way to identify a specific replica). There is no way to address a specific replica by a predictable name.
Swarm DNS Proxy polls the Docker API, builds FQDNs from your template, and runs its own DNS server with up-to-date records.
# 1. Build
docker build -t swarm-dns-proxy .
# 2. Deploy to Swarm
docker stack deploy -c docker-compose.yaml dns-proxy
# 3. Test
dig @<manager-ip> -p 5353 worker-1.replica-001.nginx.swarm.local
# 4. HTTP API — list all current records
curl http://<manager-ip>:8053/recordsYou can find a complete working example inside the example directory. It contains a simple Hello World setup (using a sticky hello application and Nginx ingress) which demonstrates how to configure Nginx dynamic upstreams with a custom DNS resolver. You can use it as a template or reference analogy for your own microservices.
Template variables:
| Variable | Description | Example |
|---|---|---|
{node} |
Swarm node hostname | worker-1 |
{service} |
Service name | nginx |
{stack} |
Stack namespace (label) or default |
myapp |
{slot} |
Replica slot number (1, 2, 3…) | 2 |
{replica} |
Zero-padded slot (001, 002…) | 002 |
{task_id} |
Short task ID (12 characters) | a1b2c3d4e5f6 |
{network} |
Overlay network name | my-overlay |
# Standard — node + replica + service
template: "{node}.replica-{replica}.{service}.swarm"
# → worker-1.replica-001.nginx.swarm
# With stack
template: "{replica}.{service}.{stack}.internal"
# → 001.api.myapp.internal
# With network (useful with multiple overlays)
template: "{node}.{service}.{network}.dc"
# → worker-2.redis.backend.dc.local
# By task ID (guaranteed unique)
template: "{task_id}.{service}.swarm"
# → a1b2c3d4e5f6.nginx.swarmWith endpoint mode vip, Swarm assigns a virtual IP to the service and
load-balances traffic internally. Swarm DNS Proxy fetches the actual container
IPs from the task's NetworksAttachments, bypassing the VIP. Each replica gets
its own DNS record.
With endpoint_mode: dnsrr, Swarm does not create a VIP and instead returns all
IPs via round-robin. The proxy works the same way — it extracts each task's IP from
NetworksAttachments and creates an individual FQDN record.
When ports are published with mode: host, the container is bound to the node's
network stack. The proxy uses the node's own IP (from node.Status.Addr) and
maps it to the replica's FQDN.
template: "{node}.replica-{replica}.{service}.swarm.local"
dns_port: 53
ttl: 30
poll_interval: 5
docker_socket: "/var/run/docker.sock"
upstream_dns: "8.8.8.8"
upstream_port: 53
log_level: "INFO"
# Service filter (optional).
# When specified, ONLY the listed services are tracked.
services:
- name: nginx
template: "{node}.{replica}.nginx.internal"
networks:
- frontend
- name: api
networks:
- backendEnvironment variables override values from the config file:
| Variable | Default |
|---|---|
CONFIG_PATH |
/etc/swarm-dns-proxy/config.yaml |
DNS_TEMPLATE |
(from file) |
DNS_PORT |
53 |
DNS_TTL |
30 |
POLL_INTERVAL |
5 |
DOCKER_SOCKET |
/var/run/docker.sock |
UPSTREAM_DNS |
8.8.8.8 |
UPSTREAM_PORT |
53 |
LOG_LEVEL |
INFO |
HTTP_PORT |
8053 |
| Endpoint | Description |
|---|---|
GET /health |
{"status": "ok"} |
GET /records |
List all current DNS records |
Example /records response:
{
"records": [
{"fqdn": "worker-1.replica-001.nginx.swarm", "ip": "10.0.1.5"},
{"fqdn": "worker-2.replica-002.nginx.swarm", "ip": "10.0.1.6"}
],
"count": 2
}┌─────────────────────────────────────────────────┐
│ Swarm DNS Proxy │
│ │
│ ┌──────────────┐ ┌──────────────────────┐ │
│ │ SwarmWatcher │───▶│ DNS Record Table │ │
│ │ (poll loop) │ │ fqdn → [ip, …] │ │
│ └──────┬───────┘ └──────────┬───────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────────────┐ │
│ │ Docker Socket │ │ UDP DNS Server │ │
│ │ /services │ │ (port 53/5353) │ │
│ │ /tasks │ │ │ │
│ │ /nodes │ │ match? → respond │ │
│ │ /networks │ │ miss? → upstream │ │
│ └──────────────┘ └──────────────────────┘ │
│ │
│ ┌──────────────────────┐ │
│ │ HTTP API (:8053) │ │
│ │ /health /records │ │
│ └──────────────────────┘ │
└─────────────────────────────────────────────────┘
To make all containers on a node use Swarm DNS Proxy, add to
/etc/docker/daemon.json:
{
"dns": ["<proxy-host-ip>"]
}Or specify DNS in the compose file of a specific service:
services:
myapp:
dns:
- <proxy-host-ip>- A-records only — AAAA (IPv6), SRV, and PTR are not yet supported.
- Polling — the Docker API is polled every N seconds. Can be switched to the Docker Events API (
/events?filters=...) for instant updates. - Single instance — no DNS table replication between multiple proxies. For HA, run one on each manager node + keepalived/VIP.
- No TCP DNS — only UDP is implemented. A TCP fallback is needed for responses exceeding 512 bytes.
MIT