Tech Digest

Guide

Why can't my Docker container reach Tailscale IPs or MagicDNS names?

A container on a Tailscale host should reach other devices on the tailnet by address and by name, with nothing extra to set up. When it cannot, there are three separate causes, and the error tells them apart: a firewall rule, a missing network interface, or a sidecar that keeps the tailnet to itself.

Last reviewed

Why can't a Docker container reach Tailscale IPs or MagicDNS names when Tailscale runs on the host?

On current Tailscale it can, with no setup, as long as Tailscale runs in kernel mode with stateful filtering off, which is the default since 1.66.4. If the name resolves but the connection times out, stateful filtering is on: run tailscale set --stateful-filtering=false. If the host itself cannot ping 100.x addresses, Tailscale is in userspace mode and has no tailscale0 interface. If the app shares a Tailscale sidecar's network, set TS_USERSPACE=false with /dev/net/tun and NET_ADMIN, plus TS_ACCEPT_DNS=true for names.

On current Tailscale, a Docker container on the host reaches other tailnet devices by IP and by MagicDNS name with no setup at all. When it cannot, one of three things is wrong. Stateful filtering is on, and it drops every connection a container opens into the tailnet. Tailscale runs in userspace mode, so there is no network interface for the container's packets to leave by. Or the app sits inside a Tailscale sidecar that runs in userspace mode or ignores the tailnet's DNS. Forum threads mix the three up, but each one fails in its own way, and the error tells you which.

The terms this answer turns on#

Tailscale is a mesh VPN. Each device runs a program called tailscaled, gets an address in 100.64.0.0/10, and talks to the others over WireGuard. The devices together are your tailnet. On Linux, tailscaled normally creates a network interface called tailscale0 and routes every peer's address through it. That is kernel mode.

Userspace mode is the other way to run it. tailscaled handles tailnet traffic inside its own process and creates no interface. Other programs reach the tailnet only through a SOCKS5 or HTTP proxy that tailscaled can offer. The official tailscale/tailscale image runs this way unless told not to. Tailscale's docs list TS_USERSPACE as "Enabled by default", and say kernel networking "requires /dev/net/tun and additional capabilities".

MagicDNS gives each device a name made of its machine name and the tailnet's domain, such as nas.yak-bebop.ts.net. Tailscale also adds that domain as a search domain, so the short name nas works too. On our lab host, joining the tailnet replaced /etc/resolv.conf with nameserver 100.100.100.100, Tailscale's own resolver, and a search line for the tailnet's domain.

Stateful filtering is a firewall rule Tailscale can add on Linux. The client's help text says it applies "to forwarded packets (subnet routers, exit nodes, and so on)". A container's traffic is forwarded traffic: it arrives on a Docker bridge and leaves on tailscale0. So the rule catches it.

A sidecar here is a Tailscale container that your app joins with network_mode: service:ts in Compose. The two then share one network: the same interfaces, routes and loopback.

Why two answers circulate#

Most of the threads date from May 2024. Tailscale 1.66.0 turned stateful filtering on, and containers on many hosts lost DNS and their connections to the tailnet. The fix posted everywhere was --stateful-filtering=false. Tailscale's 1.66.4 notes, out on 21 May 2024, say stateful filtering "is now off by default". They give the reason: it "inadvertently broke DNS resolution from containers running on the host." So that answer is right for the host it was written on. On a current install the flag is already off, and repeating it changes nothing.

The other answer says a container can never use the host's VPN, so you need network_mode: host or a sidecar. Our tests say otherwise. With Tailscale in kernel mode and filtering off, every bridge container reached the tailnet. Docker rewrites the source of outbound container traffic to the host's address on the way out. On tailscale0 that is the host's own 100.x address. The peer's web server logged every request from 100.64.0.2, the host, as if the host had made it.

The third failure, the sidecar, is not about the host's Tailscale at all. It has its own fixes.

What we measured#

The lab ran Tailscale 1.104.4, the image's latest tag on 3 November 2026, and Headscale 0.29.7 as the control server. The Docker host was a Docker-in-Docker container running Engine 29.9.2 with tailscaled installed in it. It wrote /etc/resolv.conf directly, with no systemd-resolved. A second node ran a small web server. Each test used ping, wget and nslookup from an Alpine 3.22 container.

SetupPing to 100.xHTTP to 100.xMagicDNS name
Host kernel mode, defaults, default bridgeRepliesWorksResolves; container file lists nameserver 100.100.100.100
Same, compose or user-defined networkRepliesWorksResolves; ExtServers: [host(100.100.100.100) ...]
Stateful filtering on, default bridge100% lossdownload timed outno servers could be reached, public names too
Stateful filtering on, user-defined network100% lossdownload timed outResolves
Host in userspace mode, any network100% lossdownload timed outNXDOMAIN
Sidecar, image defaults100% lossdownload timed outNXDOMAIN
Sidecar, userspace, HTTP proxy onNot testedWorks by IP, full name and short nameThrough the proxy
Sidecar, kernel mode, TS_ACCEPT_DNS unsetRepliesWorksNXDOMAIN
Sidecar, kernel mode, TS_ACCEPT_DNS=trueRepliesWorksResolves

