DockTail Documentation
DockTail exposes Docker containers as Tailscale Services using label-based configuration. It watches Docker events, reads docktail.* labels, and advertises matching containers through the local Tailscale daemon.
What DockTail Does
- Discovers labeled Docker containers automatically.
- Proxies directly to container IPs by default, so app containers do not need published Docker ports.
- Advertises HTTP, HTTPS, TCP, and TLS-terminated TCP services through Tailscale.
- Supports Tailscale HTTPS with automatic certificates.
- Supports Tailscale Funnel for public internet access.
- Supports multiple Tailscale services from one container.
- Reconciles state when containers restart and container IPs change.
- Runs as a stateless Docker container.
Recommended Reading Order
- Start with Quick Start for a minimal Compose setup.
- Read Installation for host Tailscale and sidecar options.
- Configure Tailscale permissions in Tailscale Admin Setup.
- Use Labels and Examples when exposing real services.
- Check Reference for all labels, environment variables, protocols, and behavior notes.
Quick Start
Add DockTail to your Docker Compose file alongside the service you want to expose:
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 automatic 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 the container IP.
labels:
- "docktail.service.enable=true"
- "docktail.service.name=myapp"
- "docktail.service.port=80"
Start the stack:
docker compose up -d
Then open the service from your tailnet:
curl http://myapp.your-tailnet.ts.net
This assumes the host is already connected to Tailscale and allowed to advertise services. If it is not, continue with Installation and Tailscale Admin Setup.
Installation
DockTail needs access to the Docker socket and a Tailscale socket. Use the host setup when Tailscale already runs on the Docker host. Use the sidecar setup when the host should not install Tailscale directly.
Tailscale On Host
Use this setup when Tailscale is already installed on the Docker 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:
- TAILSCALE_OAUTH_CLIENT_ID=${TAILSCALE_OAUTH_CLIENT_ID}
- TAILSCALE_OAUTH_CLIENT_SECRET=${TAILSCALE_OAUTH_CLIENT_SECRET}
Mount /var/run/tailscale as a directory rather than mounting the socket file directly. When tailscaled restarts, it recreates the socket with a new inode; a directory mount stays in sync.
The host machine must advertise a tag that matches your ACL auto-approvers:
sudo tailscale up --advertise-tags=tag:server --reset
The --reset flag briefly drops the Tailscale connection. If you are connected through SSH over Tailscale, your session may be interrupted until Tailscale reconnects.
Tailscale Sidecar
Use this setup when the host does not run Tailscale directly:
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:
- 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 it in the Tailscale Admin Console under Settings -> Keys. The sidecar should advertise tag:server so it can satisfy the ACL auto-approver example below.
Tailscale Admin Setup
DockTail can advertise services locally without Tailscale API credentials, but OAuth or API key credentials allow it to create service definitions automatically in the Tailscale Admin Console.
OAuth Credentials
OAuth is recommended. It enables automatic service creation and avoids expiring API keys.
- Open Tailscale Admin Console -> Settings -> OAuth clients.
- Create an OAuth client scoped to your server tag, for example
tag:server. - Grant these permissions:
- General -> Services: Write
- Devices -> Core: Write
- Keys -> Auth Keys: Write, only when using the sidecar method
- Add the credentials to DockTail:
environment:
- TAILSCALE_OAUTH_CLIENT_ID=your-client-id
- TAILSCALE_OAUTH_CLIENT_SECRET=your-client-secret
If OAuth and API key credentials are both configured, DockTail uses OAuth.
API Key
An API key also enables automatic service creation, but Tailscale API keys expire.
environment:
- TAILSCALE_API_KEY=tskey-api-...
Manual Mode
DockTail can run without credentials. It advertises services locally through the Tailscale CLI, but you must manually create service definitions in the Tailscale Admin Console and configure ACL auto-approvers.
ACL Configuration
Services require tag definitions in tagOwners and an autoApprovers.services rule that allows the host to advertise container services.
{
"tagOwners": {
"tag:server": ["autogroup:admin"],
"tag:container": ["tag:server"]
},
"autoApprovers": {
"services": {
"tag:container": ["tag:server"]
}
}
}
tag:server is assigned to the host machine or sidecar auth key that runs DockTail. tag:container is the default tag DockTail assigns to services it creates.
If you manage ACLs through GitOps, both tags must exist in tagOwners; otherwise Tailscale rejects references to undefined tags.
Approve Services
The first time a new service is advertised, it may need approval in the Tailscale Admin Console Services tab. After approval, the service continues to work across container restarts. OAuth or API key credentials can create service definitions automatically, but first approval may still be required depending on your ACL policy.
Labels
DockTail watches containers with docktail.* labels. Each labeled container can become a private Tailscale service, a public Funnel, or both. DockTail does not run your application containers; it only observes them and configures Tailscale.
Direct Container IP Proxying
By default, DockTail proxies directly to container IPs on the Docker network. No Docker port publishing is required.
services:
myapp:
image: nginx:latest
labels:
- "docktail.service.enable=true"
- "docktail.service.name=myapp"
- "docktail.service.port=80"
Set docktail.service.direct=false to use published host ports instead. This is mainly useful for legacy setups or unusual networking constraints.
Service Labels
| Label | Required | Default | Description |
|---|---|---|---|
docktail.service.enable |
Yes | - | Enable a private Tailscale service for the container. |
docktail.service.name |
Yes | - | Service name, such as web or api. |
docktail.service.port |
Yes | - | Backend container port to proxy to. |
docktail.service.direct |
No | true |
Proxy directly to container IP instead of requiring a published host port. |
docktail.service.network |
No | bridge or first available |
Docker network used for direct container IP detection. |
docktail.service.protocol |
No | Smart | Backend 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 service tags. |
Smart defaults:
docktail.service.protocoldefaults tohttpswhen the backend port is443; otherwise it defaults tohttp.docktail.service.service-portdefaults to443whenservice-protocolishttps; otherwise it defaults to80.docktail.service.service-protocoldefaults tohttpswhen the service port is443, totcpwhen the backend protocol is TCP, and otherwise tohttp.
Multiple Services From One Container
A single container can expose multiple separate Tailscale services using numbered labels:
services:
gluetun:
image: qmcgaw/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"
Each indexed service requires its own name and port. Per-index overridable labels are name, port, service-port, protocol, and service-protocol. Tags and network settings are inherited from the primary service config.
Funnel Labels
Funnel exposes a service to the public internet. It can be used together with a private DockTail service or on its own for funnel-only containers.
| Label | Required | Default | Description |
|---|---|---|---|
docktail.funnel.enable |
Yes | false |
Enable Tailscale Funnel. |
docktail.funnel.port |
Yes | - | Backend container port for Funnel traffic. |
docktail.funnel.funnel-port |
No | 443 |
Public Funnel port. HTTPS/HTTP Funnel supports 443, 8443, or 10000. |
docktail.funnel.protocol |
No | https |
Funnel protocol: http, https, tcp, or tls-terminated-tcp. |
Funnel notes:
- Tailscale supports only one active Funnel per public port on a node.
- Funnel URLs use the machine hostname, not the Tailscale service name.
- Funnel-only containers can omit
docktail.service.enableand otherdocktail.service.*labels. docktail.service.directanddocktail.service.networkstill control how DockTail reaches the backend for Funnel traffic.
Examples
These examples show the labels you add to application containers. They assume DockTail itself is already running on the same Docker host.
Web Application
services:
nginx:
image: nginx:latest
labels:
- "docktail.service.enable=true"
- "docktail.service.name=web"
- "docktail.service.port=80"
Access it at http://web.your-tailnet.ts.net.
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"
Access it at https://api.your-tailnet.ts.net.
Database Over 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"
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 Published-Port Mode
services:
app:
image: myapp:latest
ports:
- "8080:3000"
labels:
- "docktail.service.enable=true"
- "docktail.service.name=app"
- "docktail.service.port=3000"
- "docktail.service.direct=false"
Private Service Plus Public 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"
Tailnet URL: https://website.your-tailnet.ts.net
Public Funnel URL: https://your-machine.your-tailnet.ts.net
Funnel-Only Public Proxy
services:
immich-public-proxy:
image: ghcr.io/immich-app/immich-public-proxy:latest
labels:
- "docktail.funnel.enable=true"
- "docktail.funnel.port=3000"
- "docktail.funnel.funnel-port=8443"
Access it publicly at https://your-machine.your-tailnet.ts.net:8443.
Reference
Use this section when checking exact configuration names, defaults, and supported protocols.
Environment Variables
| Variable | Default | Description |
|---|---|---|
TAILSCALE_OAUTH_CLIENT_ID |
- | OAuth client ID. Enables automatic service creation when paired with the secret. |
TAILSCALE_OAUTH_CLIENT_SECRET |
- | OAuth client secret. Enables automatic service creation when paired with the client ID. |
TAILSCALE_API_KEY |
- | API key alternative to OAuth. |
TAILSCALE_TAILNET |
- |
Tailnet ID. Defaults to the credential's tailnet. |
DEFAULT_SERVICE_TAGS |
tag:container |
Default tags assigned to services. |
IGNORE_SERVICE_NAMES |
- | Comma-separated service names DockTail must not drain or clear during reconciliation or shutdown cleanup. |
LOG_LEVEL |
info |
Logging level: debug, info, warn, or 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 credentials are configured, DockTail uses OAuth.
IGNORE_SERVICE_NAMES accepts bare names like grafana and fully qualified names like svc:grafana.
Supported Protocols
Tailscale-facing docktail.service.service-protocol values:
| Value | Description |
|---|---|
http |
Layer 7 HTTP. |
https |
Layer 7 HTTPS with automatic TLS. |
tcp |
Layer 4 TCP. |
tls-terminated-tcp |
Layer 4 TCP with TLS termination. |
Container-facing docktail.service.protocol values:
| Value | Description |
|---|---|
http |
HTTP backend. |
https |
HTTPS backend with a valid certificate. |
https+insecure |
HTTPS backend with a self-signed certificate. |
tcp |
TCP backend. |
tls-terminated-tcp |
TCP backend with TLS termination. |
Funnel docktail.funnel.protocol values:
| Value | Description |
|---|---|
http |
HTTP Funnel. |
https |
HTTPS Funnel. |
tcp |
TCP Funnel. |
tls-terminated-tcp |
TLS-terminated TCP Funnel. |
Cleanup Behavior
DockTail cleans up the services it advertises locally when it shuts down. It does not delete Tailscale service definitions from the Admin Console API when containers stop; this is a conservative deletion strategy to avoid removing definitions unexpectedly.
Useful Links
- Tailscale Services documentation:
https://tailscale.com/kb/1552/tailscale-services - Tailscale Funnel documentation:
https://tailscale.com/kb/1311/tailscale-funnel - Tailscale service configuration reference:
https://tailscale.com/kb/1589/tailscale-services-configuration-file - Docker SDK for Go:
https://docs.docker.com/engine/api/sdk/
How It Works
DockTail is a reconciliation loop between Docker and Tailscale.
Docker container labels
|
v
DockTail watches Docker events
|
v
DockTail parses service and Funnel config
|
v
DockTail resolves the backend IP and port
|
v
Tailscale CLI advertises services and Funnels
|
v
Tailnet clients access container services
Reconciliation Flow
- DockTail monitors Docker events for container starts and stops.
- It extracts service configuration from container labels.
- It resolves the backend destination from Docker network settings or published ports.
- It generates Tailscale service configuration pointing to that backend.
- It executes the Tailscale CLI to advertise services and Funnels.
- If OAuth or API key credentials are configured, it creates service definitions through the Tailscale API.
- It periodically reconciles state so container IP changes are handled automatically.
Networking Model
Direct mode is the default. DockTail reaches containers through their Docker network IPs, so application containers do not need published host ports.
When docktail.service.direct=false, DockTail uses Docker published port bindings instead. In that mode, the target port must be published to the host.
Containers using network_mode: host are reached through localhost. Containers using network_mode: none cannot use direct mode.