Unleash your Containers as Tailscale Services
Automatically expose Docker containers as Tailscale Services using label-based configuration - zero-config service mesh for your dockerized services.
- Automatically discover and expose Docker containers as Tailscale Services
- Auto-create service definitions via Tailscale API (with OAuth or API key)
- HTTP, HTTPS and TCP protocols
- Tailscale HTTPS with automatic TLS certificates
- Tailscale Funnel support (public internet access)
- Multiple services from a single container
- Automatic cleanup when containers stop
- Runs entirely in a stateless Docker container
services:
docktail:
image: ghcr.io/marvinvr/docktail:latest
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- /var/run/tailscale:/var/run/tailscale
environment:
# Optional but recommended - enables auto-service-creation
- TAILSCALE_OAUTH_CLIENT_ID=${TAILSCALE_OAUTH_CLIENT_ID}
- TAILSCALE_OAUTH_CLIENT_SECRET=${TAILSCALE_OAUTH_CLIENT_SECRET}
myapp:
image: nginx:latest
# No ports needed! DockTail proxies directly to container IP
labels:
- "docktail.service.enable=true"
- "docktail.service.name=myapp"
- "docktail.service.port=80"docker compose up -d
curl http://myapp.your-tailnet.ts.netThat's it! See Configuration for OAuth setup (recommended) or running without credentials.
DockTail works in three modes. Choose based on your needs:
OAuth lets DockTail auto-create services in your Tailscale Admin Console. No manual setup required.
Setup:
- Go to Tailscale Admin Console → Settings → OAuth clients
- Create a new OAuth client with scope
alland your service tags (e.g.,tag:container) - Add to your DockTail environment:
environment:
- TAILSCALE_OAUTH_CLIENT_ID=your-client-id
- TAILSCALE_OAUTH_CLIENT_SECRET=your-client-secretBenefits:
- Services auto-created when containers start
- Never expires (unlike API keys)
- Proper tag-based ACL support
Also auto-creates services, but expires every 90 days.
Setup:
- Go to Tailscale Admin Console → Settings → Keys
- Generate an API key
- Add to your DockTail environment:
environment:
- TAILSCALE_API_KEY=tskey-api-...DockTail works without any credentials for basic use. Services are advertised locally via the Tailscale CLI, but you must manually create service definitions in the Admin Console.
Manual setup required:
- Go to Tailscale Admin Console → Services
- Create a service for each container you want to expose
- Configure ACL auto-approvers (see ACL Configuration)
For systems with Tailscale installed on the host:
services:
docktail:
image: ghcr.io/marvinvr/docktail:latest
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- /var/run/tailscale:/var/run/tailscale
environment:
# Optional but recommended - enables auto-service-creation
- TAILSCALE_OAUTH_CLIENT_ID=${TAILSCALE_OAUTH_CLIENT_ID}
- TAILSCALE_OAUTH_CLIENT_SECRET=${TAILSCALE_OAUTH_CLIENT_SECRET}Note: We mount the
/var/run/tailscaledirectory rather than the socket file directly. Whentailscaledrestarts, it recreates the socket with a new inode — a file bind mount would go stale, but a directory mount stays in sync automatically.
Host tag requirement: The host machine running Tailscale must advertise a tag that matches your ACL auto-approvers (see Tailscale Admin Setup). For example:
sudo tailscale up --advertise-tags=tag:server --resetWarning: The
--resetflag briefly drops the Tailscale connection. If you are connected via SSH over Tailscale, your session will be interrupted momentarily. The connection will restore automatically once Tailscale reconnects.
When using the sidecar setup below, the sidecar container handles its own tags via TS_AUTHKEY, so this step is not needed.
For systems without Tailscale installed on the host:
services:
tailscale:
image: tailscale/tailscale:latest
hostname: docktail-host
environment:
- TS_AUTHKEY=${TAILSCALE_AUTH_KEY}
- TS_STATE_DIR=/var/lib/tailscale
- TS_SOCKET=/var/run/tailscale/tailscaled.sock
volumes:
- tailscale-state:/var/lib/tailscale
- tailscale-socket:/var/run/tailscale
- /dev/net/tun:/dev/net/tun
cap_add:
- NET_ADMIN
- SYS_MODULE
network_mode: host
restart: unless-stopped
docktail:
image: ghcr.io/marvinvr/docktail:latest
depends_on:
- tailscale
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- tailscale-socket:/var/run/tailscale
environment:
# Optional but recommended - enables auto-service-creation
- TAILSCALE_OAUTH_CLIENT_ID=${TAILSCALE_OAUTH_CLIENT_ID}
- TAILSCALE_OAUTH_CLIENT_SECRET=${TAILSCALE_OAUTH_CLIENT_SECRET}
volumes:
tailscale-state:
tailscale-socket:Set TAILSCALE_AUTH_KEY to authenticate the Tailscale container (generate at Tailscale Admin → Settings → Keys).
After deploying DockTail, you need to configure a few things in the Tailscale Admin Console for services to work.
Services require ACL auto-approvers to allow the host machine to advertise them. Go to Access Controls and add an autoApprovers block:
{
"autoApprovers": {
"services": {
"tag:container": ["tag:server"]
}
}
}This allows machines tagged tag:server to advertise services tagged tag:container (the default DockTail tag). Adjust the tags to match your setup — the right side must match the tag on your host machine (or sidecar auth key), and the left side must match the docktail.tags label on your containers (defaults to tag:container).
The first time a new service is advertised, it must be manually approved in the Tailscale Admin Console:
- Go to the Services tab
- Find the newly advertised service
- Approve it to allow traffic
After the initial approval, the service will continue to work automatically on subsequent container restarts. If you are using OAuth or API key mode, service definitions are auto-created, but the first approval may still be required depending on your ACL configuration.
By default, DockTail proxies directly to container IPs on the Docker bridge network. No port publishing required!
services:
myapp:
image: nginx:latest
# No ports needed!
labels:
- "docktail.service.enable=true"
- "docktail.service.name=myapp"
- "docktail.service.port=80"DockTail automatically detects the container's IP address and configures Tailscale to proxy directly to it. When containers restart and get new IPs, DockTail automatically updates the configuration.
Opt-out: Set docktail.service.direct=false to use published port bindings instead (legacy behavior).
| Label | Required | Default | Description |
|---|---|---|---|
docktail.service.enable |
Yes | - | Enable DockTail for container |
docktail.service.name |
Yes | - | Service name (e.g., web, api) |
docktail.service.port |
Yes | - | Container port to proxy to |
docktail.service.direct |
No | true |
Proxy directly to container IP (no port publishing needed) |
docktail.service.network |
No | bridge |
Docker network to use for container IP |
docktail.service.protocol |
No | Smart* | Container protocol: http, https, https+insecure, tcp, tls-terminated-tcp |
docktail.service.service-port |
No | Smart** | Port Tailscale listens on |
docktail.service.service-protocol |
No | Smart*** | Tailscale protocol: http, https, tcp |
docktail.tags |
No | tag:container |
Comma-separated tags for ACLs |
Smart Defaults:
- *
protocol:httpsif container port is 443, otherwisehttp - **
service-port:443if service-protocol ishttps, otherwise80 - ***
service-protocol:httpsif service-port is 443, matchesprotocolfor TCP, otherwisehttp
Funnel exposes your service to the public internet. Independent from service labels.
| Label | Required | Default | Description |
|---|---|---|---|
docktail.funnel.enable |
Yes | false |
Enable Tailscale Funnel |
docktail.funnel.port |
Yes | - | Container port |
docktail.funnel.funnel-port |
No | 443 |
Public port (443, 8443, or 10000) |
docktail.funnel.protocol |
No | https |
Protocol: https, tcp, tls-terminated-tcp |
Notes:
- Only ONE funnel per port (Tailscale limitation)
- Uses machine hostname, not service name:
https://<machine>.<tailnet>.ts.net
services:
nginx:
image: nginx:latest
# No ports needed - DockTail proxies directly to container IP
labels:
- "docktail.service.enable=true"
- "docktail.service.name=web"
- "docktail.service.port=80"services:
api:
image: myapi:latest
labels:
- "docktail.service.enable=true"
- "docktail.service.name=api"
- "docktail.service.port=3000"
- "docktail.service.service-port=443" # Auto-enables HTTPSAccess: https://api.your-tailnet.ts.net
services:
postgres:
image: postgres:16
labels:
- "docktail.service.enable=true"
- "docktail.service.name=db"
- "docktail.service.port=5432"
- "docktail.service.protocol=tcp"
- "docktail.service.service-port=5432"A single container can expose multiple separate Tailscale services using numbered labels (docktail.service.N.*). Each indexed entry requires its own name and port, and becomes an independent service. Tags and network are inherited from the primary config.
This is useful for VPN gateway containers (e.g., gluetun) where multiple apps route through a single container but each needs its own Tailscale service.
services:
gluetun:
image: gluetun:latest
labels:
- "docktail.service.enable=true"
- "docktail.service.name=qbittorrent"
- "docktail.service.port=8000"
- "docktail.service.1.name=bitmagnet"
- "docktail.service.1.port=8001"Per-index overridable labels: name (required), port (required), service-port, protocol, and service-protocol.
services:
app:
image: myapp:latest
networks:
- backend
labels:
- "docktail.service.enable=true"
- "docktail.service.name=app"
- "docktail.service.port=3000"
- "docktail.service.network=backend" # Specify which network to use
networks:
backend:services:
app:
image: myapp:latest
ports:
- "8080:3000" # Required when direct=false
labels:
- "docktail.service.enable=true"
- "docktail.service.name=app"
- "docktail.service.port=3000"
- "docktail.service.direct=false" # Use published port instead of container IPservices:
website:
image: nginx:latest
labels:
- "docktail.service.enable=true"
- "docktail.service.name=website"
- "docktail.service.port=80"
- "docktail.service.service-port=443"
- "docktail.funnel.enable=true"
- "docktail.funnel.port=80"Access:
- Tailnet:
https://website.your-tailnet.ts.net - Public:
https://your-machine.your-tailnet.ts.net
| Variable | Default | Description |
|---|---|---|
TAILSCALE_OAUTH_CLIENT_ID |
- | OAuth Client ID (optional, enables auto-service-creation) |
TAILSCALE_OAUTH_CLIENT_SECRET |
- | OAuth Client Secret (optional, enables auto-service-creation) |
TAILSCALE_API_KEY |
- | API Key (optional alternative to OAuth, expires 90 days) |
TAILSCALE_TAILNET |
- |
Tailnet ID (defaults to key's tailnet) |
DEFAULT_SERVICE_TAGS |
tag:container |
Default tags for services |
LOG_LEVEL |
info |
Logging level (debug, info, warn, error) |
RECONCILE_INTERVAL |
60s |
State reconciliation interval |
DOCKER_HOST |
unix:///var/run/docker.sock |
Docker daemon socket |
TAILSCALE_SOCKET |
/var/run/tailscale/tailscaled.sock |
Tailscale daemon socket |
If both OAuth and API key are set, OAuth takes precedence.
Tailscale-facing (service-protocol):
http- Layer 7 HTTPhttps- Layer 7 HTTPS (auto TLS)tcp- Layer 4 TCPtls-terminated-tcp- Layer 4 with TLS termination
Container-facing (protocol):
http- HTTP backendhttps- HTTPS with valid certificatehttps+insecure- HTTPS with self-signed certificatetcp- TCP backendtls-terminated-tcp- TCP with TLS termination
See Tailscale Admin Setup for the required ACL auto-approver configuration and service approval steps.
┌────────────────────────────────────────────────────────┐
│ Docker Host │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ DockTail │────────▶│ Tailscale Daemon │ │
│ │ (Container) │ CLI │ (Host Process) │ │
│ └────────┬─────────┘ └────────┬─────────┘ │
│ │ │ │
│ │ Docker Socket │ Proxies to │
│ │ Monitoring │ container IP │
│ ▼ ▼ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ App Container │◀────────│ 172.17.0.3:80 │ │
│ │ Port 80 │ │ (bridge network)│ │
│ │ No ports needed │◀────────│ │ │
│ └──────────────────┘ └──────────────────┘ │
│ │
└────────────────────────────────────────────────────────┘
│
│ Tailscale Network
▼
┌─────────────────────┐
│ Tailnet Clients │
│ Access services: │
│ web.tailnet.ts.net │
└─────────────────────┘
- Container Discovery - Monitors Docker events for container start/stop
- Label Parsing - Extracts service configuration from container labels
- IP Detection - Gets container IP from Docker network settings (default: bridge)
- Config Generation - Creates Tailscale service config proxying to container IP
- Service Advertisement - Executes Tailscale CLI to advertise services
- Control Plane Sync - If OAuth/API key configured, creates service definitions via API
- Reconciliation - Periodically syncs state; auto-updates when container IPs change
Notes:
- DockTail does NOT delete service definitions from the API when containers stop (conservative deletion strategy)
- Container IP changes on restart are handled automatically during reconciliation
go build -o docktail .
docker build -t docktail:latest .
./docktail- Tailscale Services Documentation
- Tailscale Funnel Documentation
- Tailscale Service Configuration Reference
- Docker SDK for Go
AGPL v3
By @marvinvr
