Self-hosted service
marvinvr/docktail avatar
marvinvr/docktail

DockTail: Docker Containers as Tailscale Services, Configured by Labels

Expose Docker containers as Tailscale Services using label-based configuration.

1,260 stars47 forksGoAGPL-3.0

At a glance

What is it?
DockTail is a Go agent that watches the Docker socket and turns labelled containers into native Tailscale Services, without publishing ports or burning a Tailscale device slot per app. It suits homelab and small self-hosted setups; it assumes a tailscaled on the same host and Tailscale admin credentials.
Who is it for?
DockTail fits anyone already running tailscaled on a Docker host who wants tailnet-only access to containers without publishing ports or registering a device per app. Skip it if you cannot grant the Tailscale admin scopes it needs, or if you want a proxy that also fronts non-Docker backends.
Can I use it commercially?
Yes, with strict conditions. AGPL-3.0 is a network copyleft licence: if people use a modified version over a network, for example as a hosted service, you must offer them its source code under the same licence.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 1, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The Problem DockTail Targets: Per-App Tailscale Devices

The usual way to reach a container over a tailnet is to run Tailscale inside it, or to run a sidecar. Either route registers a device per application, and each of those devices has to be authorised, named, tagged and later cleaned up. On a host running ten small services, that is ten entries in the admin console that exist only to forward traffic.

DockTail takes the other route. It watches Docker containers, reads docktail.* labels, and exposes matching containers as Tailscale Services. According to the README, app containers do not need published Docker ports by default, because DockTail proxies directly to their Docker network IPs. The audience is the self-hosted and homelab operator with a single Docker host already joined to a tailnet, who wants the tailnet to reach containers by name without the device bookkeeping.

The README's comparison table puts the distinction plainly: it lists native Tailscale Services, Docker-label configuration and no per-app device slots as rows where DockTail and plain Services both qualify, while TSDProxy, ScaleTail and tsbridge do not. That table is the project's own framing, not an independent measurement, but it does describe a real architectural split.

How DockTail Reconciles Docker State with Tailscale Services

The repository layout shows the split: main.go at the top level, with reconciler/, tailscale/, cloud/ and types/ as packages. The agent is stateless at the container level, which means the desired state lives in Docker labels and the actual state lives in Tailscale, and the reconciler's job is to close the gap.

When a labelled container appears, DockTail reads its labels, resolves the container's Docker network IP, and creates or updates a Tailscale Service definition pointing at that IP and port. When a container restarts and its IP changes, reconciliation runs again. The docker-compose.yaml in the repository sets RECONCILE_INTERVAL=60s, so the loop is periodic rather than purely event-driven, which is the safety net for missed Docker events.

Two credentials paths exist. With an OAuth client or API key, DockTail can create Service definitions in the admin console automatically. The .env.example states that without credentials DockTail advertises services locally but cannot create Service definitions in the admin console. That is the dividing line between a fully automatic setup and one where you define Services yourself.

Cleanup of unused Service definitions is opt-in, and the README notes it is safe with multiple instances. That opt-in default is the right call: an automatic deleter pointed at shared admin state is the kind of feature that should require a deliberate decision.

Installing DockTail and Exposing a First Container

DockTail ships as a container image, ghcr.io/marvinvr/docktail:latest. The Quick Start compose file mounts the Docker socket read-only and the Tailscale socket directory, then passes OAuth credentials from the environment. The README marks those credentials optional but recommended, because they enable automatic service creation.

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}

The application container needs no ports section at all. You label it instead, and DockTail proxies to the container IP on the port named in docktail.service.port.

yaml
  myapp:
    image: nginx:latest
    labels:
      - "docktail.service.enable=true"
      - "docktail.service.name=myapp"
      - "docktail.service.port=80"

Bring the stack up and curl the service name on your tailnet, as the README's quick start does.

bash
docker compose up -d
curl http://myapp.your-tailnet.ts.net

If the curl fails, the README's own prerequisites are the first place to look: the Docker host must be connected to Tailscale and allowed to advertise services. Without that permission, the container starts fine and nothing appears on the tailnet. For secrets stored as files rather than environment values, the README documents FILE__TAILSCALE_OAUTH_CLIENT_ID and FILE__TAILSCALE_OAUTH_CLIENT_SECRET, or the _FILE suffixed variants, pointing at the mounted paths.

Protocols, Funnel and the Labels That Change Behaviour

The label set is where DockTail's scope becomes clear. HTTP, HTTPS, TCP and TLS-terminated TCP are supported, and the service-protocol label drives defaults: the repository's docker-compose.yaml notes that service-port defaults to 443 when service-protocol is https. A database example uses docktail.service.protocol=tcp with service-port=5432.

For TCP reverse proxies, docktail.service.proxy-protocol=2 enables PROXY protocol so the backend sees the tailnet client IP instead of the proxy's address. That label matters for anything doing IP-based rate limiting or access logs; without it, every request appears to come from DockTail.

Funnel is configured with a separate label family. docktail.funnel.enable=true plus docktail.funnel.port exposes a service to the public internet, and docktail.funnel.path mounts an HTTP or HTTPS Funnel at a path, which the README shows for a webhook endpoint. Funnel is the one feature here that deliberately steps outside the tailnet, so treat those labels as the security boundary they are.

