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.
Why DockTail?
DockTail uses native Tailscale Services, not per-container Tailscale devices.
| DockTail | TSDProxy | ScaleTail | tsbridge | Plain Services | |
|---|---|---|---|---|---|
| Native Tailscale Services | ✅ | ❌ | ❌ | ❌ | ✅ |
| Configured via Docker labels | ✅ | ✅ | ❌ | ✅ | ❌ |
| Apps do not consume separate Tailscale device slots iDockTail advertises apps as services from one tagged host instead of creating a separate Tailscale device identity per app. Exact plan limits depend on Tailscale. | ✅ | ❌ | ❌ | ❌ | ✅ |
| No app port publishing | ✅ | ⚠️iDepends on proxy and Docker network setup. | ⚠️iDepends on the sidecar template and app network setup. | ⚠️iDepends on proxy and Docker network setup. | ⚠️iYou configure how the service host reaches the backend yourself. |
| Automatic Docker reconciliation | ✅ | ✅ | ❌ | ✅ | ❌ |
| Low manual setup after install | ✅ | ✅ | ⚠️iScaleTail is template-based, so each app usually starts from its own Compose recipe. | ✅ | ❌ |
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.
- Optionally reports to DockTail Cloud for multi-host monitoring and alerting. Opt-in via one environment variable; inert when unset.
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.
- See DockTail Cloud if you want monitoring and alerting across your hosts.
- 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 a Linux Docker host that is already connected to Tailscale and allowed to advertise services. If it is not, continue with Installation and Tailscale Admin Setup. On macOS and Windows the host's Tailscale daemon cannot be shared with containers, so use the Tailscale Sidecar setup instead. Rootless Docker on Linux needs an extra operator step; see Rootless Docker.
Installation
DockTail needs access to the Docker socket and a Tailscale socket. Use the host setup when Tailscale already runs on a Linux Docker host. Use the sidecar setup when the host should not install Tailscale directly, or when the host's Tailscale daemon cannot be shared with containers (macOS and Windows).
Tailscale On Host
Use this setup when Tailscale is already installed on a Linux 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 setup only works on Linux. On macOS and Windows the Tailscale app does not expose a Unix socket at /var/run/tailscale (the local API is served over a localhost TCP port instead), and Docker Desktop runs containers in a virtual machine that cannot mount host Unix sockets anyway. If DockTail logs dial unix /var/run/tailscale/tailscaled.sock: connect: no such file or directory, switch to the Tailscale Sidecar setup below.
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.
Rootless Docker
When Docker runs rootless, the DockTail container is your user, not root. Host tailscaled then rejects serve-config writes unless that user is the Tailscale operator:
sudo tailscale set --operator=$USER
tailscale set --operator does not drop the connection. The node still needs the ACL tag from the host setup above. If it is not tagged yet:
sudo tailscale up --advertise-tags=tag:server --operator=$USER --reset
--operator lets that Unix user manage tailscaled (serve, Funnel, up/down). That is required for the host setup with rootless Docker. If you do not want to grant the Docker user that access, use the Tailscale Sidecar instead.
Rootless Docker also changes two paths:
Docker socket. Mount the user socket, not /var/run/docker.sock. Replace 1000 with your UID (id -u):
volumes:
- /run/user/1000/docker.sock:/var/run/docker.sock:ro
- /var/run/tailscale:/var/run/tailscale
Container reachability. Default direct mode tells host tailscaled to connect to the container IP. Those IPs are often unreachable from the host in rootless Docker. If the service advertises but does not connect:
- Set
docktail.service.direct=falseand publish the container port, so Tailscale proxies to127.0.0.1:<host-port>. - Or use the Tailscale Sidecar on the same Docker network as the app. Prefer that over
network_mode: hoston rootless Docker.
Tailscale Sidecar
Use this setup when the host does not run Tailscale directly. It is the required setup on macOS and Windows (Docker Desktop, OrbStack, Colima) and on many NAS devices, because tailscaled runs inside the Docker environment and shares its socket with DockTail through a named volume instead of a host mount:
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.
The sidecar uses network_mode: host so it can reach container IPs on any Docker network. On Docker Desktop this requires enabling host networking under Settings -> Resources -> Network. Alternatively, remove network_mode: host and attach the sidecar to the same Docker network as the containers you expose. On rootless Docker, prefer the shared-network form; host networking is limited in that mode.
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 -> Trust credentials.
-
Create an OAuth client scoped to your server tag, for example
tag:server. -
Grant these permissions:
- General -> Services: Write (and specify required tag)
- Devices -> Core: Write
- Keys -> Auth Keys: Write (only when using the sidecar method)
These same permissions also cover the optional
DELETE_UNUSED_SERVICEScleanup, which lists Services, inspects their advertising hosts, and deletes the ones no host advertises. They also cover DockTail Cloud tailnet health, which reads the same advertising-host data to tell an unapproved or unadvertised service from a broken container. No extra scope is required, and the credentials never leave the host. -
Add the credentials to DockTail:
environment:
- TAILSCALE_OAUTH_CLIENT_ID=your-client-id
- TAILSCALE_OAUTH_CLIENT_SECRET=your-client-secret
Or mount the credentials as files:
services:
docktail:
volumes:
- ./secrets:/run/secrets/docktail:ro
environment:
- FILE__TAILSCALE_OAUTH_CLIENT_ID=/run/secrets/docktail/tailscale_oauth_client_id
- TAILSCALE_OAUTH_CLIENT_SECRET_FILE=/run/secrets/docktail/tailscale_oauth_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-...
API keys can also be loaded from FILE__TAILSCALE_API_KEY or TAILSCALE_API_KEY_FILE.
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. Without credentials, DockTail Cloud also cannot report tailnet health, since it has no way to read the control plane.
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.
Funnel ACL
Funnel needs an extra grant on top of the service rules above. Tailscale only lets a node expose public Funnel endpoints if it has the funnel node attribute. DockTail runs Funnel on the node running its tailscaled, which is tagged tag:server in both the host and sidecar setups, so grant the attribute to tag:server:
{
"nodeAttrs": [
{
"target": ["tag:server"],
"attr": ["funnel"]
}
]
}
Notes:
- Grant
funneltotag:server(the DockTail host or sidecar), nottag:container.tag:containeris the virtual service tag, not a real device, so it cannot run Funnel. autoApprovers.servicesonly approves service advertisement inside the tailnet. It does not grant Funnel.- The public Funnel URL is the machine hostname (
https://<host>.<tailnet>.ts.net), not the service URL (https://<service>.<tailnet>.ts.net). Test it from a device that is not on your tailnet.
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. Use this for legacy setups, or when host tailscaled cannot reach container IPs (common with rootless Docker).
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.description |
No | - | Human-readable description shown for the service in the Tailscale admin panel. Requires API credentials (synced to the Service definition's comment). |
docktail.service.direct |
No | true |
Proxy directly to container IP instead of requiring a published host port. Set false when host tailscaled cannot reach container IPs (rootless Docker). |
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.service.path |
No | / |
URL path for HTTP(S) Serve traffic. Must start with /; not valid for TCP services. |
docktail.service.proxy-protocol |
No | off | PROXY protocol version (1 or 2) prepended on TCP forwards so the backend sees the tailnet client address. Only valid with service-protocol tcp or tls-terminated-tcp. |
docktail.tags |
No | tag:container |
Comma-separated service tags. Labels are the source of truth; see the note below. |
Tags are synced to the tailnet Service definition through the Tailscale API and require API credentials. Labels are authoritative: DockTail reconciles tags on every cycle, so tags edited by hand in the Tailscale admin console are reverted to the label-declared set on the next reconcile. Containers without a docktail.tags label use DEFAULT_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.docktail.service.pathdefaults to/and is passed totailscale serve --set-path. Changing or removing the label reconciles the old handler before advertising the new path.docktail.service.proxy-protocolis opt-in and off by default. The backend must understand PROXY protocol; sending the header to something that does not (Postgres, Redis, raw TCP apps) will break the connection. Set it to2unless you specifically need version1. The label is rejected on HTTP/HTTPS services because Tailscale only supports PROXY protocol for TCP forwarding.
To expose an HTTP(S) service below a custom path:
labels:
- "docktail.service.enable=true"
- "docktail.service.name=api"
- "docktail.service.port=8000"
- "docktail.service.path=/api"
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, service-protocol, path, proxy-protocol, and description. 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. |
docktail.funnel.path |
No | / |
HTTP(S) Funnel path. Must start with /. |
Funnel notes:
- HTTP(S) Funnels can share a public port when each Funnel uses a different
docktail.funnel.path. - TCP and TLS-terminated TCP Funnels support only one active Funnel per public port on a node.
docktail.funnel.pathis only valid for HTTP(S) Funnels.- 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.
Service With A Description
Add a description to label the service in the Tailscale admin panel. Requires API credentials (OAuth or API key), which DockTail syncs to the Service definition's comment.
services:
linkding:
image: sissbruecker/linkding:latest
labels:
- "docktail.service.enable=true"
- "docktail.service.name=linkding"
- "docktail.service.port=9090"
- "docktail.service.description=Bookmark Manager"
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.
HTTPS with TCP (SSH)
services:
forgejo:
image: codeberg.org/forgejo/forgejo:latest
labels:
- "docktail.service.enable=true"
- "docktail.service.name=forgejo"
- "docktail.service.port=3000"
- "docktail.service.service-port=443"
- "docktail.service.1.enable=true"
- "docktail.service.1.name=forgejo"
- "docktail.service.1.port=2222"
- "docktail.service.1.protocol=tcp"
- "docktail.service.1.service-port=22"
Access
- https at
https://forgejo.your-tailnet.ts.net - ssh at
ssh -T -l git forgejo.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"
Reverse Proxy With Client IPs (PROXY Protocol)
TCP services normally hide the tailnet client address: the backend sees tailscaled's own IP. Set docktail.service.proxy-protocol so Tailscale prepends a PROXY protocol header. The backend (Traefik, Caddy, HAProxy, nginx, and similar) must be configured to accept it.
services:
traefik:
image: traefik:latest
labels:
- "docktail.service.enable=true"
- "docktail.service.name=traefik"
- "docktail.service.port=443"
- "docktail.service.protocol=tcp"
- "docktail.service.service-protocol=tcp"
- "docktail.service.service-port=443"
- "docktail.service.proxy-protocol=2"
- "docktail.service.direct=false"
Without this label, access logs, rate limits, and IP allowlists on the reverse proxy only see tailscaled. HTTP/HTTPS DockTail services cannot use this label; Tailscale rejects PROXY protocol for those modes.
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
Use published host ports when you cannot proxy to the container IP. That includes older setups and rootless Docker with host Tailscale, where tailscaled often cannot reach container IPs.
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.
Public Funnel Mounted At A Path
services:
webhook:
image: my-webhook:latest
labels:
- "docktail.funnel.enable=true"
- "docktail.funnel.port=3000"
- "docktail.funnel.funnel-port=443"
- "docktail.funnel.path=/webhook"
Access it publicly at https://your-machine.your-tailnet.ts.net/webhook.
DockTail Cloud
DockTail Cloud is optional, opt-in monitoring for DockTail-managed services across one or more hosts. It is a hosted dashboard that watches what DockTail exposes and tells you why something is unreachable.
Explore DockTail Cloud or open the dashboard.
What You Get
- One view of every host and service. The full catalog of DockTail-managed services, plus a read-only inventory of the host's other containers, with health history.
- The useful distinction. A plain uptime check stops at "down." Cloud already has the Docker and Tailscale context, so it separates a container problem (exit code, Docker OOM event, restart loop) from an exposure problem (the container answers locally, but the Tailscale control plane says the service is awaiting approval, not advertised by this host, or gone) from a host problem (the box stopped sending heartbeats).
- Incidents with the evidence attached. Docker failure events and a bounded log tail captured at the moment of failure, so you start from the reason instead of from a graph.
- Alerts and recoveries. Notification when a service breaks and when it comes back — including hosts that drop off entirely, since detection runs in Cloud rather than on the box.
Reporting rides along with the normal agent — there is no separate binary. The same DockTail container gains cloud reporting when a workspace key is present, and stays completely inert without one.
How To Enable
- Create a workspace in the DockTail Cloud dashboard and copy the workspace key (
dtc_...). - Set
DOCKTAIL_CLOUD_KEYon the DockTail agent.
That is the only configuration — the cloud endpoint is built into the agent.
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}
# Optional. Enables DockTail Cloud reporting.
- DOCKTAIL_CLOUD_KEY=${DOCKTAIL_CLOUD_KEY}
With no key set, no connection is opened and DockTail runs exactly as before.
If Cloud marks a host as unmonitored (for example, the host sits past the workspace's host cap), the agent keeps the connection open with heartbeats and occasional catalog snapshots and pauses checks, Docker events, metrics, tailnet state, tailnet-health answers, and log excerpts. Cloud pushes an updated configuration over the same connection when that changes; the agent does not need a restart.
Environment Variables
| Variable | Default | Description |
|---|---|---|
DOCKTAIL_CLOUD_KEY |
- | Workspace key (dtc_...) from the cloud dashboard. Enables reporting. Inert when unset. |
DOCKTAIL_LOG_LEVEL |
info |
Log level for the cloud module: debug, info, warn, or error. |
DOCKTAIL_CHECK_INTERVAL |
30s |
How often local-vantage checks run (5s–5m). |
For local development, DOCKTAIL_CLOUD_URL overrides the built-in endpoint.
Plaintext ws:// is accepted only for loopback endpoints unless
DOCKTAIL_CLOUD_ALLOW_INSECURE=true is also set. Do not use that escape hatch
in production; non-loopback production endpoints must use wss://.
What It Sends
The hosted control plane (the dashboard and ingest service behind wss://ingest.docktail.org) is a separate, proprietary product. The reporting module in this repository sends operational metadata and bounded incident log tails to it over an outbound-only WebSocket Secure (WSS) connection.
When enabled, the agent reports the following operational data:
- Periodic snapshots of DockTail-managed services, including stopped containers, plus refreshes after successful reconciles.
- A read-only inventory of the host's other containers — the ones not published with
docktail.*labels, including stopped ones — with name, image, state/health, ports, and live CPU/memory. These containers are not actively probed; they are listed on the dashboard so you can see the host's whole Docker footprint, and can be explicitly watched for Docker-event-driven incidents and alerts. - Docker failure events, including container exit codes, out-of-memory (OOM) kills, health-status changes, and restart loops.
- Local-vantage check results. Checks default to TCP; cloud-managed config may select HTTP, a relative path, and expected status, but the destination always comes from the agent's local service discovery.
- Tailscale control-plane service state, when Cloud asks for it (see Tailnet Health).
- Bounded incident log excerpts when the workspace opts in. Before sending, the agent best-effort redacts common Authorization/Bearer credentials, passwords, tokens, API keys, credential URLs, JWTs, and private-key blocks, then applies the 40-line/8-KiB caps. Redaction cannot recognize every application-specific secret.
What It Never Does
- No remote command execution, deployment, or shell access. The protocol is metadata-only and has no exec, deploy, or shell message types.
- It never executes anything on the host on behalf of the cloud.
- Cloud configuration cannot select an arbitrary network destination; checks are bound to services discovered locally from Docker. A tailnet-health request carries only service names the agent itself published, and carries no credentials.
- Tailscale credentials stay on the host. Cloud never receives an OAuth client, an API key, or a Tailscale token — only the metadata the agent reads with them.
- The connection is outbound only; the agent dials the ingest endpoint and nothing dials in.
Tailnet Health
Cloud's tailnet vantage answers one question: does the Tailscale control plane
agree that this host is advertising the service and that the service is approved?
Answering it needs Tailscale API credentials, so it only works when
TAILSCALE_OAUTH_CLIENT_ID/TAILSCALE_OAUTH_CLIENT_SECRET or
TAILSCALE_API_KEY is already configured (see
Tailscale Admin Setup). No new
credential, and none of them ever leaves the host: Cloud asks a question, the
agent reads the Tailscale API and answers with metadata.
It reads the control plane rather than the host's own tailscale serve config
because serve state only says "this host believes it is serving." It cannot see
that the service is waiting for approval in the Admin Console, that its
definition was deleted, or that the advertisement never reached the control
plane — cases where the host looks perfectly healthy and nobody can reach the
service.
This is an approval and advertisement oracle, not a reachability ping. Tailscale
exposes no health or liveness probe for a service, so a service Cloud reports as
connected is one the control plane considers advertised and approved — nothing
more. Actual reachability still comes from the local vantage and, for Funnel
services, the public one.
Cloud drives the polling: it asks exactly one credentialed host per tailnet on a slow cadence (service state changes when a human approves something), and that answer covers every host on the tailnet. Agents never poll on their own, and each agent enforces its own minimum interval of 60 seconds between control-plane reads, re-serving its previous answer if asked sooner — your Tailscale API quota is yours, and nothing on the Cloud side can spend more of it than that.
Without credentials the agent still says so explicitly: its hello advertises that it understands tailnet health but has no credentials for it, so Cloud reports "no Tailscale credentials" and links to Tailscale Admin Setup rather than showing an outage — or telling you to upgrade an agent that is already current. Local checks, Docker events, metrics, and incidents are unaffected.
Identity
Each host is identified by its Docker engine ID, used as a stable fingerprint. A workspace key can enroll multiple hosts while its enrollment window is open (one hour by default). After the window closes, the key continues to authenticate the hosts it already enrolled but cannot add another fingerprint until an operator reopens enrollment in the Cloud dashboard. An agent waiting for a reopened window retries automatically at a low rate.
The agent also reports its own Tailscale node ID and tailnet name when it has a
tailnet. The Services API calls the stable identity nodeId; it is the same
identity exposed as Self.ID by tailscale status --json. Cloud uses that exact
match to associate each service advertisement with its host. The tailnet name
groups the hosts that share a control plane, so Cloud knows which single host to
ask for tailnet health. Neither is required — without them the agent simply
reports fewer signals.
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 for services whose containers set no docktail.tags label. Tags are reconciled on every cycle; manual tag edits in the admin console are overwritten. |
IGNORE_SERVICE_NAMES |
- | Comma-separated service names DockTail must not drain, clear, or delete during reconciliation or shutdown cleanup. |
DELETE_UNUSED_SERVICES |
false |
When true, DockTail deletes tailnet Service definitions that no host advertises anymore. Requires API credentials. See Cleanup Behavior. |
SKIP_SHUTDOWN_CLEANUP |
false |
When true, DockTail leaves its services and Funnels advertised on shutdown instead of draining and clearing them. This can keep ports exposed on the tailnet beyond what your current labels define; see Cleanup Behavior. |
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. Rootless Docker typically uses unix:///run/user/<uid>/docker.sock. |
TAILSCALE_SOCKET |
/var/run/tailscale/tailscaled.sock |
Tailscale daemon socket. |
EXIT_ON_SOCKET_LOSS |
true |
When true, DockTail exits if the Tailscale socket stays unreachable past the grace period, so the container's restart policy can re-establish the mount. See Tailscale Socket Loss. |
SOCKET_LOSS_GRACE_PERIOD |
90s |
How long the Tailscale socket may stay unreachable before DockTail exits. Must be longer than a normal tailscaled restart. |
If both OAuth and API key credentials are configured, DockTail uses OAuth.
Sensitive credential variables can also be loaded from files. This is useful with Docker secrets, Swarm secrets, and other secret mounts. Direct environment variables take precedence; when the direct variable is unset, DockTail checks FILE__VARIABLE_NAME first and VARIABLE_NAME_FILE second. The file content is used as the value with trailing newlines removed.
Supported file-backed credential variables:
| Direct variable | File variable alternatives |
|---|---|
TAILSCALE_OAUTH_CLIENT_ID |
FILE__TAILSCALE_OAUTH_CLIENT_ID, TAILSCALE_OAUTH_CLIENT_ID_FILE |
TAILSCALE_OAUTH_CLIENT_SECRET |
FILE__TAILSCALE_OAUTH_CLIENT_SECRET, TAILSCALE_OAUTH_CLIENT_SECRET_FILE |
TAILSCALE_API_KEY |
FILE__TAILSCALE_API_KEY, TAILSCALE_API_KEY_FILE |
IGNORE_SERVICE_NAMES accepts bare names like grafana and fully qualified names like svc:grafana.
DockTail Cloud (optional)
These variables enable optional DockTail Cloud reporting. They are opt-in: the agent is completely inert unless DOCKTAIL_CLOUD_KEY is set. See DockTail Cloud.
| Variable | Default | Description |
|---|---|---|
DOCKTAIL_CLOUD_KEY |
- | Workspace key (dtc_...) from the cloud dashboard. Enables reporting. Inert when unset. |
DOCKTAIL_LOG_LEVEL |
info |
Log level for the cloud module: debug, info, warn, or error. |
DOCKTAIL_CHECK_INTERVAL |
30s |
How often local-vantage checks run (5s–5m). |
Local-development overrides: DOCKTAIL_CLOUD_URL replaces the built-in ingest
endpoint. ws:// is allowed for loopback endpoints; non-loopback plaintext
requires DOCKTAIL_CLOUD_ALLOW_INSECURE=true and must never be used in
production. Non-loopback production endpoints must use wss://.
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; Tailscale terminates incoming TLS and forwards decrypted TCP to the backend. |
docktail.service.path selects the HTTP(S) Serve mount point and defaults to /. It must start with / and is rejected for TCP protocols.
docktail.service.proxy-protocol (1 or 2) is valid only with tcp or tls-terminated-tcp. It is off by default because the backend must understand the PROXY header. HTTP/HTTPS values are rejected.
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. |
docktail.funnel.path is supported only with HTTP(S) Funnel protocols. It defaults to / and must start with /.
Cleanup Behavior
DockTail cleans up the services and Funnels it advertises locally when it shuts down (draining then clearing them). This is the safe default: when DockTail is not running, nothing it configured stays reachable, so the advertised surface can never drift from what your labels describe.
SKIP_SHUTDOWN_CLEANUP=true disables this cleanup. DockTail then leaves its services and Funnels advertised when it exits instead of tearing them down. It affects only graceful shutdown; a hard crash never runs cleanup either way.
Warning: Enabling this keeps ports exposed on the tailnet while DockTail is down, potentially beyond what your current labels define. The serve and Funnel configuration lives in
tailscaled, not in DockTail, so anything DockTail last advertised keeps serving on the host until DockTail comes back. If you stop a container, remove it, or delete its DockTail labels while DockTail is not running, that service (and any Funnel) stays reachable for the entire downtime and is only reconciled away once DockTail restarts. The default cleanup exists precisely to prevent this stale exposure. Only enableSKIP_SHUTDOWN_CLEANUPif keeping services reachable across DockTail restarts is worth giving up that guarantee, and treat the advertised surface as detached from your labels until DockTail is running again.
On restart, DockTail re-adopts the still-advertised services and removes only those whose containers are no longer running. Ignored services (IGNORE_SERVICE_NAMES) are never cleaned up regardless of this setting.
By default DockTail does not delete Tailscale Service definitions from the Control Plane when containers stop; this is a conservative strategy that avoids removing definitions unexpectedly.
Deleting unused Service definitions
Set DELETE_UNUSED_SERVICES=true to let DockTail remove Service definitions that are no longer advertised by any host. This requires API credentials (OAuth or API key). It is disabled by default.
During each reconciliation, for every Service definition in the tailnet DockTail:
- Keeps the Service if DockTail currently advertises it (it is backed by a running container).
- Keeps the Service if its name is listed in
IGNORE_SERVICE_NAMES. - Asks the Control Plane which hosts are registered for the Service. If at least one host is registered, DockTail keeps it.
- Deletes the Service only when no host is registered for it.
Because the decision is based on the tailnet-wide host list, this is safe to enable on multiple DockTail instances at once: a Service hosted by any other host or instance always reports at least one host and is never deleted. DockTail also skips deletion whenever an API call fails, so it never deletes under uncertainty.
Note: That host list is a configuration and approval registry, not a liveness signal. A host stays listed while it is configured to host the Service, and remains listed for some time after it stops advertising it. Cleanup can therefore lag behind reality — it may keep a definition longer than expected, but it will not delete one that is still in use.
This cleanup runs only during reconciliation, not during shutdown, so restarting DockTail does not delete and recreate the Services of still-running containers.
Note: When enabled, DockTail may also delete Service definitions it did not create if they have no advertising hosts (for example, a Service you defined in the admin console but never advertised). Add such names to
IGNORE_SERVICE_NAMESto protect them.
Tailscale Socket Loss
DockTail talks to tailscaled over a Unix socket that it bind-mounts from the
host or shares with a sidecar. That mount is resolved once, when the container
starts.
This matters whenever tailscaled restarts. On a host install under systemd, the
unit declares RuntimeDirectory=tailscale and leaves RuntimeDirectoryPreserve
at its default of no, so systemd removes /run/tailscale when the daemon
stops and creates a new directory when it starts. A container that mounted the
old directory stays attached to it after it is unlinked, and the new socket never
becomes visible inside the container. Sharing the socket through a host path with
a sidecar that gets recreated has the same effect. Retrying cannot help: the
socket is not late, it is in a directory this container can no longer see.
An upgrade is the most common trigger, because upgrading the package restarts the
daemon — but it is not the only one. Measured on Debian 12 (systemd 252,
tailscale 1.102.2), an ordinary systemctl restart tailscaled replaces the
directory just as an upgrade does, and so does any stop/start pair:
| directory | DockTail's mount | |
|---|---|---|
| before | inode 264, socket present | inode 264, socket present |
after systemctl restart tailscaled |
inode 412, socket present | inode 264, empty |
| after the container restarts | inode 412, socket present | inode 412, socket present |
Because of this, mounting the directory rather than the socket file — which is what the setup guide recommends, and which does survive the socket file being recreated — is not on its own enough to survive a daemon restart on a systemd host.
Without intervention DockTail would keep running in that state — process alive, every Tailscale call failing, and every Service it manages drifting until it goes offline, with no recovery until someone restarts the container by hand.
So DockTail probes the socket, and if it stays unreachable for
SOCKET_LOSS_GRACE_PERIOD (default 90s) it logs the reason and exits, letting
the container's restart policy re-create the container and with it the mount.
- Use a restart policy.
restart: unless-stopped(oralways) is what turns the exit into a recovery. Without one, DockTail stops instead of restarting. - The grace period must stay comfortably longer than a normal
tailscaledrestart, which takes a second or two. Brief outages are ignored and never cause an exit. - The check arms only after the socket has been reachable at least once, so
starting DockTail before
tailscaledwaits rather than exits. - Set
EXIT_ON_SOCKET_LOSS=falseto disable it and keep the old behaviour of retrying forever.
Prefer a named volume over a host path when you run tailscaled as a sidecar: a
volume keeps one directory for its lifetime, so recreating the sidecar cannot
detach DockTail's mount in the first place.
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.
- If OAuth or API key credentials are configured, it creates service definitions through the Tailscale API and keeps their tags and descriptions in sync with the labels; manual edits to either are overwritten. This runs before any new service is advertised, matching Tailscale's documented order: define the Service first, then advertise a host for it.
- It executes the Tailscale CLI to advertise services and Funnels.
- If
DELETE_UNUSED_SERVICESis enabled, it deletes tailnet service definitions that no host is registered for anymore. - 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. This is also the reliable path when host tailscaled cannot reach container IPs, which is common with rootless Docker.
TCP forwards hide the tailnet client address by default: the backend sees tailscaled. Set docktail.service.proxy-protocol to 1 or 2 so Tailscale prepends a PROXY protocol header. The backend must accept that header; HTTP/HTTPS services cannot use it.
Containers using network_mode: host are served on 127.0.0.1 (IPv4, to avoid dual-stack localhost→::1 refusals). The local health check probes them from wherever the agent runs: directly via 127.0.0.1 when the agent shares the host's network namespace, or via the agent's docker-network gateway (the host's bridge address) when the agent runs in its own container. Containers using network_mode: none cannot use direct mode.