Quick Start
Get DockTail running in under a minute. Add it to your Docker Compose file alongside your services.
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.net
That's it! See Configuration for OAuth setup (recommended) or running without credentials.
Installation
Tailscale on Host
For systems with Tailscale already 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}
/var/run/tailscale directory rather than the socket file directly. When tailscaled restarts, it recreates the socket with a new inode — a file bind mount would go stale, but a directory mount stays in sync.
The host machine must advertise a tag that matches your ACL auto-approvers (see Admin Setup):
sudo tailscale up --advertise-tags=tag:server --reset
--reset flag briefly drops the Tailscale connection. If connected via SSH over Tailscale, your session will be interrupted momentarily.
Tailscale Sidecar
For systems without Tailscale on the host — runs Tailscale as a container:
services:
tailscale:
image: tailscale/tailscale:latest
hostname: docktail-host
environment:
- TS_AUTHKEY=${TAILSCALE_AUTH_KEY}
- TS_EXTRA_ARGS=--advertise-tags=tag:server
- 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). The sidecar must advertise tag:server so it can satisfy the ACL auto-approver.
Configuration
DockTail works in three modes. Choose based on your needs.
OAuth Credentials Recommended
OAuth lets DockTail auto-create services in your Tailscale Admin Console. No manual setup required.
- Go to Tailscale Admin → Settings → OAuth clients
- Create a new OAuth client with these permissions, scoped to your server tag:
- General → Services: Write
- Devices → Core: Write
- Keys → Auth Keys: Write (sidecar method only)
- Add to your DockTail environment:
environment:
- TAILSCALE_OAUTH_CLIENT_ID=your-client-id
- TAILSCALE_OAUTH_CLIENT_SECRET=your-client-secret
API Key
Also auto-creates services, but expires every 90 days.
- Go to Tailscale Admin → Settings → Keys
- Generate an API key
- Add to your DockTail environment:
environment:
- TAILSCALE_API_KEY=tskey-api-...
No Credentials (Manual Mode)
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.
- Go to Tailscale Admin → Services
- Create a service for each container you want to expose
- Configure ACL auto-approvers (see ACL Configuration)
Admin Setup
After deploying DockTail, configure your Tailscale Admin Console for services to work.
ACL Configuration
Services require tag definitions in tagOwners and auto-approvers to allow the host to advertise them. Go to Access Controls and add both blocks:
{
"tagOwners": {
"tag:server": ["autogroup:admin"],
"tag:container": ["tag:server"]
},
"autoApprovers": {
"services": {
"tag:container": ["tag:server"]
}
}
}
tagOwners — Tailscale rejects references to undefined tags.
How the tags work:
tag:server— assigned to the host machine (or sidecar auth key). Identifies who can advertise services.tag:container— default tag DockTail assigns to services. Identifies what is being advertised.
Approve Services
The first time a new service is advertised, it must be manually approved:
- Go to the Services tab
- Find the newly advertised service
- Approve it to allow traffic
After the initial approval, the service works automatically on subsequent container restarts. With OAuth or API key, service definitions are auto-created, but the first approval may still be required depending on your ACL configuration.
Labeling Containers
DockTail watches for containers with docktail.* labels and automatically advertises them as Tailscale services. Each labeled container becomes its own independent service.
Direct Container IP Proxying 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"
Container IPs are automatically detected and updated on restart. Set docktail.service.direct=false to use published port bindings instead.
Service Labels
| 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 |
docktail.service.network |
No | bridge |
Docker network for container IP |
docktail.service.protocol |
No | Smart* | Container protocol |
docktail.service.service-port |
No | Smart** | Port Tailscale listens on |
docktail.service.service-protocol |
No | Smart*** | Tailscale-facing protocol |
docktail.tags |
No | tag:container |
Comma-separated ACL tags |
Smart Defaults:
- *
protocol:httpsif container port is 443, otherwisehttp - **
service-port:443if service-protocol ishttps, otherwise80 - ***
service-protocol:httpsif service-port is 443, matches protocol for TCP, otherwisehttp
Funnel Labels (Public Internet Access)
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
Examples
Web Application
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"
HTTPS with Auto TLS
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 HTTPS
Access: https://api.your-tailnet.ts.net
Database (TCP)
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"
Multiple Services from One Container
Use numbered labels (docktail.service.N.*) to expose multiple Tailscale services from a single container. Useful for VPN gateway containers where multiple apps route through one container.
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, service-protocol.
Custom Docker Network
services:
app:
image: myapp:latest
networks:
- backend
labels:
- "docktail.service.enable=true"
- "docktail.service.name=app"
- "docktail.service.port=3000"
- "docktail.service.network=backend"
networks:
backend:
Legacy Mode (Published Ports)
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"
Public Website with Funnel
services:
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
Reference
Environment Variables
| Variable | Default | Description |
|---|---|---|
TAILSCALE_OAUTH_CLIENT_ID |
- | OAuth Client ID (enables auto-service-creation) |
TAILSCALE_OAUTH_CLIENT_SECRET |
- | OAuth Client Secret |
TAILSCALE_API_KEY |
- | API Key (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.
Supported Protocols
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
How It Works
┌────────────────────────────────────────────────────────┐
│ 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