Skip to content

Latest commit

 

History

History
91 lines (64 loc) · 4.37 KB

File metadata and controls

91 lines (64 loc) · 4.37 KB

Optional: Tailscale-only deployment

This path makes the app reachable only from your own devices on a Tailscale tailnet. The public DNS record resolves to a tailnet IP that is not publicly routable, so the deployment is invisible from the open internet. Auth still applies on top.

This is exactly the setup the original author runs. It requires more moving parts than the public Docker Compose path. Use it if you want a network-level perimeter on top of the auth allowlist.

How it works

  • A Tailscale sidecar (ts-edge) joins your tailnet on container start.
  • Caddy uses network_mode: "service:ts-edge" to share the sidecar's network namespace, so it listens only on tailscale0 (and the internal Docker bridge).
  • No host ports are published. The VPS firewall can block 80/443 entirely.
  • Public DNS for your domain points at the tailnet IPv4 (100.x.y.z) via Cloudflare DNS-only (gray cloud — no proxying). Cloudflare cannot proxy a private address, so this is correct.
  • ACME uses the DNS-01 challenge (because port 80 is unreachable). Caddy talks to the Cloudflare API directly to satisfy the challenge.

Prerequisites

  • Everything in the public Docker Compose guide (VPS, GitHub OAuth app, Voyage/OpenRouter keys, vault repo).
  • A Tailscale account.
  • The VPS kernel exposes /dev/net/tun (most providers do; some VPS-on-LXC hosts don't).
  • Docker Compose v2.24+ (the overlay uses the !reset directive).

Steps

1. Tailscale auth key + ACL

In the Tailscale admin console:

  • Create an ACL tag (e.g. tag:brain) and grant your identity ownership of it.
  • Add an ACL rule allowing autogroup:member (your devices) to reach tag:brain on TCP 80/443:
{
  "tagOwners": { "tag:brain": ["autogroup:member"] },
  "acls": [
    { "action": "accept", "src": ["autogroup:member"], "dst": ["tag:brain:80,443"] }
  ]
}
  • Generate an auth key: Settings → Keys → Generate auth key.
    • Reusable (so the container can re-auth on restart).
    • Non-ephemeral (so the node persists between restarts; state lives in the ts-state Docker volume).
    • Tagged with tag:brain.

Set TS_AUTHKEY=tskey-auth-... in .env.

2. Cloudflare DNS

  • Create an A record at your DOMAIN pointing to the tailnet IPv4 the sidecar will get (you'll know the IP after first boot — see step 4). DNS-only (gray cloud), TTL Auto.
  • Create an API token (Profile → API Tokens → Create Token):
    • Permissions: Zone:Zone:Read + Zone:DNS:Edit.
    • Resources: include only your zone.
    • Set CF_API_TOKEN=... in .env.

3. GitHub OAuth

Create a GitHub OAuth app whose callback URL is https://<your-domain>/api/auth/callback/github (same as the public path).

4. Bring up the stack with the overlay

docker compose -f docker-compose.yml -f docker-compose.tailscale.yml up -d --build
docker compose logs -f ts-edge
# Note the tailnet IPv4 the sidecar advertises: e.g. 100.64.0.42.
# Update the Cloudflare A record (step 2) to that IP.
docker compose logs -f caddy
# Expect "certificate obtained successfully" via DNS-01.

5. Verify

  • From a tailnet device: curl -I https://<your-domain> returns 200/302 with a Let's Encrypt cert.
  • From a non-tailnet network (cellular with Tailscale off): curl --max-time 5 https://<your-domain> times out.
  • Run the pen-test checklist including the Tailnet-only exposure section.

Operating

To deploy code updates with the overlay:

docker compose -f docker-compose.yml -f docker-compose.tailscale.yml pull
docker compose -f docker-compose.yml -f docker-compose.tailscale.yml up -d --build

To return to the public path: bring the stack down and run plain docker compose up -d against the default docker-compose.yml. You'll need to adjust your DNS record (point at the public VPS IP) and let Caddy re-issue via HTTP-01.

Caveats

  • The sidecar approach means every container restart re-authenticates with the tailnet. The persistent ts-state volume is what keeps the node identity stable across restarts. Don't delete that volume casually.
  • DNS-01 needs the API token to remain valid. Rotate the Cloudflare token on your normal cadence.
  • Cloudflare proxying (orange cloud) will not work — it cannot proxy a tailnet address. Keep the record DNS-only.