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.
  1. Start with Quick Start for a minimal Compose setup.
  2. Read Installation for host Tailscale and sidecar options.
  3. Configure Tailscale permissions in Tailscale Admin Setup.
  4. Use Labels and Examples when exposing real services.
  5. 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.

  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:
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.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:

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

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.

  • 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

  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.

docktail ยท generated 2026-05-04