From a8d8ee86b018d155dc4856b7ad4a71a513953125 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 22:57:50 +0000 Subject: [PATCH 1/4] Initial plan From f9ec45c0d7b8c0f12f8171c474e24867a82c3bfe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 23:12:27 +0000 Subject: [PATCH 2/4] feat: add Tailscale remote-access profile for smartphone connectivity Co-authored-by: devartifex <21122751+devartifex@users.noreply.github.com> Agent-Logs-Url: https://github.com/devartifex/copilot-unleashed/sessions/dc6a632d-bfa9-4b34-9556-adce10db7627 --- .env.example | 18 +++ Caddyfile | 52 ++++++ docker-compose.remote.yml | 20 +++ docker-compose.yml | 51 ++++++ docs/REMOTE-ACCESS.md | 272 ++++++++++++++++++++++++++++++++ scripts/tailscale-entrypoint.sh | 53 +++++++ server.js | 9 ++ 7 files changed, 475 insertions(+) create mode 100644 Caddyfile create mode 100644 docker-compose.remote.yml create mode 100644 docs/REMOTE-ACCESS.md create mode 100755 scripts/tailscale-entrypoint.sh diff --git a/.env.example b/.env.example index dbc18f9..401a93a 100644 --- a/.env.example +++ b/.env.example @@ -23,3 +23,21 @@ VAPID_SUBJECT=mailto:admin@example.com # Override where chat history and push subscriptions are stored. # CHAT_STATE_PATH=.chat-state # PUSH_STORE_PATH=.push-subscriptions + +# === Remote Access via Tailscale (optional) === +# Required only when running: docker compose -f docker-compose.yml -f docker-compose.remote.yml --profile remote up +# See docs/REMOTE-ACCESS.md for the full setup guide. +# +# TS_AUTHKEY: Create a reusable auth key at https://login.tailscale.com/admin/settings/keys +TS_AUTHKEY= +# +# TS_HOSTNAME: The Tailscale device name (default: copilot-unleashed) +TS_HOSTNAME=copilot-unleashed +# +# TS_DOMAIN: Full Tailscale FQDN — find yours at https://login.tailscale.com/admin/machines +# after the first start. Example: copilot-unleashed.tail1234.ts.net +TS_DOMAIN= +# +# BASE_URL: Must match your full Tailscale HTTPS URL when using the remote profile. +# Example: BASE_URL=https://copilot-unleashed.tail1234.ts.net +# BASE_URL=https://copilot-unleashed.your-tailnet.ts.net diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..3a2552f --- /dev/null +++ b/Caddyfile @@ -0,0 +1,52 @@ +# Caddyfile — optional advanced Caddy reverse proxy for Copilot Unleashed +# +# The default remote-access setup uses Tailscale Serve for TLS termination +# (see docs/REMOTE-ACCESS.md). This file is provided for advanced users who +# want Caddy as an additional HTTP-layer proxy (e.g. custom headers, auth, +# path rewrites). +# +# Advanced usage with Caddy: +# 1. Set TS_DOMAIN to your full Tailscale FQDN in .env +# 2. Provision a TLS cert inside the Tailscale container: +# docker compose --profile remote exec tailscale \ +# tailscale cert \ +# --cert-file=/certs/server.crt \ +# --key-file=/certs/server.key \ +# "$TS_DOMAIN" +# 3. Mount /certs and this Caddyfile into a caddy:2-alpine service and +# adjust the domain/cert paths below to match your TS_DOMAIN. +# +# For most users the default Tailscale Serve setup is sufficient. + +{ + # Admin API on localhost only (used for health checks) + admin 0.0.0.0:2019 + # TLS managed externally via Tailscale-provisioned certs + auto_https off +} + +# Replace copilot-unleashed.tail1234.ts.net with your actual TS_DOMAIN value. +https://copilot-unleashed.tail1234.ts.net { + # Use the TLS certificate provisioned by Tailscale. + # Run the tailscale cert command (see above) before starting Caddy. + tls /certs/server.crt /certs/server.key + + # Forward real client IP for rate-limiting and logging. + header_up X-Forwarded-For {remote_host} + header_up X-Forwarded-Proto {scheme} + + # WebSocket upgrade for /ws + @ws { + path /ws + header Connection *Upgrade* + header Upgrade websocket + } + reverse_proxy @ws app:3000 { + header_up Host {host} + } + + # All other traffic proxied to the Node.js app. + reverse_proxy app:3000 { + header_up Host {host} + } +} diff --git a/docker-compose.remote.yml b/docker-compose.remote.yml new file mode 100644 index 0000000..da439c9 --- /dev/null +++ b/docker-compose.remote.yml @@ -0,0 +1,20 @@ +# Remote-access overrides for the app service. +# +# Apply together with docker-compose.yml when running the remote profile: +# +# docker compose -f docker-compose.yml -f docker-compose.remote.yml \ +# --profile remote up -d --build +# +# Switches the app from development mode to production mode, and enables +# reverse-proxy trust so the X-Forwarded-For / X-Forwarded-Proto headers +# sent by Tailscale Serve are honoured for rate-limiting and secure cookies. + +services: + app: + environment: + - NODE_ENV=production + # BASE_URL must be the full HTTPS URL of your Tailscale hostname. + # Set this in your .env file — compose reads it from there. + # Example: BASE_URL=https://copilot-unleashed.tail1234.ts.net + - BASE_URL=${BASE_URL:-https://copilot-unleashed.example.ts.net} + - TRUST_PROXY=1 diff --git a/docker-compose.yml b/docker-compose.yml index 3c599e8..ccac95f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,5 +27,56 @@ services: - "991" restart: unless-stopped + # ────────────────────────────────────────────────────────────────────────── + # Remote-access profile: Tailscale sidecar + # + # Enables secure smartphone access from anywhere without port-forwarding. + # Tailscale handles WireGuard encryption, TLS termination, and Let's Encrypt + # certificate provisioning automatically — no extra proxy container needed. + # + # Quick start: + # docker compose -f docker-compose.yml -f docker-compose.remote.yml \ + # --profile remote up -d --build + # + # Prerequisites (one-time setup): + # 1. Create a reusable Tailscale auth key: + # https://login.tailscale.com/admin/settings/keys + # 2. Enable MagicDNS + HTTPS certificates in your tailnet: + # https://login.tailscale.com/admin/dns + # 3. Set TS_AUTHKEY, TS_HOSTNAME, and TS_DOMAIN in your .env file + # 4. Set BASE_URL=https:// in your .env file + # + # See docs/REMOTE-ACCESS.md for the full step-by-step guide. + # ────────────────────────────────────────────────────────────────────────── + + tailscale: + image: tailscale/tailscale:latest + profiles: [remote] + hostname: ${TS_HOSTNAME:-copilot-unleashed} + entrypoint: ["/bin/sh", "/entrypoint.sh"] + environment: + - TS_AUTHKEY=${TS_AUTHKEY:?Set TS_AUTHKEY in .env (see docs/REMOTE-ACCESS.md)} + - TS_HOSTNAME=${TS_HOSTNAME:-copilot-unleashed} + # Full Tailscale FQDN for this device, e.g. copilot-unleashed.tail1234.ts.net + # Find yours at https://login.tailscale.com/admin/machines after first start + # Leave empty on first start (bootstrap); set before enabling HTTPS serving + - TS_DOMAIN=${TS_DOMAIN:-} + - TS_STATE_DIR=/var/lib/tailscale + - TS_USERSPACE=false + volumes: + - tailscale-data:/var/lib/tailscale + - ./scripts/tailscale-entrypoint.sh:/entrypoint.sh:ro + - /dev/net/tun:/dev/net/tun + cap_add: + - NET_ADMIN + restart: unless-stopped + healthcheck: + test: ["CMD", "tailscale", "status"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 30s + volumes: copilot-data: + tailscale-data: diff --git a/docs/REMOTE-ACCESS.md b/docs/REMOTE-ACCESS.md new file mode 100644 index 0000000..66ba1f1 --- /dev/null +++ b/docs/REMOTE-ACCESS.md @@ -0,0 +1,272 @@ +# Remote Access — Smartphone to Local Container + +> **TL;DR** — Complete the one-time Tailscale setup below, then run: +> ```bash +> docker compose -f docker-compose.yml -f docker-compose.remote.yml --profile remote up -d --build +> ``` + +This guide explains how to securely access your locally-running **Copilot Unleashed** +container from a smartphone (or any device) **anywhere in the world** — without opening +firewall ports, without a static IP, and without Azure. + +The approach uses **Tailscale** (a WireGuard-based VPN mesh) with its built-in +**HTTPS/TLS serving** capability. Tailscale handles WireGuard encryption, TLS +termination, and Let's Encrypt certificate provisioning automatically. + +--- + +## Architecture + +``` +Smartphone ──── WireGuard (Tailscale) ────► Tailscale sidecar + │ Tailscale Serve + │ TLS :443 (auto Let's Encrypt cert) + │ + App container (:3000, HTTP) +``` + +**Why this is safe:** + +| Layer | Protection | +|-------|------------| +| Transport | WireGuard (ChaCha20-Poly1305) end-to-end encryption | +| TLS | Let's Encrypt certificate provisioned automatically by Tailscale | +| ACLs | Only devices you approve join your tailnet | +| App | Full production security (CSRF, secure cookies, HSTS, CSP) | +| Cookies | `Secure` flag enforced because `NODE_ENV=production` | + +--- + +## One-Time Setup + +### 1. Create a Tailscale account and tailnet + +1. Go to and sign up (free for personal use). +2. Install the Tailscale app on your **smartphone** + ([iOS](https://apps.apple.com/app/tailscale/id1470499037) / + [Android](https://play.google.com/store/apps/details?id=com.tailscale.ipn.android)). +3. Log in on your phone — it will join your tailnet automatically. + +### 2. Enable MagicDNS and HTTPS certificates + +In the Tailscale admin console (): + +- **Enable MagicDNS** — gives every device a stable hostname like + `copilot-unleashed.your-tailnet.ts.net` +- **Enable HTTPS** — Tailscale provisions a free Let's Encrypt certificate for each device + +### 3. Create a Tailscale auth key + +Go to and create a new key: + +- **Reusable**: ✅ (so the container can re-authenticate after restarts) +- **Ephemeral**: ❌ (we want the device to persist) +- **Expiry**: Set to a long duration or disable expiry for a home server + +Copy the key — it starts with `tskey-auth-…`. + +### 4. Add initial `.env` entries + +Add the following to your `.env` file (copy from `.env.example`): + +```dotenv +# Tailscale auth key from step 3 +TS_AUTHKEY=tskey-auth-XXXXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + +# Device hostname (becomes ..ts.net after registration) +TS_HOSTNAME=copilot-unleashed + +# Leave TS_DOMAIN blank for now — you'll fill it in after the first start (step 6) +TS_DOMAIN= +``` + +### 5. First start — register the device + +Run the remote stack to register the Tailscale device and discover your full hostname: + +```bash +docker compose -f docker-compose.yml -f docker-compose.remote.yml \ + --profile remote up -d --build +``` + +Wait ~15 seconds, then check the Tailscale admin console: + + +You will see a new device named `copilot-unleashed` with a full hostname like: +`copilot-unleashed.tail1234.ts.net` + +> **Note:** The `tailnet` part (e.g., `tail1234`) is unique to your account. +> The exact FQDN is shown in the **Machines** column of the admin console. + +### 6. Set `TS_DOMAIN` and `BASE_URL` + +Now that you know the full hostname, update your `.env`: + +```dotenv +# Full Tailscale FQDN from the admin machines page +TS_DOMAIN=copilot-unleashed.tail1234.ts.net + +# Full HTTPS URL — same as TS_DOMAIN with https:// prefix +BASE_URL=https://copilot-unleashed.tail1234.ts.net +``` + +### 7. Restart to apply TLS serving + +```bash +docker compose -f docker-compose.yml -f docker-compose.remote.yml \ + --profile remote up -d --force-recreate +``` + +Tailscale will now provision a Let's Encrypt TLS certificate and start serving HTTPS. +After ~30 seconds, your app is reachable at `https://copilot-unleashed.tail1234.ts.net`. + +### 8. Generate VAPID keys (for push notifications — optional but recommended) + +Push notifications on mobile require HTTPS, which Tailscale now provides. Generate keys: + +```bash +node scripts/generate-vapid-keys.mjs +``` + +Copy the output into your `.env` file: + +```dotenv +VAPID_PUBLIC_KEY=Bxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +VAPID_PRIVATE_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +VAPID_SUBJECT=mailto:your@email.com +``` + +Restart after updating VAPID keys: + +```bash +docker compose -f docker-compose.yml -f docker-compose.remote.yml \ + --profile remote up -d --force-recreate +``` + +--- + +## Access from Your Smartphone + +1. Make sure the **Tailscale app** is running on your phone +2. Open **Safari** (iOS) or **Chrome** (Android) and navigate to + `https://copilot-unleashed.tail1234.ts.net` (your actual TS_DOMAIN) +3. You should see the Copilot Unleashed login screen with a valid HTTPS padlock + +### Install as a PWA + +Because you now have HTTPS, the app qualifies as a Progressive Web App: + +**iOS (Safari):** +- Tap the **Share** button → **Add to Home Screen** +- The app opens full-screen, just like a native app + +**Android (Chrome):** +- Tap the **menu (⋮)** → **Add to Home screen** (or the install banner may appear automatically) + +### Enable Push Notifications + +After installing as a PWA: +1. Open the app → **Settings** → enable **Push Notifications** +2. Accept the permission prompt +3. You'll receive notifications when the AI responds while the app is in the background + +--- + +## Stop the Remote Stack + +```bash +docker compose -f docker-compose.yml -f docker-compose.remote.yml \ + --profile remote down +``` + +To also remove volumes (including the Tailscale machine registration): + +```bash +docker compose -f docker-compose.yml -f docker-compose.remote.yml \ + --profile remote down -v +``` + +--- + +## Restrict Access to Specific Devices + +By default all devices on your tailnet can reach the app. To lock it down further: + +1. Go to +2. Use the ACL editor to allow only specific users or device tags: + +```json +{ + "acls": [ + { + "action": "accept", + "src": ["your@email.com"], + "dst": ["tag:server:443"] + } + ] +} +``` + +You can also enable **user approval** to require manual approval before any new device joins. + +--- + +## Restrict Login to Specific GitHub Users + +In addition to Tailscale ACLs, the app has its own application-level allowlist: + +```dotenv +# Only these GitHub usernames can log in (comma-separated) +ALLOWED_GITHUB_USERS=yourusername,partnerusername +``` + +--- + +## Troubleshooting + +### Tailscale container doesn't authenticate + +- Check that `TS_AUTHKEY` is set correctly in `.env` +- Verify the key hasn't expired: +- Check logs: `docker compose --profile remote logs tailscale` + +### App returns 403 Forbidden (CSRF) + +- Verify `BASE_URL` in `.env` matches the exact HTTPS URL of your Tailscale hostname + (must include `https://` scheme and no trailing slash) +- Ensure `docker-compose.remote.yml` is included in the compose command +- Confirm `NODE_ENV=production` is active (set by `docker-compose.remote.yml`) + +### HTTPS cert not provisioning / connection fails + +- Ensure **HTTPS certificates** are enabled in the Tailscale admin DNS settings +- `TS_DOMAIN` must be set and must exactly match the device's FQDN (visible in the + Tailscale admin machines page) +- Check logs: `docker compose --profile remote logs tailscale` +- The entrypoint script (`scripts/tailscale-entrypoint.sh`) will print the domain + it's configuring — verify it matches your expected FQDN +- Certs typically take up to 60 seconds to provision on first start + +### WebSocket connection fails + +- Confirm `TRUST_PROXY=1` is active (it's set automatically in `docker-compose.remote.yml`) +- Tailscale Serve passes WebSocket connections through transparently +- Check the app container logs: `docker compose --profile remote logs app` + +### Local development still works? + +Yes — the default `docker compose up` (without the remote override or `--profile remote`) +starts only the `app` service in development mode (`NODE_ENV=development`) with HTTP on +port 3000. The Tailscale service is **only** started with `--profile remote`. + +--- + +## Security Notes + +- **Tailscale auth keys**: Treat `TS_AUTHKEY` like a password. Rotate it periodically. +- **No open firewall ports**: Tailscale Serve listens only on the Tailscale network + interface (not on a public host port), so it is unreachable from the public internet. +- **TRUST_PROXY=1**: Trusts only the first proxy hop (Tailscale Serve). If you add + additional proxies, set `XFF_DEPTH` accordingly. +- **Production cookies**: The `Secure` cookie flag is enforced in production mode, so + session cookies are only sent over HTTPS. diff --git a/scripts/tailscale-entrypoint.sh b/scripts/tailscale-entrypoint.sh new file mode 100755 index 0000000..fa9f12c --- /dev/null +++ b/scripts/tailscale-entrypoint.sh @@ -0,0 +1,53 @@ +#!/bin/sh +# Tailscale container entrypoint for the remote-access profile. +# +# When TS_DOMAIN is set: generates a tailscale serve config that routes all +# HTTPS traffic on port 443 to the app container at http://app:3000. +# Tailscale automatically provisions a Let's Encrypt TLS certificate for the +# served domain — no manual cert management required. +# +# When TS_DOMAIN is empty: starts Tailscale in plain VPN mode (no HTTPS +# serving). This is used on the first-start bootstrap run so the device can +# register and you can discover its FQDN from the Tailscale admin console. +# +# TS_DOMAIN must be the full Tailscale FQDN for this device, e.g.: +# copilot-unleashed.tail1234.ts.net +# It is set in your .env file and passed through docker-compose.yml. +# See docs/REMOTE-ACCESS.md for the step-by-step setup guide. + +set -e + +if [ -z "$TS_DOMAIN" ]; then + echo "[tailscale-entrypoint] TS_DOMAIN is not set." + echo " Starting in bootstrap mode (VPN only, no HTTPS serving)." + echo " After this container starts, find the full FQDN at:" + echo " https://login.tailscale.com/admin/machines" + echo " Then set TS_DOMAIN and BASE_URL in .env and restart." + exec /usr/local/bin/containerboot +fi + +# Generate the tailscale serve config that proxies HTTPS → http://app:3000. +# The hostname key must exactly match the device's Tailscale FQDN (TS_DOMAIN). +printf '{ + "TCP": { + "443": { + "HTTPS": true + } + }, + "Web": { + "%s:443": { + "Handlers": { + "/": { + "Proxy": "http://app:3000" + } + } + } + } +}\n' "$TS_DOMAIN" > /tmp/serve.json + +export TS_SERVE_CONFIG=/tmp/serve.json + +echo "[tailscale-entrypoint] Serve config written for domain: $TS_DOMAIN" + +# Replace this shell with containerboot (becomes PID 1, handles signals correctly) +exec /usr/local/bin/containerboot \ No newline at end of file diff --git a/server.js b/server.js index 312e92b..fbcb3b6 100644 --- a/server.js +++ b/server.js @@ -16,6 +16,15 @@ const tokenMaxAge = parseInt(process.env.TOKEN_MAX_AGE_MS || String(7 * 24 * 60 if (!process.env.ORIGIN) { process.env.ORIGIN = process.env.BASE_URL || `http://localhost:${port}`; } + +// Trust reverse-proxy headers (X-Forwarded-For, X-Forwarded-Proto) when TRUST_PROXY=1. +// Required when Tailscale Serve (or any reverse proxy) sits in front of the app. +// Set automatically by docker-compose.remote.yml for the remote-access profile. +if (process.env.TRUST_PROXY === '1') { + process.env.ADDRESS_HEADER = 'X-Forwarded-For'; + process.env.XFF_DEPTH = process.env.XFF_DEPTH || '1'; +} + const { handler } = await import('./build/handler.js'); if (!isDev && !process.env.SESSION_SECRET) { From 0d276cae1e34d110b7a6de810823fe4b680414ec Mon Sep 17 00:00:00 2001 From: devartifex Date: Thu, 26 Mar 2026 02:02:40 +0100 Subject: [PATCH 3/4] fix: resolve security issues found in PR review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docker-compose.remote.yml: restrict port binding to 127.0.0.1 to prevent LAN devices from spoofing X-Forwarded-For and bypassing rate limiting when TRUST_PROXY=1 is active - Caddyfile: change admin bind from 0.0.0.0:2019 to localhost:2019 to prevent exposing Caddy's runtime config API to the network - Caddyfile: move header_up directives inside reverse_proxy blocks; they are subdirectives of reverse_proxy in Caddy v2, not site-level directives — the previous placement caused a parse error on startup Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Caddyfile | 9 +++++---- docker-compose.remote.yml | 5 +++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Caddyfile b/Caddyfile index 3a2552f..7888b82 100644 --- a/Caddyfile +++ b/Caddyfile @@ -20,7 +20,7 @@ { # Admin API on localhost only (used for health checks) - admin 0.0.0.0:2019 + admin localhost:2019 # TLS managed externally via Tailscale-provisioned certs auto_https off } @@ -32,9 +32,6 @@ https://copilot-unleashed.tail1234.ts.net { tls /certs/server.crt /certs/server.key # Forward real client IP for rate-limiting and logging. - header_up X-Forwarded-For {remote_host} - header_up X-Forwarded-Proto {scheme} - # WebSocket upgrade for /ws @ws { path /ws @@ -43,10 +40,14 @@ https://copilot-unleashed.tail1234.ts.net { } reverse_proxy @ws app:3000 { header_up Host {host} + header_up X-Forwarded-For {remote_host} + header_up X-Forwarded-Proto {scheme} } # All other traffic proxied to the Node.js app. reverse_proxy app:3000 { header_up Host {host} + header_up X-Forwarded-For {remote_host} + header_up X-Forwarded-Proto {scheme} } } diff --git a/docker-compose.remote.yml b/docker-compose.remote.yml index da439c9..c44dd3f 100644 --- a/docker-compose.remote.yml +++ b/docker-compose.remote.yml @@ -11,6 +11,11 @@ services: app: + # Restrict the host port to localhost so external devices cannot reach the + # app directly and spoof X-Forwarded-For to bypass rate limiting. + # All external traffic must go through the Tailscale sidecar. + ports: + - "127.0.0.1:3000:3000" environment: - NODE_ENV=production # BASE_URL must be the full HTTPS URL of your Tailscale hostname. From 3824e3be2c3a7b09fe2dc11aef63aa82e94b86e9 Mon Sep 17 00:00:00 2001 From: devartifex Date: Thu, 26 Mar 2026 02:02:48 +0100 Subject: [PATCH 4/4] remove: delete unused Caddyfile reference config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The remote-access profile uses Tailscale Serve for TLS termination — no Caddy container is wired into any compose file. The Caddyfile was documented as optional but contained syntax errors (header_up at site scope, admin on 0.0.0.0) and added confusion without adding value. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Caddyfile | 53 ----------------------------------------------------- 1 file changed, 53 deletions(-) delete mode 100644 Caddyfile diff --git a/Caddyfile b/Caddyfile deleted file mode 100644 index 7888b82..0000000 --- a/Caddyfile +++ /dev/null @@ -1,53 +0,0 @@ -# Caddyfile — optional advanced Caddy reverse proxy for Copilot Unleashed -# -# The default remote-access setup uses Tailscale Serve for TLS termination -# (see docs/REMOTE-ACCESS.md). This file is provided for advanced users who -# want Caddy as an additional HTTP-layer proxy (e.g. custom headers, auth, -# path rewrites). -# -# Advanced usage with Caddy: -# 1. Set TS_DOMAIN to your full Tailscale FQDN in .env -# 2. Provision a TLS cert inside the Tailscale container: -# docker compose --profile remote exec tailscale \ -# tailscale cert \ -# --cert-file=/certs/server.crt \ -# --key-file=/certs/server.key \ -# "$TS_DOMAIN" -# 3. Mount /certs and this Caddyfile into a caddy:2-alpine service and -# adjust the domain/cert paths below to match your TS_DOMAIN. -# -# For most users the default Tailscale Serve setup is sufficient. - -{ - # Admin API on localhost only (used for health checks) - admin localhost:2019 - # TLS managed externally via Tailscale-provisioned certs - auto_https off -} - -# Replace copilot-unleashed.tail1234.ts.net with your actual TS_DOMAIN value. -https://copilot-unleashed.tail1234.ts.net { - # Use the TLS certificate provisioned by Tailscale. - # Run the tailscale cert command (see above) before starting Caddy. - tls /certs/server.crt /certs/server.key - - # Forward real client IP for rate-limiting and logging. - # WebSocket upgrade for /ws - @ws { - path /ws - header Connection *Upgrade* - header Upgrade websocket - } - reverse_proxy @ws app:3000 { - header_up Host {host} - header_up X-Forwarded-For {remote_host} - header_up X-Forwarded-Proto {scheme} - } - - # All other traffic proxied to the Node.js app. - reverse_proxy app:3000 { - header_up Host {host} - header_up X-Forwarded-For {remote_host} - header_up X-Forwarded-Proto {scheme} - } -}