$docker.recipes
·10 min read·Updated February 2026

Tailscale with Docker Compose: Secure Remote Access Without Port Forwarding

Access your Docker services from anywhere without opening ports. Use Tailscale as a sidecar container, subnet router, or with Funnel for public access.

tailscalevpnsecuritydocker-composenetworking

01The Zero-Config VPN

The traditional way to access self-hosted services remotely involves opening ports on your router, configuring dynamic DNS, setting up SSL certificates, and hoping nobody finds your exposed services on Shodan. It works, but it's a lot of moving parts and every open port is an attack surface. Tailscale takes a different approach. It creates a WireGuard-based mesh VPN between your devices — no port forwarding, no dynamic DNS, no public IP needed. Every device gets a stable IP address on your tailnet, and connections are peer-to-peer when possible (meaning traffic between your laptop and home server doesn't route through Tailscale's infrastructure). I switched from a manual WireGuard setup to Tailscale about a year ago and the experience is night and day. WireGuard is technically superior in some ways (no dependency on a third party), but Tailscale's ease of use is unmatched. Adding a new device takes 30 seconds. Key rotation is automatic. NAT traversal just works. For Docker specifically, Tailscale has excellent integration patterns. You can run it as a sidecar, as a subnet router, or use Tailscale Serve/Funnel for public access. Here's how each works.

02Tailscale as a Docker Sidecar

The sidecar pattern runs Tailscale alongside a specific service, giving that service its own address on your tailnet. This is the simplest approach when you want to expose one or two services.
[docker-compose.yml]
1services:
2 tailscale:
3 image: tailscale/tailscale:latest
4 hostname: my-app-ts
5 environment:
6 - TS_AUTHKEY=${TS_AUTHKEY}
7 - TS_STATE_DIR=/var/lib/tailscale
8 - TS_SERVE_CONFIG=/config/serve.json
9 - TS_EXTRA_ARGS=--advertise-tags=tag:container
10 volumes:
11 - tailscale-state:/var/lib/tailscale
12 - /dev/net/tun:/dev/net/tun
13 - ./ts-serve-config.json:/config/serve.json:ro
14 cap_add:
15 - net_admin
16 - sys_module
17 restart: unless-stopped
18
19 app:
20 image: myapp:latest
21 network_mode: service:tailscale
22 # App is now accessible via tailscale hostname
23 # No ports needed — traffic routes through tailscale
24 depends_on:
25 - tailscale
26
27volumes:
28 tailscale-state:

Use TS_EXTRA_ARGS=--advertise-tags=tag:container to tag your Docker services in Tailscale ACLs. This lets you create access rules like 'only my personal devices can reach containers tagged tag:container' — much better than IP-based rules that break when addresses change.

03Exposing Your Docker Network via Subnet Router

The subnet router pattern is more powerful: instead of exposing individual services, you expose your entire Docker network to your tailnet. Any device on your tailnet can access any Docker service by its container IP or Docker DNS name. This is my preferred approach because it requires only one Tailscale container regardless of how many services you run.
[docker-compose.yml]
1services:
2 tailscale-router:
3 image: tailscale/tailscale:latest
4 hostname: docker-router
5 environment:
6 - TS_AUTHKEY=${TS_AUTHKEY}
7 - TS_STATE_DIR=/var/lib/tailscale
8 - TS_ROUTES=172.18.0.0/16
9 - TS_EXTRA_ARGS=--advertise-exit-node
10 volumes:
11 - ts-router-state:/var/lib/tailscale
12 - /dev/net/tun:/dev/net/tun
13 cap_add:
14 - net_admin
15 - sys_module
16 networks:
17 - app-network
18 restart: unless-stopped
19
20 app:
21 image: myapp:latest
22 ports:
23 - "8080:8080"
24 networks:
25 - app-network
26
27 db:
28 image: postgres:16-alpine
29 networks:
30 - app-network
31
32networks:
33 app-network:
34 ipam:
35 config:
36 - subnet: 172.18.0.0/16
37
38volumes:
39 ts-router-state:

04Tailscale Funnel and Serve

Tailscale Serve exposes a service to your tailnet with automatic HTTPS. Tailscale Funnel goes further — it exposes a service to the public internet through Tailscale's infrastructure, with a valid SSL certificate and no port forwarding. Funnel is perfect for sharing a development preview with a client, running a webhook endpoint, or hosting a low-traffic public service. Your server stays behind NAT with no open ports, and Tailscale handles TLS termination. To use Serve/Funnel with Docker, create a serve configuration file that maps your tailnet hostname to the container's port. The Tailscale sidecar reads this config and sets up the reverse proxy automatically. The experience is magical: run docker compose up and your service is available at https://my-app-ts.tail12345.ts.net with a valid certificate. No Traefik, no Let's Encrypt, no DNS configuration.

Tailscale Funnel exposes your service to the entire internet, not just your tailnet. Treat it like any public-facing service — ensure authentication is in place, rate limiting is configured, and you're not accidentally exposing admin panels or sensitive data. Funnel is not a substitute for proper application security.

05Connecting Multiple Docker Hosts

The real power of Tailscale shows when you run Docker on multiple machines. I have services split across a home server, a VPS, and a Raspberry Pi. With Tailscale subnet routers on each machine, every Docker container can reach containers on the other machines as if they were on the same network. My setup: Prometheus on the VPS scrapes metrics from node-exporters running on my home server and Pi. Loki on the home server receives logs from Promtail instances on all three machines. Grafana on the VPS queries both and gives me a unified dashboard. Without Tailscale, this would require public IPs, firewall rules, and VPN tunnels. With Tailscale, each server just joins the tailnet and the routing works automatically. The traffic is encrypted end-to-end with WireGuard, and the connections are peer-to-peer so latency is minimal. For this multi-server pattern, I use ACL tags to control which servers can talk to which. My VPS can reach monitoring ports on the home server, but not database ports. The Pi can push logs to the home server, but nothing else. Least-privilege networking without complex firewall rules.

06Tailscale vs WireGuard vs Cloudflare Tunnel

All three solve the "access my stuff remotely" problem, but they make different trade-offs: Tailscale: Easiest setup, best NAT traversal, free for up to 100 devices. You trust Tailscale's coordination server (but not your traffic — that's encrypted end-to-end). Best for: personal use, small teams, connecting multiple locations. WireGuard (manual): Maximum control, no third-party dependency, slightly better performance. You manage keys, endpoints, and routing yourself. Best for: security purists, when you have a public IP, when you want zero external dependencies. Cloudflare Tunnel: Free, excellent for exposing web services publicly. Traffic routes through Cloudflare's network (they can inspect it). No peer-to-peer — everything goes through Cloudflare. Best for: public-facing web services, when you're already using Cloudflare for DNS, when you want DDoS protection. I use Tailscale for internal access (admin panels, databases, SSH) and Cloudflare Tunnel for the few services I want publicly accessible (my blog, a status page). This gives me the best of both worlds: zero exposed ports for private services, and Cloudflare's CDN and DDoS protection for public ones. Headscale is worth mentioning — it's a self-hosted implementation of Tailscale's coordination server. If you want Tailscale's ease of use without depending on their infrastructure, Headscale gives you that. The trade-off is more complex setup and you lose some features like Funnel.

About the Author

Frank Pegasus

DevOps engineer and self-hosting enthusiast with over a decade of experience running containerized workloads in production. Creator of docker.recipes.