diff --git a/.dockerignore b/.dockerignore index 93ae9da..773fb5f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -32,7 +32,7 @@ coverage.txt # Docker Dockerfile -docker-compose.yaml +docker-compose*.yaml .dockerignore # Example data diff --git a/.env.example b/.env.example index 8b64cd4..f2d31dc 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,11 @@ # Copy to .env and uncomment what you need. Every variable below is documented # in docs/07-reference.md (https://docktail.org/docs/#reference); the defaults # shown in comments are what DockTail uses when the variable is unset. +# +# Compose reads .env only to fill in ${...} references. A variable reaches +# DockTail only when the service lists it under environment:, as the example +# compose files do for the Tailscale credentials and DOCKTAIL_CLOUD_KEY; add a +# line there for anything else you set here. # ============================================================================= # Tailscale sidecar (only for docker-compose.sidecar.yaml) @@ -74,10 +79,21 @@ TAILSCALE_AUTH_KEY=tskey-auth-xxxxx # ============================================================================= # Docker daemon socket. Rootless Docker typically uses -# unix:///run/user//docker.sock +# unix:///run/user//docker.sock; a read-only socket proxy is +# tcp://docker-socket-proxy:2375 (docs/02-security.md). Set it under the +# service's environment: rather than here: Compose itself can also pick up a +# DOCKER_HOST from .env and would then talk to that daemon. # DOCKER_HOST=unix:///var/run/docker.sock -# Tailscale daemon socket. +# Standard Docker client variables, only needed for a TLS-protected tcp:// +# daemon or to pin the API version (negotiated by default). +# DOCKER_CERT_PATH= +# DOCKER_TLS_VERIFY=1 +# DOCKER_API_VERSION= + +# tailscaled socket probed by the socket-loss check. The bundled tailscale CLI +# always uses /var/run/tailscale/tailscaled.sock, so mount the daemon's socket +# directory at /var/run/tailscale regardless. # TAILSCALE_SOCKET=/var/run/tailscale/tailscaled.sock # Exit when the Tailscale socket stays unreachable past the grace period, so the @@ -88,20 +104,36 @@ TAILSCALE_AUTH_KEY=tskey-auth-xxxxx # stay comfortably longer than a normal tailscaled restart. # SOCKET_LOSS_GRACE_PERIOD=90s -# Logging level: debug, info, warn, or error. +# Logging level for all output, including the cloud module: debug, info, warn, +# or error. # LOG_LEVEL=info +# Log lines are colored only on a terminal. Any value disables color there too. +# NO_COLOR=1 + # ============================================================================= # DockTail Cloud (optional) # ============================================================================= -# Opt-in reporting to cloud.docktail.org. The module is completely inert unless -# DOCKTAIL_CLOUD_KEY is set. See docs/06-cloud.md (https://docktail.org/cloud/). +# Opt-in reporting to cloud.docktail.org, a paid service. The module is +# completely inert unless DOCKTAIL_CLOUD_KEY is set. See docs/06-cloud.md +# (https://docktail.org/docs/#docktail-cloud) and docker-compose.cloud.yaml. # Workspace key (dtc_...) from the cloud dashboard. # DOCKTAIL_CLOUD_KEY= -# Log level for the cloud module: debug, info, warn, or error. +# Read but currently has no effect; LOG_LEVEL sets the cloud module's level. # DOCKTAIL_LOG_LEVEL=info -# How often local-vantage checks run (5s to 5m). +# How often local-vantage checks run (5s to 5m). An invalid value keeps the +# cloud module from starting. # DOCKTAIL_CHECK_INTERVAL=30s + +# Where the host's root filesystem is bind-mounted read-only (- /:/host:ro) to +# report disk usage for every host filesystem. Used only when mounted. +# DOCKTAIL_HOST_ROOT=/host + +# Local development only: override the ingest endpoint. Plaintext ws:// is +# accepted for loopback hosts, or elsewhere with DOCKTAIL_CLOUD_ALLOW_INSECURE. +# Never use either in production. +# DOCKTAIL_CLOUD_URL=ws://localhost:8080/v1/agent +# DOCKTAIL_CLOUD_ALLOW_INSECURE=false diff --git a/Makefile b/Makefile index e9ddac3..bdb6755 100644 --- a/Makefile +++ b/Makefile @@ -59,14 +59,14 @@ build-all: GOOS=darwin GOARCH=amd64 go build -o $(BINARY_NAME)-darwin-amd64 . GOOS=darwin GOARCH=arm64 go build -o $(BINARY_NAME)-darwin-arm64 . -# Start docker-compose +# Start the development stack (builds from source; see docker-compose.dev.yaml) up: - docker compose up -d + docker compose -f docker-compose.dev.yaml up -d -# Stop docker-compose +# Stop the development stack down: - docker compose down + docker compose -f docker-compose.dev.yaml down -# View logs +# View the development stack's DockTail logs logs: - docker logs -f docktail + docker logs -f dev-docktail diff --git a/README.md b/README.md index 77c6d71..892b1ae 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ docker compose up -d curl http://myapp.your-tailnet.ts.net ``` -This assumes the Docker host is connected to Tailscale and allowed to advertise services. See the full docs for host setup, sidecar setup, rootless Docker, OAuth permissions, ACLs, labels, Funnel, and examples. +This assumes the Docker host is connected to Tailscale and allowed to advertise services. See the full docs for host setup, sidecar setup, rootless Docker, OAuth permissions, ACLs, labels, Funnel, and examples. [`docker-compose.yaml`](docker-compose.yaml) is a ready-to-run version of this setup; for Podman, Synology, Unraid, TrueNAS, macOS, Windows, Swarm and Kubernetes see the [platform guides](docs/02-platforms.md), and for a read-only Docker socket proxy and dropped capabilities see [Hardening](docs/02-security.md). For Docker secrets or other mounted secret files, set `FILE__TAILSCALE_OAUTH_CLIENT_ID` / `FILE__TAILSCALE_OAUTH_CLIENT_SECRET` or `TAILSCALE_OAUTH_CLIENT_ID_FILE` / `TAILSCALE_OAUTH_CLIENT_SECRET_FILE` to the mounted file paths instead of putting the values directly in the environment. @@ -151,6 +151,8 @@ environment: Without the key the module is completely inert: no connection is opened and DockTail behaves exactly as before. The link is outbound-only and metadata-only — the protocol has no exec, deploy, or shell message types, which you can verify in [`cloud/`](cloud/). +DockTail Cloud is a paid service ([plans and pricing](https://docktail.org/cloud/#pricing)); a new workspace can connect one host as an unmonitored preview before you choose a plan. [`docker-compose.cloud.yaml`](docker-compose.cloud.yaml) is a complete example. + [Explore DockTail Cloud](https://docktail.org/cloud/) · [open the dashboard](https://cloud.docktail.org/login) · [agent setup](docs/06-cloud.md) ## Documentation diff --git a/docker-compose.cloud.yaml b/docker-compose.cloud.yaml new file mode 100644 index 0000000..fa34e03 --- /dev/null +++ b/docker-compose.cloud.yaml @@ -0,0 +1,51 @@ +# DockTail with DockTail Cloud reporting, on a Linux host that already runs +# Tailscale. DockTail Cloud is a paid, hosted service: +# https://docktail.org/cloud/ -- agent setup: https://docktail.org/docs/#docktail-cloud +# +# Before you start: +# 1. DockTail works on this host without Cloud (host tagged, tailnet policy +# set up): https://docktail.org/docs/#tailscale-admin-setup +# 2. Tailscale OAuth credentials (or an API key) are configured. Cloud's +# tailnet health check needs them; without them it reports "no Tailscale +# credentials" for this host instead. +# 3. A workspace key (dtc_...) from https://cloud.docktail.org whose +# enrollment window is open. +# +# Usage: +# Put TAILSCALE_OAUTH_CLIENT_ID, TAILSCALE_OAUTH_CLIENT_SECRET and +# DOCKTAIL_CLOUD_KEY in .env (see .env.example), then: +# docker compose -f docker-compose.cloud.yaml up -d +# +# No Tailscale on the host (macOS, Windows, most NAS devices)? Take the +# tailscale service and the tailscale-socket volume from +# docker-compose.sidecar.yaml and mount that volume here instead of +# /var/run/tailscale. + +services: + docktail: + image: ghcr.io/marvinvr/docktail:latest + container_name: docktail + # Required: DockTail exits on purpose when it loses the Tailscale socket and + # relies on the restart policy to come back with a fresh mount. + restart: unless-stopped + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + # Mount the directory, not the socket file. + - /var/run/tailscale:/var/run/tailscale + # Optional. Reports disk usage for every host filesystem, not just the + # one backing /var/lib/docker. Read-only and used for nothing else. + # - /:/host:ro + environment: + - TAILSCALE_OAUTH_CLIENT_ID=${TAILSCALE_OAUTH_CLIENT_ID} + - TAILSCALE_OAUTH_CLIENT_SECRET=${TAILSCALE_OAUTH_CLIENT_SECRET} + # Enables DockTail Cloud reporting. Without it the module is inert. + - DOCKTAIL_CLOUD_KEY=${DOCKTAIL_CLOUD_KEY:?set DOCKTAIL_CLOUD_KEY in .env} + + # Example app. No published ports: DockTail proxies to the container IP. + whoami: + image: traefik/whoami:latest + restart: unless-stopped + labels: + - "docktail.service.enable=true" + - "docktail.service.name=whoami" + - "docktail.service.port=80" diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml new file mode 100644 index 0000000..314ae11 --- /dev/null +++ b/docker-compose.dev.yaml @@ -0,0 +1,119 @@ +# Development stack: builds DockTail from source next to a handful of labelled +# test containers. Not a deployment example -- for that, see +# docker-compose.yaml (plain) or docker-compose.cloud.yaml (with DockTail Cloud). +# +# Usage: +# make up # docker compose -f docker-compose.dev.yaml up -d +# make logs + +services: + # DockTail service + dev-docktail: + build: . + container_name: dev-docktail + restart: unless-stopped + volumes: + # Docker socket for container monitoring + - /var/run/docker.sock:/var/run/docker.sock:ro + # Tailscale socket directory (directory mount survives tailscaled restarts) + - /var/run/tailscale:/var/run/tailscale + environment: + - LOG_LEVEL=debug + - RECONCILE_INTERVAL=60s + + # Example: Direct mode (default) - no ports needed! + web1: + build: ./test/nginx-web1 + container_name: web1 + restart: unless-stopped + # No ports needed - DockTail proxies directly to container IP + labels: + - "docktail.service.enable=true" + - "docktail.service.name=test-web1" + - "docktail.service.port=80" + - "docktail.service.protocol=http" + - "docktail.service.service-port=80" + - "docktail.service.service-protocol=http" + + # Example: Direct mode with HTTPS (using smart defaults) + web2: + build: ./test/nginx-web2 + container_name: web2 + restart: unless-stopped + # No ports needed - DockTail proxies directly to container IP + labels: + - "docktail.service.enable=true" + - "docktail.service.name=test-web2" + - "docktail.service.port=80" + - "docktail.service.service-protocol=https" + # service-port defaults to "443" (based on service-protocol=https) + + # Example: Serve + Funnel (both tailnet AND public internet access) + web-public: + build: ./test/nginx-web1 + container_name: web-public + restart: unless-stopped + # No ports needed - direct mode handles both serve and funnel + labels: + # Serve configuration (tailnet-only): + - "docktail.service.enable=true" + - "docktail.service.name=public-demo" + - "docktail.service.port=80" + - "docktail.service.service-port=443" + + # Funnel configuration (public internet): + - "docktail.funnel.enable=true" + - "docktail.funnel.port=80" + + # Example: TCP service with direct mode + test-tcp: + image: nginx:latest + container_name: test-tcp + restart: unless-stopped + # No ports needed + labels: + - "docktail.service.enable=true" + - "docktail.service.name=test-tcp" + - "docktail.service.port=80" + - "docktail.service.protocol=tcp" + + # Example: Legacy mode (published ports) - opt out of direct mode + test-legacy: + image: nginx:latest + container_name: test-legacy + restart: unless-stopped + ports: + - "9080:80" # Required when direct=false + labels: + - "docktail.service.enable=true" + - "docktail.service.name=test-legacy" + - "docktail.service.port=80" + - "docktail.service.direct=false" # Use published port instead of container IP + + # Example: Custom Docker network + test-custom-network: + image: nginx:latest + container_name: test-custom-network + restart: unless-stopped + networks: + - backend + labels: + - "docktail.service.enable=true" + - "docktail.service.name=test-custom-net" + - "docktail.service.port=80" + - "docktail.service.network=backend" # Specify which network to use + + # Example: Host networking mode + test-host-network: + image: nginx:latest + container_name: test-host-network + restart: unless-stopped + network_mode: host + labels: + - "docktail.service.enable=true" + - "docktail.service.name=test-host" + - "docktail.service.port=80" + # Host network uses localhost directly + +networks: + backend: diff --git a/docker-compose.yaml b/docker-compose.yaml index 64e7019..b21f19e 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,116 +1,37 @@ -version: '3.8' +# DockTail on a Linux host that already runs Tailscale. +# +# Usage: +# 1. Tag the host and set up the tailnet policy: +# https://docktail.org/docs/#tailscale-admin-setup +# 2. Copy .env.example to .env and set the Tailscale OAuth credentials. +# 3. docker compose up -d +# +# No Tailscale on the host (macOS, Windows, most NAS devices)? Use +# docker-compose.sidecar.yaml instead. Want monitoring and alerting from +# DockTail Cloud? See docker-compose.cloud.yaml. Building DockTail itself? +# docker-compose.dev.yaml is the development stack. services: - # DockTail service - dev-docktail: - build: . - container_name: dev-docktail + docktail: + image: ghcr.io/marvinvr/docktail:latest + container_name: docktail + # Required: DockTail exits on purpose when it loses the Tailscale socket and + # relies on the restart policy to come back with a fresh mount. restart: unless-stopped volumes: - # Docker socket for container monitoring - /var/run/docker.sock:/var/run/docker.sock:ro - # Tailscale socket directory (directory mount survives tailscaled restarts) + # Mount the directory, not the socket file. - /var/run/tailscale:/var/run/tailscale environment: - - LOG_LEVEL=debug - - RECONCILE_INTERVAL=60s + # Optional but recommended. Lets DockTail create Service definitions. + - TAILSCALE_OAUTH_CLIENT_ID=${TAILSCALE_OAUTH_CLIENT_ID} + - TAILSCALE_OAUTH_CLIENT_SECRET=${TAILSCALE_OAUTH_CLIENT_SECRET} - # Example: Direct mode (default) - no ports needed! - web1: - build: ./test/nginx-web1 - container_name: web1 + # Example app. No published ports: DockTail proxies to the container IP. + whoami: + image: traefik/whoami:latest restart: unless-stopped - # No ports needed - DockTail proxies directly to container IP labels: - "docktail.service.enable=true" - - "docktail.service.name=test-web1" + - "docktail.service.name=whoami" - "docktail.service.port=80" - - "docktail.service.protocol=http" - - "docktail.service.service-port=80" - - "docktail.service.service-protocol=http" - - # Example: Direct mode with HTTPS (using smart defaults) - web2: - build: ./test/nginx-web2 - container_name: web2 - restart: unless-stopped - # No ports needed - DockTail proxies directly to container IP - labels: - - "docktail.service.enable=true" - - "docktail.service.name=test-web2" - - "docktail.service.port=80" - - "docktail.service.service-protocol=https" - # service-port defaults to "443" (based on service-protocol=https) - - # Example: Serve + Funnel (both tailnet AND public internet access) - web-public: - build: ./test/nginx-web1 - container_name: web-public - restart: unless-stopped - # No ports needed - direct mode handles both serve and funnel - labels: - # Serve configuration (tailnet-only): - - "docktail.service.enable=true" - - "docktail.service.name=public-demo" - - "docktail.service.port=80" - - "docktail.service.service-port=443" - - # Funnel configuration (public internet): - - "docktail.funnel.enable=true" - - "docktail.funnel.port=80" - - # Example: TCP service with direct mode - test-tcp: - image: nginx:latest - container_name: test-tcp - restart: unless-stopped - # No ports needed - labels: - - "docktail.service.enable=true" - - "docktail.service.name=test-tcp" - - "docktail.service.port=80" - - "docktail.service.protocol=tcp" - - # Example: Legacy mode (published ports) - opt out of direct mode - test-legacy: - image: nginx:latest - container_name: test-legacy - restart: unless-stopped - ports: - - "9080:80" # Required when direct=false - labels: - - "docktail.service.enable=true" - - "docktail.service.name=test-legacy" - - "docktail.service.port=80" - - "docktail.service.direct=false" # Use published port instead of container IP - - # Example: Custom Docker network - test-custom-network: - image: nginx:latest - container_name: test-custom-network - restart: unless-stopped - networks: - - backend - labels: - - "docktail.service.enable=true" - - "docktail.service.name=test-custom-net" - - "docktail.service.port=80" - - "docktail.service.network=backend" # Specify which network to use - - # Example: Host networking mode - test-host-network: - image: nginx:latest - container_name: test-host-network - restart: unless-stopped - network_mode: host - labels: - - "docktail.service.enable=true" - - "docktail.service.name=test-host" - - "docktail.service.port=80" - # Host network uses localhost directly - -networks: - backend: - -volumes: - postgres-data: diff --git a/docs/00-overview.md b/docs/00-overview.md index 4d57e28..cede89d 100644 --- a/docs/00-overview.md +++ b/docs/00-overview.md @@ -92,12 +92,12 @@ DockTail uses native Tailscale Services, not per-container Tailscale devices. - 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](#docktail-cloud) for multi-host monitoring and alerting. Opt-in via one environment variable; inert when unset. +- Optionally reports to [DockTail Cloud](#docktail-cloud), a paid hosted service, for multi-host monitoring and alerting. Opt-in via one environment variable; inert when unset. ## 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. +2. Read [Installation](#installation) for host Tailscale and sidecar options, [Platform Guides](#platform-guides) for Podman, NAS systems, macOS, Windows, Swarm and Kubernetes, and [Hardening](#hardening) to narrow what DockTail can reach. 3. Configure Tailscale permissions in [Tailscale Admin Setup](#tailscale-admin-setup). 4. Use [Labels](#labels) and [Examples](#examples) when exposing real services. 5. See [DockTail Cloud](#docktail-cloud) if you want monitoring and alerting across your hosts. diff --git a/docs/01-quick-start.md b/docs/01-quick-start.md index 9ed7a31..287d40b 100644 --- a/docs/01-quick-start.md +++ b/docs/01-quick-start.md @@ -36,4 +36,4 @@ 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](02-installation.md#installation) and [Tailscale Admin Setup](03-tailscale-admin.md#tailscale-admin-setup). On macOS and Windows the host's Tailscale daemon cannot be shared with containers, so use the [Tailscale Sidecar](02-installation.md#tailscale-sidecar) setup instead. Rootless Docker on Linux needs an extra operator step; see [Rootless Docker](02-installation.md#rootless-docker). +This assumes a Linux Docker host that is already connected to Tailscale and allowed to advertise services. If it is not, continue with [Installation](02-installation.md#installation) and [Tailscale Admin Setup](03-tailscale-admin.md#tailscale-admin-setup). On macOS and Windows the host's Tailscale daemon cannot be shared with containers, so use the [Tailscale Sidecar](02-installation.md#tailscale-sidecar) setup instead. Rootless Docker on Linux needs an extra operator step; see [Rootless Docker](02-installation.md#rootless-docker). For Podman, Synology, Unraid, TrueNAS, Swarm and Kubernetes, see the [Platform Guides](02-platforms.md#platform-guides). diff --git a/docs/02-installation.md b/docs/02-installation.md index 6063040..1aa71c5 100644 --- a/docs/02-installation.md +++ b/docs/02-installation.md @@ -1,6 +1,6 @@ ## 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). +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). [Platform Guides](02-platforms.md#platform-guides) covers Podman, NAS systems, Swarm and Kubernetes, and [Hardening](02-security.md#hardening) narrows what DockTail can reach. ### Tailscale On Host diff --git a/docs/02-platforms.md b/docs/02-platforms.md new file mode 100644 index 0000000..98db0d4 --- /dev/null +++ b/docs/02-platforms.md @@ -0,0 +1,151 @@ +## Platform Guides + +DockTail has two hard requirements wherever it runs: + +- **A Docker Engine API.** DockTail discovers containers and reads their `docktail.*` labels through the Docker socket (or `DOCKER_HOST`). It only reads: it lists and inspects containers and follows container events, plus engine info, stats and log tails when [DockTail Cloud](06-cloud.md#docktail-cloud) is on. +- **A `tailscaled` socket it can write serve config to.** Either the host's daemon on Linux ([Tailscale On Host](02-installation.md#tailscale-on-host)) or a `tailscale/tailscale` container next to it ([Tailscale Sidecar](02-installation.md#tailscale-sidecar)). That daemon's node must carry a tag your policy lets advertise services ([ACL Configuration](03-tailscale-admin.md#acl-configuration)); `service hosts must be tagged nodes` in the logs means it does not. + +A third, softer one: a **restart policy**. DockTail exits on purpose when it loses the Tailscale socket and relies on the restart to come back with a fresh mount ([Tailscale Socket Loss](07-reference.md#tailscale-socket-loss)). + +The guides below say how each platform meets those, and how well each is known to work. The maintainers test DockTail on plain Linux Docker; everything else is based on user reports in the issue tracker and on the requirements above. + +### Podman + +Reported working. Podman serves a Docker-compatible API socket, and DockTail uses it like the Docker one: mount it at `/var/run/docker.sock`. + +```bash +# Rootful Podman: the socket is /run/podman/podman.sock +sudo systemctl enable --now podman.socket + +# Rootless Podman: the socket is /run/user//podman/podman.sock +systemctl --user enable --now podman.socket +``` + +```yaml +services: + docktail: + image: ghcr.io/marvinvr/docktail:latest + restart: unless-stopped + volumes: + - /run/podman/podman.sock:/var/run/docker.sock:ro # or /run/user/1000/podman/podman.sock + - /var/run/tailscale:/var/run/tailscale + environment: + - TAILSCALE_OAUTH_CLIENT_ID=${TAILSCALE_OAUTH_CLIENT_ID} + - TAILSCALE_OAUTH_CLIENT_SECRET=${TAILSCALE_OAUTH_CLIENT_SECRET} +``` + +- **One DockTail per `tailscaled`.** One instance watches one engine. If rootful and rootless Podman (or Podman and Docker) run on the same host, a second DockTail against the same `tailscaled` will fight the first over which services the node serves. `IGNORE_SERVICE_NAMES` on each instance keeps them apart; separate machines or VMs, each with its own Tailscale node, avoid the problem. +- **Rootless Podman** has the same two issues as [Rootless Docker](02-installation.md#rootless-docker): the host's `tailscaled` must trust the user (`tailscale set --operator`), and container IPs are often unreachable from the host, so use `docktail.service.direct=false` with a published port or run the Tailscale sidecar on the app's network. +- **SELinux.** On an enforcing host (Fedora, RHEL and derivatives), SELinux normally keeps a container from connecting to a host socket it has mounted; relaxing labelling for the DockTail container (`security_opt: [label=disable]`) is the usual fix. Not tested with DockTail. +- DockTail Cloud reporting with Podman has not been tested. + +### Synology + +Not tested by the maintainers. Container Manager (DSM 7.2 and later) is Docker, so its socket is at `/var/run/docker.sock` and a Compose file runs as a **Project**. + +Use the [Tailscale Sidecar](02-installation.md#tailscale-sidecar) setup, or start from [`docker-compose.sidecar.yaml`](https://github.com/marvinvr/docktail/blob/main/docker-compose.sidecar.yaml). DockTail has not been tried against the Tailscale package from the Package Center; the sidecar gives DockTail a daemon of its own instead of depending on where the package keeps its socket. + +The sidecar needs `/dev/net/tun`. If the `tailscale` container logs that it is missing, follow Tailscale's Synology documentation for loading the TUN module at boot. + +### Unraid + +Reported to reach the host's `tailscaled` at `/var/run/tailscale` with the [Tailscale On Host](02-installation.md#tailscale-on-host) setup; the one problem reported was an untagged node (below). The rest of this section is not tested by the maintainers. DockTail can run as a Compose stack (Compose Manager plugin) or as a container added from the Docker tab: + +| Setting | Value | +| --- | --- | +| Repository | `ghcr.io/marvinvr/docktail:latest` | +| Path | `/var/run/docker.sock` → `/var/run/docker.sock`, read-only | +| Path | `/var/run/tailscale` → `/var/run/tailscale` | +| Variables | `TAILSCALE_OAUTH_CLIENT_ID`, `TAILSCALE_OAUTH_CLIENT_SECRET` | + +Make sure the container restarts on its own after it exits; if yours does not, add `--restart unless-stopped` under Extra Parameters. + +- **Tag the Unraid node.** `Failed to add service ... service hosts must be tagged nodes` means the host is not tagged. Run `tailscale up --advertise-tags=tag:server` in the Unraid terminal and add the tag to your policy ([ACL Configuration](03-tailscale-admin.md#acl-configuration)). +- A `Warning: client version ... != tailscaled server version ...` line only says that the `tailscale` CLI in the image and the plugin's daemon are different versions; on its own it is not an error. +- Containers with their own address on a macvlan or ipvlan network (Unraid's `br0`) are not reachable from the host by default, and in direct mode it is the host's `tailscaled` that connects to them. Keep exposed apps on a bridge network, or enable host access to custom networks in Unraid's Docker settings. + +### TrueNAS SCALE + +Not tested by the maintainers. TrueNAS SCALE 24.10 (Electric Eel) and later run apps on Docker, so DockTail can be installed as a custom app from YAML. Releases before 24.10 run apps on Kubernetes and are not supported (see [Kubernetes](#kubernetes)). + +Paste the [Tailscale Sidecar](02-installation.md#tailscale-sidecar) Compose file as the custom app's YAML, so DockTail and its `tailscaled` are deployed together. The Docker socket is at `/var/run/docker.sock`. Apps you expose must be reachable from the sidecar: it runs with host networking, which reaches container IPs on the host's Docker networks. + +### macOS And Windows + +Supported with the [Tailscale Sidecar](02-installation.md#tailscale-sidecar) only. The Tailscale app on macOS and Windows does not expose a Unix socket, and Docker Desktop, OrbStack and Colima run containers in a Linux VM that cannot mount host Unix sockets anyway. `dial unix /var/run/tailscale/tailscaled.sock: connect: no such file or directory` is the symptom of trying the host setup. + +- The sidecar needs its own auth key (`TAILSCALE_AUTH_KEY`) in addition to DockTail's OAuth credentials, and joins the tailnet as its own device. +- The sidecar uses `network_mode: host`. On Docker Desktop, enable host networking under Settings -> Resources -> Network, or drop that line and attach the sidecar to the same Docker network as the apps you expose. +- With DockTail Cloud, host vitals and disk usage describe the Linux VM the containers run in, not the Mac or PC. + +### Docker Swarm + +No native Swarm support: DockTail reads **container** labels from the engine it is connected to, so it sees only the tasks running on its own node and ignores service-level `deploy.labels`. The layout below is based on the one users reported working on multi-node clusters in [issue #43](https://github.com/marvinvr/docktail/issues/43): + +- Run DockTail as a `global` service, so every node has one, each with its own `tailscaled` — installed on the host, or a `global` sidecar as below. Each node joins the tailnet as a tagged device. +- Put the `docktail.*` labels under the service's top-level `labels:`, which Swarm applies to each task container, not under `deploy.labels`. +- Every node that runs a replica advertises the service, and Tailscale Services routes each client to one of the advertising hosts. +- Allow at most one replica of a labelled service per node (`deploy.placement.max_replicas_per_node: 1`, or `mode: global`). Two replicas on one node claim the same service name and port, which DockTail treats as a conflict and does not resolve ([Service Name Conflicts Between Containers](04-labels.md#service-name-conflicts-between-containers)). +- Avoid `update_config.order: start-first` for the `tailscale` service. It briefly runs two daemons on the same node and state volume; the working reports use the default order. + +```yaml +services: + tailscale: + image: tailscale/tailscale:latest + environment: + - TS_AUTHKEY=${TAILSCALE_AUTH_KEY} # a reusable key: every node uses it + - TS_EXTRA_ARGS=--advertise-tags=tag:server + - TS_STATE_DIR=/var/lib/tailscale + - TS_SOCKET=/var/run/tailscale/tailscaled.sock + - TS_USERSPACE=false + volumes: + - tailscale-state:/var/lib/tailscale + - tailscale-socket:/var/run/tailscale + - /dev/net/tun:/dev/net/tun + cap_add: + - NET_ADMIN + - SYS_MODULE + deploy: + mode: global + + docktail: + image: ghcr.io/marvinvr/docktail:latest + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - tailscale-socket:/var/run/tailscale + environment: + - TAILSCALE_OAUTH_CLIENT_ID_FILE=/run/secrets/tailscale_oauth_client_id + - TAILSCALE_OAUTH_CLIENT_SECRET_FILE=/run/secrets/tailscale_oauth_client_secret + secrets: + - tailscale_oauth_client_id + - tailscale_oauth_client_secret + deploy: + mode: global + + whoami: + image: traefik/whoami:latest + labels: + - "docktail.service.enable=true" + - "docktail.service.name=whoami" + - "docktail.service.port=80" + deploy: + replicas: 2 + placement: + max_replicas_per_node: 1 + +volumes: + tailscale-state: + tailscale-socket: + +secrets: + tailscale_oauth_client_id: + external: true + tailscale_oauth_client_secret: + external: true +``` + +Named volumes are local to each node, so each node's `tailscale` task keeps its own state and shares its socket only with the DockTail task on the same node. The sidecar here is not on the host network; it reaches the apps over the stack's overlay network, so deploy them in the same stack or attach them to a shared network. + +### Kubernetes + +Not supported, and there is no Helm chart. Kubernetes nodes run containerd or CRI-O without a Docker Engine API for DockTail to watch, and workloads are described by pod annotations and Services rather than Docker labels. For Kubernetes, use Tailscale's own Kubernetes operator. diff --git a/docs/02-security.md b/docs/02-security.md new file mode 100644 index 0000000..867249a --- /dev/null +++ b/docs/02-security.md @@ -0,0 +1,70 @@ +## Hardening + +DockTail needs two sockets, and both are powerful. This section says what they grant and how to narrow it. None of it is required, and none of it is covered by DockTail's automated tests; what DockTail needs is taken from its source. + +### The Docker Socket + +Every example mounts `/var/run/docker.sock:ro`. The `:ro` flag makes the bind mount read-only, which does not matter for a socket: it does not stop anyone from connecting to it, and a connection gets the full Docker API — start, exec, create privileged containers. Treat access to the Docker socket as root on the host. + +DockTail itself only reads from it: it lists and inspects containers and follows container events, and with [DockTail Cloud](06-cloud.md#docktail-cloud) on it also reads engine info and version, one-shot container stats, and log tails. It never creates, starts, stops or execs anything. To make that a guarantee rather than a property of the code, put a read-only proxy in front of the socket. + +### Read-Only Socket Proxy + +[`tecnativa/docker-socket-proxy`](https://github.com/Tecnativa/docker-socket-proxy) allows only the API sections you enable and, with `POST=0` (its default), only `GET` and `HEAD` requests. DockTail needs `CONTAINERS` on top of the sections the proxy allows by default (`EVENTS`, `PING`, `VERSION`), and `INFO` as well when DockTail Cloud is on: + +```yaml +services: + docker-socket-proxy: + image: tecnativa/docker-socket-proxy:latest + restart: unless-stopped + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + environment: + - CONTAINERS=1 # list, inspect, stats, logs + - INFO=1 # DockTail Cloud only: engine ID and host specs + - EVENTS=1 # default; container start/stop/die/restart/oom/health events + - PING=1 # default; API version negotiation + - VERSION=1 # default + - POST=0 # default; read-only + networks: + - docker-api + + docktail: + image: ghcr.io/marvinvr/docktail:latest + restart: unless-stopped + depends_on: + - docker-socket-proxy + volumes: + - /var/run/tailscale:/var/run/tailscale + environment: + - DOCKER_HOST=tcp://docker-socket-proxy:2375 + - TAILSCALE_OAUTH_CLIENT_ID=${TAILSCALE_OAUTH_CLIENT_ID} + - TAILSCALE_OAUTH_CLIENT_SECRET=${TAILSCALE_OAUTH_CLIENT_SECRET} + networks: + - default # outbound: the Tailscale API and DockTail Cloud + - docker-api + +networks: + docker-api: + internal: true +``` + +- Keep the proxy on an `internal` network that only DockTail joins, and never publish port 2375: anyone who can reach it can read what DockTail reads. DockTail itself also stays on a normal network, since it calls the Tailscale API (and DockTail Cloud) over the internet. +- Read-only is not the same as harmless. `CONTAINERS=1` allows every `GET` under `/containers`: inspecting any container (including environment variables that may hold other apps' secrets), reading its logs, and downloading files from it through the archive and export endpoints. +- If the proxy closes the event stream, DockTail logs `Docker event stream error` and reconnects after five seconds; the periodic reconcile covers anything that changed in between. + +### Container Privileges + +The image runs DockTail as root. It needs no Linux capabilities for its own work — it talks to Unix sockets (or the proxy above), reads files, and runs the `tailscale` CLI against `tailscaled`'s socket — so you can drop them all and block privilege escalation: + +```yaml +services: + docktail: + image: ghcr.io/marvinvr/docktail:latest + cap_drop: + - ALL + security_opt: + - no-new-privileges:true +``` + +Dropping `CAP_DAC_OVERRIDE` also makes root subject to ordinary file permission bits. Credential files loaded through `*_FILE` or `FILE__*` must then be owned by root or readable by everyone inside the container; Docker Swarm secrets are, by default. A `0600` file owned by your own user, bind-mounted from the host, is not, and DockTail exits at startup when it cannot read it. diff --git a/docs/06-cloud.md b/docs/06-cloud.md index cd68ad3..5d3312f 100644 --- a/docs/06-cloud.md +++ b/docs/06-cloud.md @@ -4,6 +4,8 @@ DockTail Cloud is optional, opt-in monitoring for DockTail-managed services acro [Explore DockTail Cloud](https://docktail.org/cloud/) or [open the dashboard](https://cloud.docktail.org/login). +DockTail Cloud is a paid service, priced by host count ([plans and pricing](https://docktail.org/cloud/#pricing)); the agent code that reports to it is the same open-source DockTail. Until you choose a plan, a new workspace can connect one host as a preview: it shows up online, but Cloud runs no checks and raises no incidents or alerts for it. A workspace's first subscription can come with an introductory offer, such as a free trial or a reduced first-months price; the dashboard shows which one applies when you choose a plan. + ### 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. @@ -15,10 +17,18 @@ Reporting rides along with the normal agent — there is no separate binary. The ### How To Enable +Before you start: + +- **Tailscale API credentials.** Configure `TAILSCALE_OAUTH_CLIENT_ID`/`TAILSCALE_OAUTH_CLIENT_SECRET` (or `TAILSCALE_API_KEY`) first, as in [Tailscale Admin Setup](03-tailscale-admin.md#tailscale-admin-setup). Cloud's [tailnet vantage](#tailnet-health) reads the control plane through them, and one credentialed host per tailnet is enough. With none, Cloud reports "no Tailscale credentials" instead of approval and advertisement state; everything else still works. +- **A working DockTail.** The host is tagged, and its services already show up on the tailnet without Cloud. +- **A plan, for monitoring.** You can connect the first host before choosing one; it stays an unmonitored preview until you do (see above). + +Then: + 1. Create a workspace in the DockTail Cloud dashboard and copy the workspace key (`dtc_...`). 2. Set `DOCKTAIL_CLOUD_KEY` on the DockTail agent. -That is the only configuration — the cloud endpoint is built into the agent. +That is the only configuration — the cloud endpoint is built into the agent. A complete Compose file with it is [`docker-compose.cloud.yaml`](https://github.com/marvinvr/docktail/blob/main/docker-compose.cloud.yaml). ```yaml services: @@ -48,7 +58,7 @@ connection when that changes; the agent does not need a restart. | 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_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). | | `DOCKTAIL_HOST_ROOT` | `/host` | Where the host's root filesystem is bind-mounted, for whole-host disk usage (see [Disk Usage](#disk-usage)). Only used when that path exists. | @@ -175,7 +185,8 @@ events, metrics, and incidents are unaffected. 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 +(one hour by default; the dashboard's agent key settings offer other lengths when a +key is created or its enrollment reopened). 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. diff --git a/docs/07-reference.md b/docs/07-reference.md index 6a05536..ffa6e2e 100644 --- a/docs/07-reference.md +++ b/docs/07-reference.md @@ -14,10 +14,10 @@ Use this section when checking exact configuration names, defaults, and supporte | `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](#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](#cleanup-behavior). | -| `LOG_LEVEL` | `info` | Logging level: `debug`, `info`, `warn`, or `error`. | +| `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//docker.sock`. | -| `TAILSCALE_SOCKET` | `/var/run/tailscale/tailscaled.sock` | Tailscale daemon socket. | +| `TAILSCALE_SOCKET` | `/var/run/tailscale/tailscaled.sock` | The `tailscaled` socket DockTail checks: the startup missing-socket hint, the [socket-loss check](#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](#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. | @@ -35,6 +35,22 @@ Supported file-backed credential variables: `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](02-security.md#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](#docktail-cloud). @@ -42,8 +58,8 @@ These variables enable optional DockTail Cloud reporting. They are opt-in: the a | 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). | +| `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](06-cloud.md#disk-usage). | Local-development overrides: `DOCKTAIL_CLOUD_URL` replaces the built-in ingest diff --git a/test/README.md b/test/README.md index af39047..68c84af 100644 --- a/test/README.md +++ b/test/README.md @@ -17,7 +17,7 @@ Both containers: ## Usage -These are referenced in the root `docker-compose.yaml` and serve as examples for: +These are referenced in the root `docker-compose.dev.yaml` and serve as examples for: - Port publishing requirements - Label configuration - Multi-instance service setup