DNS for your tailnet.
A Docker-based CoreDNS server that turns every machine in your Tailscale network into a name under a custom domain.
A pre-built multi-arch image (amd64/arm64) is published at ghcr.io/alltuner/nameplate.
-
Create a
docker-compose.yml: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:
dig @localhost my-machine.internal.example.com
-
Configure Tailscale split DNS (see below).
Building locally: clone the repo and use
build: .instead ofimage:in your compose file. See Upgrading versions for details.
Nameplate runs CoreDNS with the coredns-tailscale plugin compiled in. The plugin reads your tailnet's machine list directly from the Tailscale daemon socket (no API tokens required) and serves A and AAAA records for each machine under a domain you configure.
A machine named media-server becomes media-server.internal.example.com, resolvable from any device on the tailnet.
This is designed as a split DNS setup: Tailscale routes only your custom domain's queries to Nameplate, while every other DNS query continues to use your normal resolver.
| 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 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. |
All settings are 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.
tailscale ip -4Note this IP (e.g., 100.64.x.x). This is where Tailscale will send DNS queries.
- 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.
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, 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 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 restartingsystemd-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
Useful for giving friendly names to services running on specific machines.
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 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.
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.
Nameplate is an open source project built by David Poblador i Garcia through All Tuner Labs.
If this project was useful to you, consider supporting its development.
Built by David Poblador i Garcia with the support of All Tuner Labs.
Made with ❤️ in Poblenou, Barcelona.

{ "tagOwners": { "tag:cname-git": ["autogroup:admin"], }, }