<!-- Generated from docs/*.md. Do not edit directly. -->

# 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, delete, or restore missing advertisements for 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](https://docktail.org/docs/reference/#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](https://docktail.org/docs/reference/#cleanup-behavior). |
| `LOG_LEVEL` | `info` | Logging level for all output, including the DockTail Cloud module: `debug`, `info`, `warn`, or `error`. Any other value means `info`. |
| `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` | The `tailscaled` socket DockTail checks: the startup missing-socket hint, the [socket-loss check](https://docktail.org/docs/reference/#tailscale-socket-loss), and the image's health check. DockTail does not pass it to the bundled `tailscale` CLI, which does the serve and Funnel work at its own default of `/var/run/tailscale/tailscaled.sock`, so mount the daemon's socket directory at `/var/run/tailscale` either way. |
| `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](https://docktail.org/docs/reference/#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. |
| `UPDATE_CHECK` | `true` | When `true`, DockTail checks once a day whether a newer release exists and logs it once. Set `false` to never contact GitHub. See [Version And Updates](https://docktail.org/docs/reference/#version-and-updates). |
| `HEALTH_FILE` | `/tmp/docktail-health.json` | Where DockTail writes the status file that `docktail health` (the image's health check) reads. Point it at a writable path if the container's `/tmp` is read-only. See [Health Check](https://docktail.org/docs/reference/#health-check). |

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

Durations (`RECONCILE_INTERVAL`, `SOCKET_LOSS_GRACE_PERIOD`) use Go syntax such as `30s`, `5m` or `1h30m`; booleans accept `true`/`false`, `1`/`0`, and `t`/`f`. An unparseable value in one of the variables above logs a warning and falls back to the default.

### Docker Connection And Log Output

DockTail builds its Docker client from the standard Docker environment variables, so they work as they do for the `docker` CLI. Besides `DOCKER_HOST` above, which also takes `tcp://host:port` (for example a [read-only socket proxy](https://docktail.org/docs/security/#read-only-socket-proxy)):

| Variable | Default | Description |
| --- | --- | --- |
| `DOCKER_API_VERSION` | negotiated | Pins the Docker API version instead of negotiating it with the daemon. |
| `DOCKER_CERT_PATH` | - | Directory with `ca.pem`, `cert.pem` and `key.pem`. Setting it makes DockTail talk TLS to a `tcp://` daemon. |
| `DOCKER_TLS_VERIFY` | - | Used with `DOCKER_CERT_PATH`: any non-empty value verifies the daemon's certificate; unset or empty skips verification. |

Log lines are colored only when stdout is a terminal; setting `NO_COLOR` (to any value) or `TERM=dumb` turns color off there too.

`TAILSCALE_AUTH_KEY` in the examples is read by the `tailscale/tailscale` sidecar container (as `TS_AUTHKEY`), not by DockTail.

### 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](https://docktail.org/docs/cloud/).

| Variable | Default | Description |
| --- | --- | --- |
| `DOCKTAIL_CLOUD_KEY` | - | Workspace key (`dtc_...`) from the cloud dashboard. Enables reporting. Inert when unset. |
| `DOCKTAIL_LOG_LEVEL` | `info` | Read, but currently has no effect: the cloud module logs at the level `LOG_LEVEL` sets. |
| `DOCKTAIL_CHECK_INTERVAL` | `30s` | How often local-vantage checks run (5s–5m). A value outside that range, or one that does not parse, keeps the cloud module from starting; DockTail itself keeps running. |
| `DOCKTAIL_HOST_ROOT` | `/host` | Where the host's root filesystem is bind-mounted, for whole-host disk usage. Only used when that path exists; see [Disk Usage](https://docktail.org/docs/cloud/#disk-usage). |

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 attempts to clean up the services and Funnels it advertises locally when it shuts down, reducing stale exposure while it is not running. Service cleanup runs serially, once per service name even when it has several ports: drain first, then clear. Draining stops new connections; DockTail clears the configuration immediately afterward without waiting for all existing connections to finish.

Cleanup has an 8-second budget, leaving headroom within Docker’s default 10-second stop timeout. Cleanup stops scheduling commands when its budget expires. A crash, SIGKILL, daemon error, or the cleanup deadline can leave some services configured or advertised; reconciliation repairs missing advertisements when DockTail resumes.

On startup and every reconciliation cycle, DockTail also reads `tailscale debug prefs`. If a desired service has matching Serve configuration but is missing from `AdvertiseServices`, DockTail restores its advertisement with `tailscale serve advertise` after syncing its control-plane definition (when API sync is enabled). This repairs services left drained but configured after interrupted cleanup without rewriting their endpoints. It uses the same local daemon socket as Serve, requires no additional API credentials, and runs once per service name. Ignored services and service names with endpoint conflicts are excluded from this repair. Failed, malformed, or unsupported prefs reads are treated as unknown state: DockTail skips advertisement repair for that cycle while continuing normal configuration reconciliation.

A manual `tailscale serve drain` of a desired, matching service is therefore reversed on the next successful reconciliation. Stop the backing container, remove its service labels, or exclude the service from advertisement repair with `IGNORE_SERVICE_NAMES` if the drain should persist. Local configuration and advertisement do not guarantee that Tailscale has propagated peer routes or that the service is reachable.

`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. Only enable `SKIP_SHUTDOWN_CLEANUP` if keeping services reachable across DockTail restarts is worth deliberately skipping cleanup, 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:

1. Keeps the Service if DockTail currently advertises it (it is backed by a running container).
2. Keeps the Service if its name is listed in `IGNORE_SERVICE_NAMES`.
3. Asks the Control Plane which hosts are registered for the Service. If **at least one** host is registered, DockTail keeps it.
4. 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_NAMES` to 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` (or `always`) 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 `tailscaled`
  restart, 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 `tailscaled` waits rather than exits. At startup
  DockTail also waits up to 15 seconds for the socket before its first
  reconcile, so a sidecar that is still starting does not delay your Services
  by a whole `RECONCILE_INTERVAL`.
- Set `EXIT_ON_SOCKET_LOSS=false` to 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.

## Version And Updates

DockTail logs its version on startup (`Starting DockTail version=…`), and the
binary prints it on request:

```bash
docker exec docktail /app/docktail --version
# or, without a running container:
docker run --rm ghcr.io/marvinvr/docktail:latest --version
```

Include that version in bug reports.

About 30 seconds after startup and then once a day (hourly after a failed
check), a release build asks the
GitHub API for the repository's tags and, when a newer stable release exists,
logs it once:

```text
INF A newer DockTail release is available; pull the new image and recreate the container to update (set UPDATE_CHECK=false to stop checking) current=1.8.2 latest=1.8.3 release_notes=https://github.com/marvinvr/docktail/releases
```

The request is an anonymous `GET` to `api.github.com` that carries nothing
about your installation beyond the `docktail/<version>` user agent (GitHub
sees the source IP, as with any request). It is
skipped for development builds, a failure is only logged at debug level, and
`UPDATE_CHECK=false` turns it off. Pre-release tags are never offered, and a
pre-release build newer than the latest stable release is not nagged.

## Health Check

The image's `HEALTHCHECK` runs `docktail health`, which tracks DockTail itself
rather than only the Tailscale daemon. The running process rewrites a small
JSON status file every 10 seconds (and after every reconcile) at `HEALTH_FILE`;
`docktail health` reads it, prints a one-line verdict, and exits `0` when
healthy and `1` when not. Nothing listens on a port.

DockTail is **unhealthy** when:

- the status file is missing or has not been updated for a minute (DockTail is
  hung, stopped, or cannot write the file);
- the `tailscaled` socket does not accept connections;
- the last two reconciles could not read the containers from Docker;
- no reconcile has finished for three reconcile intervals (at least three
  minutes), meaning the loop is stuck.

A reconcile that fails for individual services — a label conflict, a service
the tailnet refuses — is reported as `healthy, with errors`: that is one
container's configuration, and DockTail keeps serving the rest.

The [DockTail Cloud](https://docktail.org/docs/cloud/#docktail-cloud) link state (`connecting`,
`connected`, `disconnected`, `rejected`, or `failed`) is included in the output
but never makes the container unhealthy: a cloud outage or a revoked key does
not stop DockTail from serving containers, and a restart triggered by the
health status (Swarm, autoheal) would drain every service without fixing it.

```bash
docker exec docktail /app/docktail health
# healthy: last reconcile 12s ago; cloud connected for 3h12m4s

docker inspect --format '{{json .State.Health}}' docktail   # Docker's view, with recent outputs
docker exec docktail cat /tmp/docktail-health.json          # the full status
```

With a read-only root filesystem, mount a `tmpfs` at `/tmp` or set
`HEALTH_FILE` to a writable path; otherwise DockTail logs a warning and the
health check reports unhealthy.

## 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/`
