# DockTail Full Documentation Source: https://docktail.org/docs/ # 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 1. Start with [Quick Start](#quick-start) for a minimal Compose setup. 2. Read [Installation](#installation) for host Tailscale and sidecar options. 3. Configure Tailscale permissions in [Tailscale Admin Setup](#tailscale-admin-setup). 4. Use [Labels](#labels) and [Examples](#examples) when exposing real services. 5. Check [Reference](#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: ```yaml 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: ```bash docker compose up -d ``` Then open the service from your tailnet: ```bash 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](#installation) and [Tailscale Admin Setup](#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: ```yaml 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: ```bash 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: ```yaml 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. 1. Open Tailscale Admin Console -> Settings -> OAuth clients. 2. Create an OAuth client scoped to your server tag, for example `tag:server`. 3. Grant these permissions: - General -> Services: Write - Devices -> Core: Write - Keys -> Auth Keys: Write, only when using the sidecar method 4. Add the credentials to DockTail: ```yaml 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. ```yaml 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. ```json { "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. ```yaml 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.protocol` defaults to `https` when the backend port is `443`; otherwise it defaults to `http`. - `docktail.service.service-port` defaults to `443` when `service-protocol` is `https`; otherwise it defaults to `80`. - `docktail.service.service-protocol` defaults to `https` when the service port is `443`, to `tcp` when the backend protocol is TCP, and otherwise to `http`. ### Multiple Services From One Container A single container can expose multiple separate Tailscale services using numbered labels: ```yaml 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.enable` and other `docktail.service.*` labels. - `docktail.service.direct` and `docktail.service.network` still 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 ```yaml 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 ```yaml 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 ```yaml 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 ```yaml 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 ```yaml 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 ```yaml 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 ```yaml 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. ```text 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 1. DockTail monitors Docker events for container starts and stops. 2. It extracts service configuration from container labels. 3. It resolves the backend destination from Docker network settings or published ports. 4. It generates Tailscale service configuration pointing to that backend. 5. It executes the Tailscale CLI to advertise services and Funnels. 6. If OAuth or API key credentials are configured, it creates service definitions through the Tailscale API. 7. 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.