The first two rows are the baseline. On the default bridge Docker copied the host's nameserver, because 100.100.100.100 is not a loopback address. On a user-defined network the container asked Docker's embedded server at 127.0.0.11, which passed the query to 100.100.100.100. How Docker chooses between those two paths is covered in Why a Docker container cannot resolve DNS.

A worked case: Uptime Kuma checking a NAS by name#

Take a compose stack running Uptime Kuma on a host that has Tailscale installed. It should check http://nas.yak-bebop.ts.net:5000, and the check fails.

Start with the host. Run ping -c1 to the NAS's 100.x address. Suppose it gets no reply. Then the fault is on the host, before Docker. Run ip link show tailscale0. If that says the device does not exist, Tailscale is in userspace mode, and failure 2 below applies.

Suppose instead the host pings fine. Run the same lookup from a throwaway container on the stack's network: docker run --rm --network <project>_default alpine nslookup nas.yak-bebop.ts.net. If it returns the 100.x address, DNS is fine. Then try wget -T5 against the address. A timeout there, after a lookup that worked, is the stateful filtering pattern from our fourth row. tailscale debug prefs | grep NoStatefulFiltering would then print false.

Now suppose Uptime Kuma runs with network_mode: service:ts on a Tailscale sidecar. The host's Tailscale is no longer in the path. The sidecar's settings decide everything, and failure 3 applies.

Failure 1: stateful filtering is on#

Check the setting and the rule:

bash
tailscale debug prefs | grep NoStatefulFiltering   # false = filtering ON
iptables -S ts-forward | grep ctstate

With filtering on, our host showed this rule in Tailscale's ts-forward chain:

text
-A ts-forward -o tailscale0 -m conntrack ! --ctstate RELATED,ESTABLISHED -j DROP

It drops any new forwarded connection that leaves through tailscale0. Every connection a container starts to the tailnet is one. That explains both stateful rows. On the default bridge the container's only nameserver was 100.100.100.100. The DNS query was forwarded traffic too, so it was dropped, and public names failed along with tailnet ones. On a user-defined network, Docker's embedded DNS server sends the query from the host's own network. That query is not forwarded, so it passed. The name resolved, and the connection after it timed out.

The fix is one command:

bash
tailscale set --stateful-filtering=false

The next container request worked, with no restart. Tailscale's KB 1570 lists other fixes, such as --netfilter-mode=off or dockerd --iptables=false. Each of those hands the firewall work to you. Use the flag unless you have a reason not to.

KB 1570 also describes a warning: "Stateful filtering is enabled and Docker was detected". Our host showed no health warning in tailscale status with filtering on. Check the setting yourself rather than waiting for the warning.

One older form of this failure has its own log line. On 1.66.0 and 1.66.1, hosts without the conntrack package logged Couldn't load match 'conntrack':No such file or directory, and MagicDNS broke. GitHub issue 12087 traced it, and installing conntrack fixed it. The rule above is written with that match.

Failure 2: Tailscale on the host runs in userspace mode#

With tailscaled --tun=userspace-networking, our host had no tailscale0 and no routes to the tailnet. tailscale ping still got a pong, because it goes through tailscaled itself. A plain ping from the host lost every packet. /etc/resolv.conf was not pointed at 100.100.100.100, so nslookup returned NXDOMAIN. Every container failed the same way. Containers cannot use a route the host does not have.

This happens when Tailscale on the host is really the official image run with network_mode: host and left on its defaults. It also happens on any install started with the userspace flag. The fix is kernel mode. Install Tailscale from your distribution's package, or set TS_USERSPACE=false on the image and give it /dev/net/tun and NET_ADMIN.

Failure 3: the app shares a Tailscale sidecar#

A container that joins another's network gets that container's interfaces and its /etc/resolv.conf file. Our app and sidecar reported the same ResolvConfPath. So the app can do exactly what the sidecar's network can do, no more.

With the image defaults, that is nothing. In userspace mode there is no tailscale0. The app's packets to 100.x went out the normal default route, and timed out. TS_ACCEPT_DNS is also off: Tailscale's docs say "Defaults to not accepted". So the name returned NXDOMAIN.

Kernel mode fixes the addresses:

yaml
services:
  ts:
    image: tailscale/tailscale:latest
    environment:
      TS_AUTHKEY: ${TS_AUTHKEY}
      TS_STATE_DIR: /var/lib/tailscale
      TS_USERSPACE: "false"
      TS_ACCEPT_DNS: "true"
    volumes:
      - ./ts-state:/var/lib/tailscale
    devices:
      - /dev/net/tun:/dev/net/tun
    cap_add:
      - NET_ADMIN
  app:
    image: alpine:3.22
    command: sleep infinity
    network_mode: service:ts