One container can carry several services. The README lists multiple Tailscale services from one container as a feature, which is what you want for an app that serves both an HTTP UI and a TCP port.

Where DockTail Is the Wrong Tool

DockTail is built around one assumption: a tailscaled on the same host, reachable through the mounted socket. The README's quick start says so directly, and the compose file mounts /var/run/tailscale. If your containers run somewhere without a local Tailscale daemon, this is not the project for you. The repository does include docker-compose.sidecar.yaml and a TAILSCALE_AUTH_KEY variable for a tailscale/tailscale sidecar, and the .env.example notes that key is not read by DockTail itself, so the sidecar route exists, but it is a second deployment shape rather than the default.

The second boundary is the admin API. Without OAuth or API key credentials, DockTail advertises services locally but cannot create Service definitions in the admin console, per the .env.example. Anyone unwilling to grant Services: Write and Devices: Core: Write scopes to an automated agent gets a materially smaller feature set, and the API key option expires every 90 days according to the same file, which is an operational chore OAuth avoids.

Third, DockTail only knows about Docker. Backends that are not containers, or containers it cannot see through the socket, are outside its model. A general reverse proxy that reads its own config files will cover more ground.

Finally, the project is young enough that its own release history is the honest signal. Version 1.4.0 was titled Funnel only containers and 1.5.0 Quality of life improvements, which is normal for a tool still filling in edge cases. The last push was on 2026-08-30, so it is current, but the label surface is broad and the documentation for each combination is spread across docs/ rather than the README.

DockTail Compared with a Sidecar Proxy Approach

The nearest alternative in the README's own table is TSDProxy, and the difference is not cosmetic. TSDProxy and tsbridge are proxies that register themselves with Tailscale and forward to backends; the README's table marks them as not using native Tailscale Services and as consuming separate Tailscale device slots per app. DockTail instead creates Service definitions, so the apps do not each become devices.

That changes what you manage. With a device-per-app model, each app has its own Tailscale identity, its own ACL entries and its own key expiry. With Services, the identity is the service name and the ACLs are written against Services. The trade-off is that you now depend on the Tailscale admin API being reachable and on your credentials being valid; a device-per-app proxy can keep working with a local daemon alone.

The README also notes ScaleTail is template-based, so each app usually starts from its own Compose recipe, and that plain Services require you to configure how the service host reaches the backend yourself. DockTail's contribution is the automation layer over plain Services, not a new data plane. If you already write Service definitions by hand and your container IPs are stable, DockTail is replacing a small amount of manual work with a daemon and a credential.

Licence, Maintenance and Upgrade Cost

DockTail is licensed AGPL-3.0. For self-hosted use that changes nothing about how you run it. It matters if you modify DockTail and offer it to others over a network, because the AGPL's network clause is broader than the GPL's. Running the published container against your own Docker host is the ordinary case, and the licence permits it. This is a description of the licence text, not legal advice.

Upgrades are container image pulls, and the agent is described as a stateless Docker container runtime, so there is no local database to migrate. The version is injected at build time through the agentVersion linker flag in the Dockerfile, which is how the optional Cloud reporting knows what it is talking to.

The credential rotation schedule is the real recurring cost. OAuth clients do not expire, which the .env.example calls out as the reason to prefer them; API keys expire every 90 days. The Dockerfile also copies the tailscale CLI from the official image specifically so the CLI version matches the sidecar daemon exactly, which means image updates are the supported way to keep that pair aligned rather than installing a CLI yourself.

DockTail Cloud is optional and inert without DOCKTAIL_CLOUD_KEY. The README states the link is outbound-only and metadata-only, with no exec, deploy or shell message types, and points at the cloud/ directory to verify that. It is a reasonable claim to check in source before enabling it on a host that already has both the Docker and Tailscale sockets mounted.

Editorial conclusion

DockTail fits anyone already running tailscaled on a Docker host who wants tailnet-only access to containers without publishing ports or registering a device per app. Skip it if you cannot grant the Tailscale admin scopes it needs, or if you want a proxy that also fronts non-Docker backends. Before adopting, verify three things: that your host is allowed to advertise Services, that your OAuth client is scoped to the server tag with Services: Write and Devices: Core: Write, and that you are comfortable with the AGPL-3.0 terms, since DockTail runs as a separate process rather than being linked into your code.

Frequently asked questions

Does DockTail require published Docker ports on my application containers?

No. The README states that app containers do not need published Docker ports by default, because DockTail proxies directly to their Docker network IPs. The example application container in the quick start has no ports section at all.

What happens if I run DockTail without Tailscale OAuth or API credentials?

The .env.example states that without credentials DockTail advertises services locally but cannot create Service definitions in the admin console. If both credential methods are set, OAuth wins.

How does DockTail handle a container whose IP changes after a restart?

Automatic reconciliation when containers restart or IPs change is listed as a feature, and the repository's docker-compose.yaml sets RECONCILE_INTERVAL=60s. That periodic loop re-reads Docker state and updates the corresponding Tailscale Service.

Official sources

  1. License: AGPL-3.0
  2. marvinvr/docktail on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/marvinvr-docktail.svg)](https://hysenlabs.com/projects/marvinvr-docktail)