Skip to content

docs(deploy): document Tailscale Funnel as an alternative HTTPS exposure#22

Merged
cmeans-claude-dev[bot] merged 1 commit into
mainfrom
docs/tailscale-funnel-deployment-v2
Apr 26, 2026
Merged

docs(deploy): document Tailscale Funnel as an alternative HTTPS exposure#22
cmeans-claude-dev[bot] merged 1 commit into
mainfrom
docs/tailscale-funnel-deployment-v2

Conversation

@cmeans-claude-dev

@cmeans-claude-dev cmeans-claude-dev Bot commented Apr 26, 2026

Copy link
Copy Markdown
Contributor

Replaces #19. Original PR closed due to a CreateEvent leak under the v1 bot-push design (the first push of docs/tailscale-funnel-deployment was attributed to cmeans instead of the bot, which would block merge under require_last_push_approval once cmeans approves). The v2 design that prevents this class of leak landed in cmeans/claude-dev#4; this replacement branch was created under v2 so the CreateEvent actor is cmeans-claude-dev[bot]. Branch contents identical (same commit cherry-picked).


Summary

Self-hosters behind CGNAT, on rotating residential IPs, or who'd rather not expose a home IP in public DNS have been left without a documented path in deploy/. Tailscale Funnel is a clean fit: routes inbound public-internet traffic through Tailscale's relay to a localhost port, with HTTPS terminated by Tailscale, on the free Personal plan.

Adds a new ## Alternative HTTPS exposure: Tailscale Funnel section to deploy/README.md, positioned after the three runtime approaches (systemd / Docker / Compose) and before ## Required environment. The section is orthogonal to runtime choice — Funnel just swaps in for Caddy + Let's Encrypt + DDNS + the router port-forward, regardless of whether the collector runs as a systemd timer, host-cron'd Docker, or Compose run-once. A short cross-reference paragraph after the Pick an approach table points readers at it so it's discoverable without reading the whole doc end-to-end.

