Skip to main content

Junyi's Lab

nerdctl + Tailscale Sidecar Exit Node: A Complete Pitfall Guide

Table of Contents

In containerized deployments, sometimes you need all outbound traffic from a container to go through a specific network exit. Tailscale’s sidecar pattern makes this possible: a Tailscale container acts as a sidecar, other containers share its network namespace, and traffic flows through a WireGuard tunnel via a remote exit node.

This setup is well-documented for Docker Compose, but migrating to nerdctl (containerd) introduced a series of pitfalls. Here’s what I ran into so others can avoid the same traps.

# The Goal

Route all outbound traffic from any container through a Tailscale sidecar, forwarding via a remote exit node:

nerdctl + Tailscale Sidecar Architecture

For verification, I used nicolaka/netshoot as a test container — it comes with curl, dig, tcpdump, iperf, and a full suite of networking tools out of the box.

# Pitfall 1: nerdctl compose doesn’t support network_mode: service:xxx

Docker Compose supports network_mode: service:<service_name>, which automatically resolves the service name to the corresponding container’s network namespace. nerdctl compose hasn’t implemented this translation layer and throws an error:

unsupported network_mode: service:tailscale-sidecar

Fix: Use network_mode: container:<container_name> instead, and pin the sidecar container’s name with container_name.

services:
  tailscale-sidecar:
    container_name: tailscale-sidecar  # Pin the name
    # ...
  netshoot:
    network_mode: container:tailscale-sidecar  # Use container name, not service name

Without a fixed container_name, nerdctl compose auto-generates names like blockchain-tailscale-sidecar-1, causing reference mismatches.

# Pitfall 2: Undefined tag in Tailscale ACL

After starting, the Tailscale container immediately exits with:

Received error: requested tags [tag:container] are invalid or not permitted

Fix: Define the tag in your Tailscale admin console’s ACL policy and select it when generating the auth key:

"tagOwners": {
    "tag:container": ["autogroup:admin"]
}

# Pitfall 3: Missing SYS_MODULE capability

This one is the sneakiest. The container joins the tailnet fine, tailscale ping works, but after enabling the exit node, TCP traffic is completely broken — only ICMP gets through.

The root cause: kernel mode (TS_USERSPACE=false) needs to load kernel modules like xt_mark and nf_nat to set up iptables forwarding rules. Without SYS_MODULE capability, these modules can’t load. The iptables rules are written but don’t take effect, resulting in working ICMP but broken TCP.

Fix:

cap_add:
  - net_admin
  - SYS_MODULE  # Essential

# Pitfall 4: DNS failure in exit node mode

With the exit node enabled, all outbound traffic goes through the WireGuard tunnel to the remote exit node. But /etc/resolv.conf still points to local DNS servers (e.g., 10.0.2.3), which are unreachable from the exit node’s perspective. DNS queries naturally fail.

The symptom: curl ifconfig.me reports Could not resolve host, while direct IP access hangs or returns errors.

Fix: Let Tailscale take over DNS so queries also go through the tunnel:

environment:
  - TS_ACCEPT_DNS=true

# Final Working compose.yaml

services:
  tailscale-sidecar:
    image: tailscale/tailscale:latest
    container_name: tailscale-sidecar
    hostname: tailscale-sidecar
    environment:
      - TS_AUTHKEY=${TS_AUTHKEY}
      - TS_EXTRA_ARGS=--advertise-tags=tag:container --exit-node=${TS_EXITNODE}
      - TS_STATE_DIR=/var/lib/tailscale
      - TS_USERSPACE=false
      - TS_ACCEPT_DNS=true
    volumes:
      - ${PWD}/tailscale-sidecar/state:/var/lib/tailscale
    devices:
      - /dev/net/tun:/dev/net/tun
    cap_add:
      - net_admin
      - SYS_MODULE
    restart: unless-stopped

  netshoot:
    image: nicolaka/netshoot
    depends_on:
      - tailscale-sidecar
    network_mode: container:tailscale-sidecar
    command: sleep infinity

Prepare a .env file in the same directory:

TS_AUTHKEY=tskey-auth-xxxxx
TS_EXITNODE=100.x.x.x

# Verification

nicolaka/netshoot comes with plenty of network tools, perfect for validation:

# Check exit IP to confirm traffic goes through the exit node
nerdctl exec -it netshoot curl ifconfig.me

# DNS resolution test
nerdctl exec -it netshoot dig google.com

# View routing table
nerdctl exec -it netshoot ip route show table all

# Packet capture for debugging
nerdctl exec -it netshoot tcpdump -i any -n

# Tailscale status checks
nerdctl exec -it tailscale-sidecar tailscale status
nerdctl exec -it tailscale-sidecar tailscale exit-node status
nerdctl exec -it tailscale-sidecar tailscale ping <exit-node-ip>

# Debugging Methodology

The debugging process itself is worth reviewing. When facing a compound problem like “exit node doesn’t work”, the effective approach is to isolate layer by layer:

  1. Verify tunnel connectivity first: tailscale ping confirms whether the WireGuard tunnel itself works.
  2. Comparative testing: Disable the exit node to determine if the issue is with the exit node or the underlying network.
  3. Differentiate protocols: ICMP works but TCP doesn’t — points to iptables/kernel module issues.
  4. Separate DNS from connectivity: Use direct IP access to bypass DNS and isolate each issue.
  5. Compare against known-working configs: Spotting that a previously working setup had SYS_MODULE immediately pinpointed the problem.

Each step narrows the scope, avoiding wasted time in the wrong direction.

# Final Thoughts

nerdctl compose has matured as a Docker Compose alternative, but gaps remain in edge-case compatibility. The missing network_mode: service: support and different container naming conventions are easy migration traps. For Tailscale in containers with exit node routing, SYS_MODULE and TS_ACCEPT_DNS are two easily overlooked but essential configurations. Hope this write-up helps anyone hitting the same issues.