TS_ACCEPT_DNS: "true" fixes the names. tailscaled rewrites the shared /etc/resolv.conf to 100.100.100.100, and the app sees it. You cannot do this from the app's side. Adding dns: to it failed with conflicting options: dns and the network mode.

That DNS change has a cost, and it depends on one tailnet setting. Tailscale's docs say that with "Override DNS servers" on, devices "ignore their local DNS settings and always use the global nameservers defined for the tailnet". Headscale calls the same setting override_local_dns. With it on, our app lost its compose service names: wget: bad address 'headscale:8080'. With it off, the default, the app resolved the tailnet names and the compose names both.

Userspace mode can work too, through a proxy. With TS_OUTBOUND_HTTP_PROXY_LISTEN: 127.0.0.1:1055 on the sidecar, and http_proxy=http://127.0.0.1:1055 set in the app, wget reached the peer by IP, by full name and by short name. That suits apps that honor a proxy setting. It does nothing for apps that do not.

Read the error#

What you seeCauseFix
Host can ping 100.x; container lookup works, connection times outStateful filtering ontailscale set --stateful-filtering=false
Default bridge container: no servers could be reached, public names tooStateful filtering onSame
Host cannot ping 100.x; tailscale ping worksHost Tailscale in userspace modeKernel mode: distribution package, or TS_USERSPACE=false, tun device, NET_ADMIN
Sidecar app: timeout to 100.x and NXDOMAINSidecar on image defaultsTS_USERSPACE=false with tun and NET_ADMIN, or the HTTP proxy
Sidecar app: address works, name NXDOMAINTS_ACCEPT_DNS unsetTS_ACCEPT_DNS=true on the sidecar
Sidecar app: bad address for a compose serviceAccepted DNS with Override DNS servers onTurn the override off
Couldn't load match 'conntrack' in tailscaled logsFiltering on, conntrack package missing (issue 12087)Install conntrack, or turn filtering off

When this answer stops applying#

This page covers containers reaching out to the tailnet. The other way round, tailnet devices reaching a container, is a published port on the host or a subnet route. KB 1019 covers --advertise-routes, and the host side of a published port is in Reaching a host service from a container. The lab wrote /etc/resolv.conf directly. A host on systemd-resolved was not tested, and there Docker reads a different file, so check the container's own copy for 100.100.100.100. We did not run the official image in host mode, Docker Desktop, rootless Docker or Podman. Tailscale's own control server was not used. Our lab used Headscale, and how it differs from running plain WireGuard is in Headscale vs wg-easy. Tailnet access rules can block any of this on their own, and nothing here tests them.

What to do next#

Ping a 100.x address from the host first. If that fails, fix the host and leave Docker alone. If it works, run nslookup and then wget from a throwaway container on the app's network, and match the result to the table above. If you are still choosing how to get remote access in the first place, Remote access without port forwarding sets out the options, and Headscale has the costs of running your own control server.

Questions#

Do I need network_mode host for a container to use Tailscale on the host?

No. With Tailscale in kernel mode on the host, a container on the default bridge or on a compose network reaches tailnet devices through Docker's normal outbound path. In our tests both reached a peer by 100.x address, by full MagicDNS name and by short name. The peer saw the connections come from the host's own tailnet address. Host networking only helps when the host has no tailscale0 interface, and then the real fix is kernel mode.

Is stateful filtering on by default in Tailscale?

No, not since 1.66.4, released 21 May 2024. Its notes say stateful filtering is now off by default, and that it had inadvertently broken DNS resolution from containers on the host. The current client's help text also says (default false). A host can still have it on if someone set it. Run tailscale debug prefs | grep NoStatefulFiltering: false there means filtering is on.

Why does my container resolve the Tailscale name but cannot connect?

On a compose or user-defined network that is the signature of stateful filtering. Docker's embedded DNS server forwards queries from the host's own network namespace, so the lookup never crosses the firewall rule. The connection is forwarded traffic leaving through tailscale0, and the rule drops it. In our test the lookup returned the peer's address and wget then reported download timed out. tailscale set --stateful-filtering=false fixed it at once.

Can I set dns: 100.100.100.100 on a container using network_mode service?

No. Docker refuses to create it: Error response from daemon: conflicting options: dns and the network mode. A container that joins another container's network also uses that container's /etc/resolv.conf file. Our two containers reported the same ResolvConfPath. So set TS_ACCEPT_DNS=true on the Tailscale sidecar, and the app gets the MagicDNS resolver along with it.

What does Couldn't load match conntrack mean in tailscaled logs?

It comes from Tailscale 1.66.0 and 1.66.1, which turned stateful filtering on. That needs the iptables conntrack match, and hosts without the conntrack package logged Couldn't load match 'conntrack':No such file or directory. MagicDNS then broke. GitHub issue 12087 tracked it, and installing conntrack fixed it. The issue tied that need to stateful filtering, which is off by default on a current client.

Sources#

Published . Last reviewed . Found something out of date? Tell us and we will fix it and log the change.