# 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.