Mesh VPN Integration

Use DynIP with Tailscale

DynIP and Tailscale solve different halves of the same problem. Tailscale gives you an encrypted private mesh between your own devices; DynIP gives you a stable public DNS name for services that have to be reachable from outside that mesh. They are complementary, not competing — running both on the same host is a common and well-supported pattern.

The pattern: public DNS + private mesh

A typical home server has two kinds of consumers. Your phone, laptop, and family devices — all on your tailnet — can reach it over Tailscale's WireGuard mesh and don't need anything public. But anything outside the tailnet (a webhook from a SaaS provider, a friend's browser, a public API endpoint, a Let's Encrypt validation server) needs a real DNS name that resolves to a routable IP. DynIP handles the second case without forcing you to expose the first.

Say you self-host a home server at 192.168.1.50 on your LAN, with Tailscale installed so it also appears on your tailnet as 100.x.y.z. You give it two hostnames pointing at different addresses for different audiences:

Hostname Resolves to Reachable from
home.ddns.dynip.dev 203.0.113.42 (public) Anywhere on the internet (DynIP-managed)
home (MagicDNS) 100.x.y.z (tailnet) Only your tailnet devices (Tailscale-managed)

DynIP runs the standard /update loop on the host so home.ddns.dynip.dev always tracks your real public IP, even as your ISP rotates it. Tailscale handles the home name automatically via MagicDNS for any device that's signed into your tailnet — no configuration, no DNS server to operate, no public exposure of the tailnet address.

What you'll need

  • A DynIP account with at least one zone created (free tier is fine; the dashboard's snippets-generator will hand you a ready-to-paste DDNS updater).
  • A Tailscale tailnet, with the host you want to expose already signed in (tailscale up).
  • MagicDNS enabled in the Tailscale admin console (it's on by default for new tailnets).
  • A shell on the host so you can install or schedule a small updater that pings DynIP's /update endpoint when the public IP changes.

Setup

1. Create the public DynIP zone

In the DynIP dashboard, register a zone like home.ddns.dynip.dev (or use a BYOD namespace under your own domain). Copy the generated TSIG key and the snippet for your platform — generic (cURL) works on any Linux host.

2. Schedule the DynIP updater

Paste the snippet into a cron job, systemd timer, or your existing routine. A minimal cURL-based loop every 5 minutes is plenty — DynIP only writes when the IP actually changes:

*/5 * * * * curl -s "https://api.dynip.dev/update?domain=home.ddns.dynip.dev&ip=$(curl -s https://api.dynip.dev/ip)&key=YOUR_TSIG_KEY" > /dev/null

3. Leave Tailscale's MagicDNS alone

You don't need to touch Tailscale's DNS config. As long as MagicDNS is on and the host's machine name is home in the admin console, every signed-in tailnet device will reach it at http://home/ automatically.

4. Optional — share one TLS certificate across both audiences

Because DynIP issues Let's Encrypt certificates via the DNS-01 challenge, you can get a trusted cert for home.ddns.dynip.dev without opening port 80 and without Let's Encrypt ever needing to reach a tailnet IP. The cert binds to the name, not the address, so it works whether a request lands on the public IP or the tailnet IP.

To make tailnet devices serve the public name over TLS, configure Tailscale's split DNS so the tailnet's resolver answers home.ddns.dynip.dev with the tailnet IP. In the Tailscale admin console, add a split-DNS entry for ddns.dynip.dev (or your own zone) pointing at the host's tailnet IP. Inside the tailnet you get the private path; outside, the public DNS still wins.

Verification

From a device outside the tailnet (your phone on cellular, for example):

$ dig +short home.ddns.dynip.dev
203.0.113.42

$ curl -I https://home.ddns.dynip.dev
HTTP/2 200

From a device inside the tailnet with split DNS configured:

$ dig +short home.ddns.dynip.dev
100.x.y.z

Common issues

Public DNS resolves to the wrong IP

DynIP records what your updater sends. If the host is behind double NAT (or a VPN client other than Tailscale), curl https://api.dynip.dev/ip may see the VPN egress instead of your real public IP. Run the IP check directly from the LAN, or hardcode the egress address in your updater.

Tailnet devices still hit the public IP

Split DNS only applies to devices using the tailnet's DNS resolver. Confirm tailscale status shows DNS as enabled, and on the device check scutil --dns (macOS) or resolvectl status (Linux) to verify the tailnet's resolver is consulted for the DynIP zone.

Cert renewal fails after the host moves

DNS-01 reads from DynIP, not from the host, so a moving public IP doesn't matter. If renewal fails, check that the TSIG key still exists in the dashboard and that your renewer is using the current key — rotated keys are surfaced on the zone's tsig-status endpoint.

Running services in docker-compose

If you're exposing a docker-compose stack via tailscale serve or tailscale funnel, the dynip-updater container is the natural fit to keep the public hostname's A and AAAA records current without a host-level cron. Drop it into the same compose file as the services Tailscale is fronting. See the Docker container section of the docs for the snippet and full env reference.

Related guides

  • Caddy + DynIP — serve the shared TLS cert with a single Caddyfile that handles DNS-01 automatically.
  • Home Assistant remote access — expose HA through DynIP and a reverse proxy while keeping the tailnet path private.
  • Traefik + DynIP — for Docker-based stacks, Traefik handles routing and TLS via DynIP DNS-01.
  • Netbird + DynIP — self-hosted alternative to Tailscale, also WireGuard-based.