diff --git a/stacks/roms/docker-compose.yml b/stacks/roms/docker-compose.yml index 1064c31..0444bfc 100644 --- a/stacks/roms/docker-compose.yml +++ b/stacks/roms/docker-compose.yml @@ -15,6 +15,10 @@ # The labdata/romm/* targets are auto-created by Docker as root on first start. # # Only romm joins `edge` (Caddy proxies romm:8080); romm-db stays on `roms` only. +# +# To share RomM with an EXTERNAL tailnet user (a friend on his own tailnet), see +# the dedicated `share` stack (stacks/share/), which runs a Tailscale `serve` +# node joined to this `roms` network. services: romm: diff --git a/stacks/share/docker-compose.yml b/stacks/share/docker-compose.yml new file mode 100644 index 0000000..a00a857 --- /dev/null +++ b/stacks/share/docker-compose.yml @@ -0,0 +1,45 @@ +# share stack — Tailscale `serve` nodes that expose individual INTERNAL services +# to EXTERNAL tailnet users (a friend on his own tailnet) over each node's 100.x +# CGNAT identity. No public exposure, no LAN access, and immune to the friend's +# home-subnet addressing (a node IP can never collide with his 192.168.1.0/24). +# +# Each service to share = one userspace Tailscale container that `tailscale serve`s +# straight to that service's backend, so the container joins that service's PRIVATE +# network (roms, foundry, media, ...) — NOT edge/Caddy. +# +# Shared, set-once-for-all plumbing: +# - Auth: ONE reusable, NON-ephemeral, TAGGED (tag:share) auth key in stack.env +# authenticates EVERY node here. Tagged nodes never expire. +# - ACL : ONE rule { src: [friend@…], dst: ["tag:share:443"] } governs them all. +# - Admin (one-time): enable MagicDNS + HTTPS certificates; add +# "tagOwners": { "tag:share": ["autogroup:admin"] }. +# +# To share a NEW service: +# 1. add a `serve-.json` (Proxy -> http://:) +# 2. add a `ts-` service block below (copy ts-roms), join the svc network +# 3. add that network under `networks:` as external +# 4. push, then admin console -> Machines -> -share -> Share -> friend email + +services: + ts-roms: + image: tailscale/tailscale:latest + container_name: ts-roms + hostname: roms-share + restart: unless-stopped + networks: [roms] + env_file: + - stack.env + environment: + - TS_HOSTNAME=roms-share + - TS_STATE_DIR=/var/lib/tailscale + - TS_USERSPACE=true + - TS_SERVE_CONFIG=/config/serve.json + - TS_EXTRA_ARGS=--advertise-tags=tag:share + volumes: + - /config/share/ts-roms:/var/lib/tailscale + - ./serve-roms.json:/config/serve.json:ro + +networks: + roms: + name: roms + external: true diff --git a/stacks/share/serve-roms.json b/stacks/share/serve-roms.json new file mode 100644 index 0000000..90f160d --- /dev/null +++ b/stacks/share/serve-roms.json @@ -0,0 +1,16 @@ +{ + "TCP": { + "443": { + "HTTPS": true + } + }, + "Web": { + "${TS_CERT_DOMAIN}:443": { + "Handlers": { + "/": { + "Proxy": "http://romm:8080" + } + } + } + } +} diff --git a/stacks/share/stack.env b/stacks/share/stack.env new file mode 100644 index 0000000..da31194 --- /dev/null +++ b/stacks/share/stack.env @@ -0,0 +1,13 @@ +# share stack secrets — Tailscale `serve` nodes. +# Read via env_file (no ${VAR} interpolation), so this git stack needs no +# Portainer UI env vars. See memory portainer-env-interpolation. +TZ=America/Chicago + +# ── Tailscale auth (shared by EVERY ts-* node in this stack) ────────────────── +# ONE reusable, NON-ephemeral, TAGGED auth key from: +# https://login.tailscale.com/admin/settings/keys ("Generate auth key") +# - Reusable: ON - Ephemeral: OFF - Tags: tag:share +# Prereq: add "tagOwners": { "tag:share": ["autogroup:admin"] } to your ACLs +# first, or key generation rejects the tag. Tagged nodes never expire, so once +# the nodes have joined this key is no longer needed (state lives in the volumes). +TS_AUTHKEY=tskey-auth-kGQ7kgFizL11CNTRL-HWpbE5TudYTaV2ss4LDmYT7sZ8EddXVH