services: tailscale: image: tailscale/tailscale:v1.96.5 container_name: ods-tailscale restart: unless-stopped # network_mode: host puts the Tailscale daemon in the host's network # namespace so the device's tailnet IP (100.x.y.z) is the host # itself. **It does NOT make loopback-bound services reachable # from the tailnet.** Incoming tailnet packets arrive on the host's # Tailscale interface, not 127.0.0.1, so a service bound to # 127.0.0.1:3000 still refuses them. To make ODS services # reachable over the tailnet, also: # - Enable ods-proxy (port 80 with host-based routing) # - Set BIND_ADDRESS=0.0.0.0 in .env so the proxy listens on # the LAN interface (which is the same interface the tailscaled # daemon is observing). # See docs/TAILSCALE.md "Prerequisites for tailnet reachability". # # Linux only. macOS + Docker Desktop need a different topology # (userspace networking + tailscale serve); not supported in v1. network_mode: host cap_add: # NET_ADMIN: manipulate routing tables (Tailscale's mesh) # NET_RAW: send raw packets (NAT traversal probes) - NET_ADMIN - NET_RAW devices: # The TUN device is how Tailscale's userspace daemon hands packets # back to the kernel. Without this, tailscale up exits with # "no TUN device available". - /dev/net/tun:/dev/net/tun environment: # TS_AUTHKEY is consumed once on first join; the daemon caches the # resulting node key in TS_STATE_DIR so the auth key can be rotated # or deleted afterward. Empty value = container starts but doesn't # join (waits for the user to set it). - TS_AUTHKEY=${TS_AUTHKEY:-} - TS_HOSTNAME=${TS_HOSTNAME:-${ODS_DEVICE_NAME:-ods}} - TS_STATE_DIR=/var/lib/tailscale # Optional extra flags for `tailscale up`. Leave empty for a normal # auth key. Operators with a tailnet ACL tag configured can set: # TS_EXTRA_ARGS=--advertise-tags=tag:ods # A hard-coded tag breaks first join on tailnets that do not define # or authorize that tag, so ODS keeps this opt-in. - TS_EXTRA_ARGS=${TS_EXTRA_ARGS:-} # Userspace mode is OFF — we want full kernel networking so the # tailscaled daemon ends up sharing the host's network namespace. # That places the device's tailnet IP on the host (rather than # inside the container) so ODS's host-bound services can answer # tailnet traffic — provided they're bound to 0.0.0.0 / a real # interface, not loopback. See the network_mode comment above. - TS_USERSPACE=false volumes: - ./data/tailscale:/var/lib/tailscale:z healthcheck: # `tailscale status` returns 0 if the daemon is up and authenticated, # 1 if not. Useful for "is this device on the tailnet right now." test: ["CMD", "tailscale", "status", "--json"] interval: 30s timeout: 10s retries: 3 start_period: 30s logging: driver: "json-file" options: max-size: "10m" max-file: "3"