Trade-offs documented:

  • URL is <device>.<tailnet>.ts.net on the free plan, locked to the tailnet (custom domains require a paid plan).
  • Funnel's public-facing HTTPS port must be 443, 8443, or 10000; the local service can listen on any port.
  • Non-configurable bandwidth limits (Tailscale doesn't publish exact figures). For a once-per-day JSON cached at shields.io's CDN this is a non-issue.
  • One more daemon to keep updated. End-to-end encrypted relay; home IP stays hidden from clients.

Setup walked end-to-end:

Five-command path against the bare-systemd runtime — tailscale up, systemd-run'd python3 -m http.server bound to 127.0.0.1:8443, a single tailscale funnel --bg, URL discovery via tailscale funnel status, and a curl -sI smoke-check. Tear-down is two commands. Includes the URL-encoded shields.io endpoint snippet for the README badge update.

CHANGELOG entry under [Unreleased] / Added describes the new section, the trade-offs, and the orthogonality to runtime choice.

No code change; deploy/README.md + CHANGELOG.md only.

Test plan

  • deploy/README.md renders correctly on the GitHub blob view (anchor link from the Pick-an-approach paragraph resolves).
  • All four external links resolve: tailscale.com/kb/1223/funnel, tailscale.com/install.sh, the embedded shields.io endpoint URL, the embedded PyPI project URL.
  • pypi-winnow-downloads-status and project:pypi-winnow-downloads awareness entries are unaffected (no in-tree code touched, no behavior change).
  • Spot-check the section's commands against a real Tailscale install (optional, but: tailscale up, tailscale funnel --bg http://127.0.0.1:8443, tailscale funnel status should all be valid against current Tailscale CLI).
  • Verify cross-reference anchor — the Pick-an-approach link #alternative-https-exposure-tailscale-funnel matches the auto-generated GitHub anchor for the heading.
  • Confirm no drift: deploy/caddy/Caddyfile.example, deploy/systemd/*.service, and the existing systemd / Docker / Compose sections are unchanged — Funnel is additive, not a replacement.

@github-actions github-actions Bot added Awaiting CI Dev complete, waiting for CI/Codecov to pass before QA Ready for QA Dev work complete — QA can begin review and removed Awaiting CI Dev complete, waiting for CI/Codecov to pass before QA labels Apr 26, 2026
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@cmeans cmeans added QA Active QA is actively reviewing; Dev should not push changes and removed Ready for QA Dev work complete — QA can begin review labels Apr 26, 2026

@cmeans cmeans left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QA round 1 (v2 branch) — clean

Tree-equality check: git diff docs/tailscale-funnel-deployment docs/tailscale-funnel-deployment-v2-qa is empty. v2 is bit-identical to v1 head 6fa23f8; only the CreateEvent attribution differs.

Static verification on head eee5c8a:

Check Result
Diff scope CHANGELOG.md (+16) + deploy/README.md (+93), 0 deletions. No other paths touched.
Drift on adjacent runtime sections deploy/caddy/, deploy/systemd/, deploy/docker/ — zero diff. Funnel block is purely additive between Compose and Required-environment.
Anchor slug match Heading ## Alternative HTTPS exposure: Tailscale Funnel → GitHub auto-slug alternative-https-exposure-tailscale-funnel. Cross-reference link #alternative-https-exposure-tailscale-funnel matches exactly.
External links (curl -sI -L) tailscale.com/kb/1223/funnel → 308 (canonical KB redirect, expected); tailscale.com/install.sh → 200; img.shields.io/endpoint?url=... → 200 (SVG); pypi.org/project/ → 200. All four resolve.
In-tree code touched none — src/, tests/, deploy unit files, Caddyfile, Dockerfile/compose all unchanged. Awareness pypi-winnow-downloads-status / project:pypi-winnow-downloads entries unaffected.
CHANGELOG [Unreleased] / Added entry present, accurately describes section + trade-offs + orthogonality
CI on PR head all SUCCESS (test 3.11/3.12/3.13, lint, typecheck, on-push, qa-approved)

Tailscale CLI commands (test-plan item 4, optional): not exercised against a live tailscale binary in this session, but each command is canonical against current Tailscale CLI surface — tailscale up, tailscale funnel --bg <url>, tailscale funnel status, tailscale funnel --https=443 off. Tear-down's --https=443 correctly matches the default public Funnel port (the localhost-side 127.0.0.1:8443 from setup is the local port, not the Funnel-public port). Leaving item 4 unchecked since it's marked optional in the test plan.

Test-plan checkboxes: ticked items 1, 2, 3, 5, 6 (all verified pre-merge). Item 4 left for the maintainer if they want to exercise the commands on a live host.

No findings. Transitioning label to Ready for QA Signoff.

@cmeans

cmeans commented Apr 26, 2026

Copy link
Copy Markdown
Owner

Applying Ready for QA Signoff — see review above. v2 tree-identical to v1 6fa23f8, all static + link checks green on eee5c8a, no drift in adjacent runtime sections, no findings. Item 4 (live Tailscale CLI spot-check) left optional for the maintainer.

@cmeans cmeans added Ready for QA Signoff QA passed — ready for maintainer final review and merge and removed QA Active QA is actively reviewing; Dev should not push changes labels Apr 26, 2026
Self-hosters behind CGNAT, on rotating residential IPs, or who'd
rather not expose a home IP in public DNS have been left without a
documented path in deploy/. Tailscale Funnel is a clean fit for the
use case: routes inbound traffic from the public internet through
Tailscale's relay to a localhost port, with HTTPS terminated by
Tailscale, on the free Personal plan.

Adds a new ## Alternative HTTPS exposure: Tailscale Funnel section
positioned after the three runtime approaches (systemd / Docker /
Compose) and before Required environment. The section is orthogonal
to runtime — Funnel just swaps in for Caddy + Let's Encrypt + DDNS +
the router port-forward, regardless of which runtime above you pick.
A short cross-reference paragraph after the Pick-an-approach table
points readers at it.

Trade-offs documented:
- URL is <device>.<tailnet>.ts.net on the free plan, locked to the
  tailnet (custom domains require a paid plan).
- Funnel's public-facing HTTPS port must be 443, 8443, or 10000;
  local service can listen on any port.
- Non-configurable bandwidth limits (Tailscale doesn't publish exact
  figures). Once-per-day JSON cached at shields.io's CDN is a
  non-issue.
- One more daemon to keep updated; end-to-end encrypted relay so
  your home IP stays hidden.

Five-command setup walks through tailscale install + up,
systemd-run'd python3 -m http.server bound to 127.0.0.1:8443, a
single tailscale funnel --bg invocation, URL discovery, and a curl
smoke-check. Tear-down is two commands. Includes the URL-encoded
shields.io endpoint snippet for the README badge update.

CHANGELOG entry under [Unreleased] / Added describes the new
section, the trade-offs documented, and the orthogonality to runtime
choice.

No code change; deploy/README.md + CHANGELOG.md only.
@cmeans-claude-dev cmeans-claude-dev Bot force-pushed the docs/tailscale-funnel-deployment-v2 branch from eee5c8a to 31b21c0 Compare April 26, 2026 18:26
@github-actions github-actions Bot added Awaiting CI Dev complete, waiting for CI/Codecov to pass before QA Ready for QA Dev work complete — QA can begin review and removed Ready for QA Signoff QA passed — ready for maintainer final review and merge Awaiting CI Dev complete, waiting for CI/Codecov to pass before QA labels Apr 26, 2026
@cmeans cmeans added QA Active QA is actively reviewing; Dev should not push changes and removed Ready for QA Dev work complete — QA can begin review labels Apr 26, 2026

@cmeans cmeans left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QA round 2 (post-rebase) — clean

Rebase verification: New head 31b21c0, previous head eee5c8a. git diff eee5c8a..31b21c0 returns empty — the working tree is bit-identical, only the parent commit moved forward onto the new main (which now includes #20 + #21). No content changes from round 1.

Re-verification on 31b21c0:

Check Result
git diff old vs new head empty (bit-identical tree)
Diff vs new main CHANGELOG.md +16, deploy/README.md +93, 0 deletions — same scope as round 1
Drift check on deploy/caddy/, deploy/systemd/, deploy/docker/ zero diff vs new main
CHANGELOG [Unreleased] / Added ordering new Tailscale entry above the pre-existing Acknowledgments/License/pypinfo entries — clean rebase, no duplicate sections
Cross-ref anchor + heading still co-located (line 43 link → line 156 heading); slug match unchanged
External links re-curl'd all 200 (tailscale.com/kb/1223/funnel, tailscale.com/install.sh, img.shields.io/endpoint, pypi.org/project/)
CI on 31b21c0 all SUCCESS (test 3.11/3.12/3.13, lint, typecheck, on-push, qa-approved, dependabot.yml — the latter is now active because #21 merged)

Test-plan checkboxes from round 1 still apply (1, 2, 3, 5, 6 ticked; 4 left optional). No new findings.

Re-applying Ready for QA Signoff.

@cmeans

cmeans commented Apr 26, 2026

Copy link
Copy Markdown
Owner

Re-applying Ready for QA Signoff after round 2 — see review above. Rebase is content-clean (git diff eee5c8a..31b21c0 empty), only the parent commit moved forward onto post-#20/#21 main. CI green on new head, drift checks unchanged, external links still resolve.

@cmeans cmeans added Ready for QA Signoff QA passed — ready for maintainer final review and merge and removed QA Active QA is actively reviewing; Dev should not push changes labels Apr 26, 2026

@cmeans cmeans left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@cmeans cmeans added QA Approved Manual QA testing completed and passed and removed Ready for QA Signoff QA passed — ready for maintainer final review and merge labels Apr 26, 2026
@cmeans-claude-dev cmeans-claude-dev Bot merged commit 1647350 into main Apr 26, 2026
35 checks passed
@cmeans-claude-dev cmeans-claude-dev Bot deleted the docs/tailscale-funnel-deployment-v2 branch April 26, 2026 18:42
cmeans-claude-dev Bot added a commit that referenced this pull request Apr 27, 2026
Three mechanical edits:

- pyproject.toml: version "0.1.0" -> "0.1.1"
- CHANGELOG.md: insert `## [0.1.1] - 2026-04-26` directly under
  the (still empty) `## [Unreleased]` header so all 12 PRs'
  worth of bullets that have been accumulating since v0.1.0
  ship are now categorized under the 0.1.1 release. Updated
  the link refs at the bottom: [Unreleased] now compares from
  v0.1.1, and a new [0.1.1] entry compares v0.1.0...v0.1.1.
- uv.lock: refreshed by `uv lock` so the locked
  pypi-winnow-downloads version (0.1.1) matches pyproject.toml.

What ships in v0.1.1 (highlights — full changelog under
## [0.1.1]):

Library fixes (operator-visible):
- collector: _write_health OSError no longer escapes
  per-package isolation. Disk-full / perm errors now produce
  structured `winnow-collect: ...; health file write failed:
  [Errno 28] No space left on device` exit instead of a raw
  traceback. Closes #32.
- collector: stale_threshold_days is now actually consulted —
  the "warn if previous run is older than N days" feature
  documented in config.example.yaml since v0.1.0 finally
  fires. Log-only per the documented v1 contract; degrades
  silently on first-run / unreadable / malformed / future-
  timestamped previous _health.json. Closes #33.

Documentation:
- README acknowledgments / license / BigQuery dataset link
  refresh (PR #15)
- README shields.io URL canonicalization (PR #27, closes #16)
- deploy/README.md Tailscale Funnel as alternative HTTPS
  exposure (PR #22)
- deploy/README.md "Pick an approach" table updated to
  reflect the new Caddy logging shape (in PR #30)

CI / project infrastructure (no PyPI consumer impact, but
hardens future releases):
- Community health files: CONTRIBUTING / CoC / SECURITY /
  issue templates (PR #20)
- .github/dependabot.yml across pip + github-actions + docker
  ecosystems (PR #21)
- Dependabot PR hygiene cascade from cmeans/mcp-synology:
  PULL_REQUEST_TEMPLATE.md + auto-CHANGELOG workflow (App-
  token authenticated so required CI re-fires on the bot's
  HEAD SHA) + dependabot.yml prefix fix (PR #25). Validated
  end-to-end via the first two real Dependabot bumps PR #23
  (codecov-action 5->6) and PR #24 (python 3.13-slim ->
  3.14-slim).
- deploy-smoke CI job that builds the Dockerfile, smokes the
  entrypoint, validates compose+Caddyfile against caddy:2
  (PR #29, closes #7). Promoted to required status check on
  the main-protection ruleset 2026-04-26 22:43 (issue #31
  closed via operator action).
- deploy/caddy/Caddyfile.example gains global error logger +
  per-site access logger with built-in lumberjack rotation,
  documents the validate-as-root gotcha (PR #30). Live CT 112
  deployment fixed in the same change.
- 100% coverage on src/ via real tests (no `# pragma: no
  cover`), with `fail_under = 100` gate in pyproject.toml so
  future regressions trip CI (PR #38, closes #37).

Verified locally: 71/71 pytest pass, ruff/format/mypy clean,
coverage gate green at 100.00%.

After this merges:
1. Tag the squash-merge commit as v0.1.1 and push the tag —
   publish.yml fires and uploads to PyPI via the existing
   trusted-publisher OIDC flow.
2. Update the live CT 112 deployment to install
   pypi-winnow-downloads==0.1.1 from PyPI (currently runs a
   wheel built from main, but pinning to the released
   version keeps deploy reproducible).
3. Close any post-release follow-ups Chris wants tracked.

Co-authored-by: cmeans-claude-dev[bot] <272174644+cmeans-claude-dev[bot]@users.noreply.github.com>
cmeans-claude-dev Bot added a commit that referenced this pull request Apr 27, 2026
)

Documentation-only patch release. v0.1.1 shipped with three CHANGELOG
entries miscategorized under `### Fixed` instead of `### Added`:

- `deploy/README.md` Tailscale Funnel section (originally added in PR #22)
- README `## Acknowledgments` and `## License` sections (PR #15)
- README `## Install` pointer to pypinfo's GCP setup (PR #15)

The orphaning was introduced when PR #25 added new `### Added` and
`### Changed` blocks at the top of the then-active `## [Unreleased]`
section without repositioning the entries from earlier PRs. PR #27
later inserted a `### Fixed` block between `### Changed` and the
orphans, which is the layout that shipped in v0.1.1.

Three mechanical edits:

- `pyproject.toml`: version 0.1.1 → 0.1.2
- `CHANGELOG.md`: insert `## [0.1.2] - 2026-04-27` with one `### Fixed`
  bullet describing the recategorization; move three orphan bullets
  from `## [0.1.1]` `### Fixed` to `## [0.1.1]` `### Added`; update
  link refs (Unreleased compares from v0.1.2, new [0.1.2] compares
  v0.1.1...v0.1.2)
- `uv.lock`: refreshed by `uv lock` so locked version (0.1.2) matches
  pyproject.toml

No code, dependency, or runtime behavior changes. Closes #28.

Co-authored-by: cmeans-claude-dev[bot] <272174644+cmeans-claude-dev[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

QA Approved Manual QA testing completed and passed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants