Tailscale and Pi-Hole work together also as exit-node

# Pi-hole + Tailscale on ZimaOS: The Setup That Actually Works

**TL;DR:** Two containers, one trick. Tailscale runs in `host` network mode. Pi-hole sits on a bridge with port 53 mapped to the host. DNS queries to the Tailscale IP of your ZimaBox flow straight into Pi-hole. No sidecars, no installing Tailscale inside Pi-hole, no image hacking. And it survives a reboot.

## The problem

I run Pi-hole as an ad blocker on my ZimaOS home server. I also run a Tailscale container for remote access and an exit node. I wanted Pi-hole to serve DNS on my tailnet so I get ad blocking when I’m away from home.

Sounds simple. But none of the common approaches worked on ZimaOS:

- **Tailscale inside the Pi-hole container?** Works until the container restarts, then it’s gone. The Pi-hole Docker image has no s6-overlay — just `/bin/bash /usr/bin/start.sh` as PID 1. No hooks, no startup scripts to latch onto.

- **`docker commit` + tag override?** ZimaOS manages the container YAML (`is_uncontrolled: false`) and reverts your image change on restart. The committed image sits in Portainer as “unused” forever.

- **Sidecar pattern (`network_mode: service:tailscale`)?** Fights the ZimaOS-managed YAML and conflicts with the Tailscale container already running in host mode.

- **Editing the entrypoint in the YAML?** ZimaOS regenerates it.

- **A startup script in the Pi-hole volume?** Vanishes on reinstall.

After an evening of dead ends, the actual solution turned out to be embarrassingly simple.

## The trick

The Tailscale container on ZimaOS runs in `network_mode: host`. That means the Tailscale IP (a `100.x.x.x` address) lives on the host machine itself. Pi-hole’s port 53 is published to `0.0.0.0:53` on the host.

So when a DNS query arrives at the host’s Tailscale IP on port 53, it lands on the host’s port 53 — and Docker NATs it straight into Pi-hole.

Nothing to install inside Pi-hole. Nothing to commit. Nothing to change in the YAML beyond what the app store already gives you.

## The two container configs

### Pi-hole (`pihole.yaml`)

```yaml

services:

pihole:

cap_add:

  - NET_ADMIN

command: null

container_name: pihole

entrypoint: null

environment:

  FTLCONF_dns_listeningMode: all

  \# Replace with your own password:

  FTLCONF_webserver_api_password: <your-admin-password>

  TZ: Europe/Amsterdam

image: pihole/pihole:2026.05.0

network_mode: bridge

ports:

  - target: 80

    published: "8800"

    protocol: tcp

  - target: 53

    published: "53"

    protocol: tcp

  - target: 53

    published: "53"

    protocol: udp

  - target: 443

    published: "8443"

    protocol: tcp

restart: unless-stopped

volumes:

  - type: bind

    source: /DATA/AppData/pihole/etc/pihole/

    target: /etc/pihole

```

### Tailscale (`tailscale.yaml`)

```yaml

services:

tailscale:

cap_add:

  - NET_ADMIN

  - NET_RAW

command: null

container_name: tailscale

entrypoint:

  - /bin/sh

  - -c

  - tailscaled --state=/var/lib/tailscale/tailscaled.state & sleep 10;

    tailscale web --listen 0.0.0.0:5252

environment:

  TS_STATE_DIR: /var/lib/tailscale

image: tailscale/tailscale:stable

network_mode: host

restart: unless-stopped

volumes:

  - type: bind

    source: /DATA/AppData/tailscale

    target: /var/lib/tailscale

  - type: bind

    source: /dev/net/tun

    target: /dev/net/tun

```

Use `tailscale/tailscale:stable` rather than `latest` — it’s the recommended track and ZimaOS will pull the newest stable release on every restart, so security updates arrive automatically.

## Setup steps

1. **Install Pi-hole from the ZimaOS app store.** Make sure the YAML has `FTLCONF_dns_listeningMode: all` (Permit all origins) and the standard port mappings for 53/tcp and 53/udp.

2. **Install the Tailscale container from the ZimaOS app store.** Leave it in `network_mode: host` — do not change this. Set the image to `tailscale/tailscale:stable`.

3. **Get the Tailscale IP of your host:**

```bash

sudo docker exec tailscale tailscale ip -4

```

4. **Verify Pi-hole answers on that IP:**

```bash

sudo docker exec pihole nslookup google.com

```

## Tailscale admin console

Go to the DNS page in the Tailscale admin console. Add your host’s Tailscale IP as a **global nameserver**. **Also add a fallback** (like `1.1.1.1`). This is important.

If you enable “Override local DNS” without a fallback, a Pi-hole outage will kill internet for every device on your tailnet. Ask me how I know. With a fallback, Tailscale tries Pi-hole first and falls back to Cloudflare if it doesn’t get an answer.

## Exit node

Want to route all traffic through home? One command:

```bash

sudo docker exec tailscale tailscale set --advertise-exit-node

```

Then approve it in the admin console: **Machines → your server → ellipsis menu → Edit route settings → check “Use as exit node” → Save**.

Make sure IP forwarding is on:

```bash

sudo sysctl net.ipv4.ip_forward # must be 1

```

## Why this works

It exploits the fact that `host` network mode puts the Tailscale IP on the host itself, and Docker’s port mapping handles the rest. The two containers never touch each other. They don’t need to.

- Pi-hole restarts? Fine. Tailscale keeps running, DNS keeps working.

- Tailscale restarts? Fine. Pi-hole keeps running, local DNS via the router keeps working.

- Power outage? Everything comes back up as ZimaOS restarts the containers. No custom images, no modified entrypoints, no fragile state.

## The moral

Sometimes the simplest setup is the one that survives. I spent hours trying to wire these two containers together when they were already connected by the host they both run on.

I hope this saves someone else the same evening.

It looks good. Thank you for sharing.