A Docker-based CoreDNS server that automatically creates DNS records for all machines in your Tailscale network (tailnet) under a custom domain. Uses the coredns-tailscale plugin to read machine information directly from the Tailscale daemon socket, with no API tokens required.
For example, a machine named media-server in your tailnet becomes resolvable as media-server.internal.example.com from any device on the tailnet.
CoreDNS runs with the coredns-tailscale plugin compiled in. The plugin connects to the Tailscale daemon socket (mounted into the container) and queries the local API to discover all machines visible to the host. It then serves A and AAAA records for each machine under your configured domain.
This is designed as a split DNS setup: Tailscale routes only your custom domain's queries to this server, while all other DNS queries continue to use your normal DNS resolver.
The Corefile includes the following plugins:
| Plugin | Purpose |
|---|---|
| tailscale | Queries the Tailscale local API via the daemon socket and serves A/AAAA records for each machine in the tailnet. Supports CNAME aliases via Tailscale ACL tags (see CNAME aliases). |
| cache | Caches DNS responses for CACHE_TTL seconds (default 30). Reduces load on the Tailscale socket and speeds up repeated lookups. |
| log | Logs all queries to stdout. Useful for debugging. Can be removed in production if you find it too noisy. |
| errors | Logs errors to stdout. |
| health | Exposes an HTTP health check at /health on HEALTH_PORT (default 8080). Used by the Docker healthcheck. |
| ready | Exposes an HTTP readiness check at /ready on READY_PORT (default 8181). Reports 200 once all plugins are loaded. |
A pre-built multi-arch image (amd64/arm64) is published at ghcr.io/alltuner/nameplate.
-
Create a
docker-compose.ymlwith your domain:services: coredns: image: ghcr.io/alltuner/nameplate:latest container_name: nameplate ports: - "53:53/tcp" - "53:53/udp" volumes: - /var/run/tailscale/tailscaled.sock:/var/run/tailscale/tailscaled.sock:ro environment: DOMAIN: internal.example.com # <-- replace with your domain restart: unless-stopped healthcheck: test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/health"] interval: 30s timeout: 5s retries: 3
-
Start the server:
docker compose up -d
-
Verify it works:
dig @localhost my-machine.internal.example.com
-
Configure Tailscale split DNS (see below).
Building locally: Clone the repo and use
build: .instead of theimageline in your compose file. See Upgrading versions for version details.
All settings are controlled via environment variables. Copy .env.example to .env and adjust:
| Variable | Default | Description |
|---|---|---|
DOMAIN |
internal.example.com |
The domain under which tailnet machines are served |
DNS_PORT |
53 |
Port CoreDNS listens on (TCP and UDP) |
TAILSCALE_SOCKET_PATH |
/var/run/tailscale/tailscaled.sock |
Path to the Tailscale socket on the host |
CACHE_TTL |
30 |
DNS cache duration in seconds |
HEALTH_PORT |
8080 |
Port for the health check HTTP endpoint |
READY_PORT |
8181 |
Port for the readiness check HTTP endpoint |
UPSTREAM_DNS |
(empty) | Upstream DNS server for forwarding unmatched queries (e.g., 1.1.1.1 or 100.100.100.100 for Tailscale MagicDNS). Leave empty to disable forwarding. |
Split DNS tells Tailscale to route DNS queries for a specific domain to a nameserver you control, while leaving all other queries untouched. This is how you make *.internal.example.com resolve on every device in your tailnet.
On the machine running this container:
tailscale ip -4Note this IP (e.g., 100.64.x.x). This is the address Tailscale will send DNS queries to.
- Go to the Tailscale admin console.
- Scroll to Nameservers.
- Under Add nameserver, select Custom....
- Click Add Split DNS.
- Enter:
- Domain: your configured
DOMAIN(e.g.,internal.example.com) - Nameserver: the Tailscale IP from Step 1
- Domain: your configured
- Save.
From any machine on your tailnet:
dig my-machine.internal.example.comYou should see an A record pointing to the machine's Tailscale IP.
- Queries not reaching CoreDNS: Ensure the CoreDNS host's Tailscale IP is correct and the container is running. Check
docker compose logs. - SERVFAIL responses: The Tailscale socket might not be accessible. Verify the socket path in
.envmatches the actual location on your host. On Linux it's typically/var/run/tailscale/tailscaled.sock, on macOS it's/Library/Tailscale/tailscaled.sock. - Stale results: Increase
CACHE_TTLif you want longer caching, or decrease it if machines are being added/removed frequently. - Port conflicts: Tailscale split DNS only routes queries to port 53, so the server must listen on port 53. If port 53 is already in use (common on Linux where systemd-resolved binds a stub listener to
127.0.0.53:53), disable the stub listener by settingDNSStubListener=noin/etc/systemd/resolved.confand restarting the service withsudo systemctl restart systemd-resolved.
The coredns-tailscale plugin supports CNAME records via Tailscale ACL tags. Add a tag prefixed with cname- to any machine in your Tailscale ACL policy:
Then apply the tag to a machine. This creates a CNAME record:
git.internal.example.com -> my-server.internal.example.com
This is useful for giving friendly names to services running on specific machines.
Nameplate is a thin packaging layer around two excellent projects:
- CoreDNS (GitHub) - A flexible, plugin-based DNS server written in Go, graduated from the CNCF.
- coredns-tailscale by @damomurf - The CoreDNS plugin that makes Tailnet machine discovery and CNAME aliasing possible.
The CoreDNS version is pinned in the Dockerfile and must match the upstream version that plugin.cfg is based on. To upgrade CoreDNS, update both files together:
- Generate a new
plugin.cfgfrom the target CoreDNS release (adding thetailscaleline). - Update
COREDNS_VERSIONin the Dockerfile to match.
The COREDNS_TAILSCALE_VERSION build argument in the Dockerfile can be overridden independently:
docker compose build --build-arg COREDNS_TAILSCALE_VERSION=v0.3.22- HTTPS with Let's Encrypt - How to add TLS certificates to services on your custom internal domain.
- Alternative approaches - Lighter DNS servers, the Tailscale local API, and the ecosystem landscape.
{ "tagOwners": { "tag:cname-git": ["autogroup:admin"], }, }