From 7ddf1d1dd3a9186904099928c9a8c5524fd0c499 Mon Sep 17 00:00:00 2001 From: Hachiwa0 <140583188+Hachiwa0@users.noreply.github.com> Date: Wed, 26 Aug 2026 23:51:45 +0800 Subject: [PATCH 1/2] feat(deploy): add Docker relay container, nginx alternative, and CN mirror docs --- .dockerignore | 12 ++++ CHANGELOG.md | 9 +++ CHANGELOG_zh.md | 6 ++ README.md | 17 ++++++ README_en.md | 20 +++++++ deploy/Dockerfile | 75 +++++++++++++++++++++++++ deploy/README.md | 44 +++++++++++++++ deploy/docker-compose.yml | 40 +++++++++++++ deploy/env.relay.docker.example | 36 ++++++++++++ deploy/nginx-reverse-proxy.conf.example | 59 +++++++++++++++++++ 10 files changed, 318 insertions(+) create mode 100644 .dockerignore create mode 100644 deploy/Dockerfile create mode 100644 deploy/docker-compose.yml create mode 100644 deploy/env.relay.docker.example create mode 100644 deploy/nginx-reverse-proxy.conf.example diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..826c954 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +# Keep the relay Docker build context lean and free of machine-local state. +# web/dist is built inside the image (stage 1); web/node_modules would bloat +# the context and is never committed upstream. +.git/ +.venv/ +__pycache__/ +*.pyc +.pytest_cache/ +web/node_modules/ +web/dist/ +web/test-results/ +*.log diff --git a/CHANGELOG.md b/CHANGELOG.md index c0d8e6a..7f7bde5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ ## Unreleased +- Add an official container deploy for the Relay: a multi-stage + `deploy/Dockerfile` builds `web/dist` from source and installs the + hash-locked wheels as a non-root `ccremote` user, with `docker-compose.yml`, + a Docker-oriented `env.relay.docker.example`, and a compose layout that only + publishes the relay to the host loopback so an existing nginx can keep TLS + and WebSocket termination. `deploy/nginx-reverse-proxy.conf.example` + documents that alternative front, and the Docker build exposes + `PIP_INDEX_URL` / `PIP_EXTRA_INDEX_URL` build args plus documented + mainland-China mirror guidance. - Upgrade the coordinated Wrapper/Relay/Web gate to protocol v35. Exact Codex app-server and source-validated rollout terminals now travel independently of the narrative History projection, so a multi-hundred-MiB rollout cannot keep diff --git a/CHANGELOG_zh.md b/CHANGELOG_zh.md index d362541..2cd7c7a 100644 --- a/CHANGELOG_zh.md +++ b/CHANGELOG_zh.md @@ -4,6 +4,12 @@ ## 未发布 +- 新增 Relay 的官方容器化部署:`deploy/Dockerfile` 分阶段构建——Node 阶段从源码 + 编译 `web/dist`,Python 阶段以非 root 的 `ccremote` 用户按哈希锁安装依赖;附 + `docker-compose.yml` 与 Docker 版 `env.relay.docker.example`。compose 只把端口 + 发布到宿主机 loopback,TLS/WebSocket 终止仍交给现有 nginx; + `deploy/nginx-reverse-proxy.conf.example` 记录该反代前端。Docker 构建支持 + `PIP_INDEX_URL` / `PIP_EXTRA_INDEX_URL` 构建参数,并补充大陆镜像加速指引。 - Wrapper、Relay 与 Web 的协同 gate 升级到 protocol v35。Codex app-server 的 精确终态与通过源文件校验的 rollout 终态现在独立于 History 正文投影下发;数百 MiB 的 rollout 即使仍在补建内容索引,也不会让已经完成的回合继续转圈。终态事实 diff --git a/README.md b/README.md index 7cb095e..14daaa7 100644 --- a/README.md +++ b/README.md @@ -427,6 +427,23 @@ Wrapper 已有设备凭据时只需: 协议大版本升级仍应在同一维护窗口完成 Relay、Web 和所有 Wrapper;已经打开的页面要 硬刷新。安装器保留上一 release,服务验活失败会把 `current` 和服务定义恢复到旧版。 +### 中国大陆网络加速(可选) + +`install.sh` 从 GitHub Release 下载角色包、内置的 `uv` 再下载 Python 运行时, +两者在国内默认路径都可能很慢。可用镜像加速: + +```bash +# 1) 加速 uv 安装 PyPI 依赖(阿里云 PyPI 镜像) +export UV_DEFAULT_INDEX=https://mirrors.aliyun.com/pypi/simple +# 2) 加速 uv 下载 python-build-standalone 运行时(npmmirror) +export UV_PYTHON_INSTALL_MIRROR=https://registry.npmmirror.com/-/binary/python-build-standalone +./install.sh wrapper --relay https://remote.example.com --pair XXXXX-XXXXX-XXXXX-XXXXX --name "MacBook Pro" +``` + +若 `install.sh` 本身下载角色包超时,可先在别处下好 release 包再重试。Relay 用 +Docker 容器部署时,构建阶段同样可用 `--build-arg PIP_INDEX_URL=...` 加速 +(见 deploy/README.md 的容器章节)。 + ## 生产部署(公网 VPS 中继 + 你机器上的 wrapper) 以下保留源码 staging / 手工配置路径,适合开发、自定义部署和故障恢复。普通正式安装 diff --git a/README_en.md b/README_en.md index 51373ad..29398d1 100644 --- a/README_en.md +++ b/README_en.md @@ -505,6 +505,26 @@ window, then hard-refresh open browser tabs. The installers retain the previous release and restore both `current` and the service definition if activation does not become healthy. +### Faster installs behind the GFW (optional) + +`install.sh` downloads the role bundle from the GitHub Release and the bundled +`uv` then downloads the Python runtime — both default paths can be slow from +mainland China. Point uv at mirrors to speed it up: + +```bash +# 1) Faster PyPI dependencies (Aliyun PyPI mirror) +export UV_DEFAULT_INDEX=https://mirrors.aliyun.com/pypi/simple +# 2) Faster python-build-standalone runtime downloads (npmmirror) +export UV_PYTHON_INSTALL_MIRROR=https://registry.npmmirror.com/-/binary/python-build-standalone +./install.sh wrapper --relay https://remote.example.com --pair XXXXX-XXXXX-XXXXX-XXXXX --name "MacBook Pro" +``` + +If `install.sh` itself times out fetching the role bundle, pre-download the +release archive elsewhere and retry. When deploying the Relay in a Docker +container, the build stage can use the same mirror idea via +`--build-arg PIP_INDEX_URL=...` (see the container section of +[deploy/README.md](deploy/README.md)). + ## Production deploy (public VPS relay + wrapper on your machine) The source-staging/manual path below remains available for development, custom diff --git a/deploy/Dockerfile b/deploy/Dockerfile new file mode 100644 index 0000000..27f1d2b --- /dev/null +++ b/deploy/Dockerfile @@ -0,0 +1,75 @@ +# Multi-stage image for the cc-remote Relay. +# +# The official deploy path is a systemd venv staged by setup-vps.sh in front of +# a managed Caddy (see deploy/README.md and install-relay.sh). This container +# runs the exact same `python -m cc_remote.relay` release in an isolated root +# filesystem — for VPSes that already manage services with Docker, or where the +# relay should be pinned to its own writable state volume. +# +# The repository does not track web/dist, so the first stage builds the web +# client from source (npm run build) and the second stage only carries the +# built artifact. Python wheels are installed with the same hash-locked, +# binary-only rules the official installer uses; http-ece ships only an sdist +# on PyPI, so it is exempted with --no-binary exactly as setup-vps.sh does. +# +# Mirrors: PyPI.org by default. In mainland China build with +# docker build -f deploy/Dockerfile \ +# --build-arg PIP_INDEX_URL=https://mirrors.aliyun.com/pypi/simple \ +# --build-arg PIP_EXTRA_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple \ +# -t cc-remote-relay . + +FROM node:24-alpine AS web-build +WORKDIR /src/web +COPY web/package.json web/package-lock.json ./ +RUN npm ci +COPY web/ ./ +RUN npm run build + +FROM python:3.13-slim-bookworm AS relay + +ENV PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 \ + RELAY_HOST=0.0.0.0 \ + RELAY_PORT=8765 \ + WEB_STATIC_DIR=/app/web/dist \ + PUSH_DB_PATH=/app/state/relay-push.sqlite3 \ + DEVICE_DB_PATH=/app/state/relay-devices.sqlite3 \ + SESSION_TTL_SECONDS=604800 \ + LOG_LEVEL=INFO + +WORKDIR /app + +# Relay application code, deploy reference files, and the hash-locked +# dependency list (the installed wheels are the ones setup-vps.sh pins). +COPY cc_remote/ ./cc_remote/ +COPY deploy/ ./deploy/ +COPY requirements-relay.lock requirements-relay.lock + +# The systemd unit runs the relay as a dedicated `ccremote` user +# (deploy/cc-remote-relay.service); mirror that inside the container. State is +# written to /app/state — mount it as a volume (see docker-compose.yml). +RUN groupadd --gid 10001 ccremote \ + && useradd --uid 10001 --gid ccremote --home-dir /app \ + --shell /usr/sbin/nologin ccremote \ + && mkdir -p /app/state \ + && chown -R ccremote:ccremote /app/state + +ARG PIP_INDEX_URL=https://pypi.org/simple +ARG PIP_EXTRA_INDEX_URL= +RUN pip install --index-url "$PIP_INDEX_URL" \ + ${PIP_EXTRA_INDEX_URL:+--extra-index-url "$PIP_EXTRA_INDEX_URL"} \ + --require-hashes --only-binary=:all: --no-binary=http-ece \ + -r requirements-relay.lock + +# Built web client from the first stage. +COPY --from=web-build --chown=ccremote:ccremote /src/web/dist ./web/dist/ + +EXPOSE 8765 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8765/healthz', timeout=4)" || exit 1 + +USER ccremote + +CMD ["python", "-m", "cc_remote.relay"] diff --git a/deploy/README.md b/deploy/README.md index 59f5153..8e724de 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -59,6 +59,13 @@ machine). The **full step-by-step guide is in the main [README](../README.md#生 - `com.muggle.cc-remote.wrapper.plist.in` — secret-free macOS LaunchAgent template. The runtime reads the current user's mode-0600 device JSON instead of embedding control credentials in the plist. +- `Dockerfile` / `docker-compose.yml` / `env.relay.docker.example` — the same + relay release as a container (build `web/dist` in a Node stage, install the + hash-locked wheels, run as the `ccremote` user). See the container section + below; this is an alternative to the systemd + Caddy path, not a fork of it. +- `nginx-reverse-proxy.conf.example` — a WebSocket reverse-proxy front for + hosts that already run nginx instead of the managed Caddy. Loopback-only + requirement is documented in the file header. - `work_registry_snapshot.py` — snapshots provider-local Work SQLite databases through SQLite's backup API, restores the matching pre-release images before an older wrapper is restarted, and verifies the v34 Codex ownership backfill. @@ -87,6 +94,43 @@ created after activation will no longer be registered (their private files are not deleted). Use this for immediate failed activation; after normal use, prefer a roll-forward fix unless that metadata rollback is explicitly accepted. +## Container deploy (Docker) and the nginx alternative + +The official relay install is a systemd venv staged by `setup-vps.sh` behind a +managed Caddy. Two alternative topologies are supported for hosts that already +manage their own services or TLS: + +**Docker container.** `Dockerfile` builds the same relay release as a +multi-stage image: the Node stage compiles `web/dist` from source, the Python +stage installs the same hash-locked wheels `setup-vps.sh` pins and runs +`python -m cc_remote.relay` as a non-root `ccremote` user. From the `deploy/` +directory: + +```bash +cp env.relay.docker.example env.relay # then fill in the secrets +docker compose up -d --build +curl https://your-domain/healthz # -> {"ok":true,...} +``` + +The compose file publishes the relay only to the host loopback +(`127.0.0.1:8765`) and mounts a named volume for the SQLite device/Web Push +state. Public TLS + WebSocket termination stays with your existing front. + +**nginx instead of Caddy.** `nginx-reverse-proxy.conf.example` terminates TLS +and proxies the `/ws` WebSocket to `127.0.0.1:8765`. Keep it loopback-only: +the relay trusts forwarded transport metadata only from loopback peers. + +**Mainland-China mirrors.** The Docker build defaults to PyPI.org. Behind the +GFW, build with Aliyun as the primary index and TUNA as the fallback (both +carry the sdist-only `http-ece` wheel): + +```bash +docker build -f deploy/Dockerfile \ + --build-arg PIP_INDEX_URL=https://mirrors.aliyun.com/pypi/simple \ + --build-arg PIP_EXTRA_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple \ + -t cc-remote-relay . +``` + ## Native terminal coordination - **Claude Code:** run `claude` directly for the untouched official process; diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml new file mode 100644 index 0000000..1a0eace --- /dev/null +++ b/deploy/docker-compose.yml @@ -0,0 +1,40 @@ +# cc-remote Relay container. +# +# The official deploy is a systemd venv + managed Caddy (see deploy/README.md). +# This compose file runs the same relay inside a container for isolated, +# memory-bounded VPSes, or for any host that already terminates TLS itself. +# +# Public TLS/WebSocket termination is expected to stay with nginx or another +# host reverse proxy: the relay is published only to the host loopback here, +# and the relay only trusts forwarded transport metadata from loopback peers. +# See nginx-reverse-proxy.conf.example for the nginx alternative to Caddy. +# +# Usage (from this deploy/ directory): +# cp env.relay.docker.example env.relay # then fill in the secrets +# docker compose up -d --build +# curl https://your-domain/healthz # -> {"ok":true,...} +services: + relay: + build: + context: .. + dockerfile: deploy/Dockerfile + image: cc-remote-relay:latest + container_name: cc-remote-relay + restart: unless-stopped + ports: + # Loopback-only publish: the host nginx/Caddy terminates the public TLS + # and WebSocket connection and proxies it to 127.0.0.1:8765. + - "127.0.0.1:8765:8765" + env_file: + - env.relay + volumes: + # Persistent device registry + Web Push subscription databases. The named + # volume inherits /app/state ownership from the image (the ccremote + # user), so no host-side chown is needed. + - relay-state:/app/state + read_only: true + tmpfs: + - /tmp + +volumes: + relay-state: diff --git a/deploy/env.relay.docker.example b/deploy/env.relay.docker.example new file mode 100644 index 0000000..57f6c38 --- /dev/null +++ b/deploy/env.relay.docker.example @@ -0,0 +1,36 @@ +# deploy/env.relay — environment for the Docker container (docker-compose.yml). +# +# cp env.relay.docker.example env.relay # then fill in the secrets below +# +# Generate each secret with: openssl rand -hex 32 +# setup-vps.sh rejects placeholders, LOGIN_PASSWORD shorter than 16 characters, +# and SESSION_SECRET / WRAPPER_TOKEN shorter than 32 characters. The relay +# enforces the same rules at startup, so the container will fail fast on any +# placeholder left behind. +# +# The Dockerfile already sets the container-appropriate defaults, so these do +# not need to appear in this file: +# RELAY_HOST=0.0.0.0 (published to the host loopback by compose) +# RELAY_PORT=8765 +# WEB_STATIC_DIR=/app/web/dist +# PUSH_DB_PATH=/app/state/relay-push.sqlite3 +# DEVICE_DB_PATH=/app/state/relay-devices.sqlite3 +# SESSION_TTL_SECONDS=604800 +# Override any of them here if you need a different value. + +# Exact browser origin allowed to open a cookie-authenticated WebSocket. +PUBLIC_ORIGIN=https://cc-remote.example.com +# Leave at 0 for the normal domain + TLS path (see the nginx example / Caddy). +ALLOW_INSECURE_HTTP=0 +# Web login password (what you type in the browser to log in). REQUIRED. +LOGIN_PASSWORD=REPLACE_WITH_A_STRONG_PASSWORD +# HMAC secret for signing web session tokens. REQUIRED. +SESSION_SECRET=REPLACE_WITH_openssl_rand_hex_32 +# Must match the wrapper's WRAPPER_TOKEN. +WRAPPER_TOKEN=REPLACE_WITH_STRONG_WRAPPER_TOKEN +# Optional durable Web Push completion notifications. Configure all three +# VAPID values; keep the private key readable only by the relay service user. +# PUSH_VAPID_PUBLIC_KEY=REPLACE_WITH_VAPID_PUBLIC_KEY +# PUSH_VAPID_PRIVATE_KEY=REPLACE_WITH_VAPID_PRIVATE_KEY +# PUSH_VAPID_SUBJECT=mailto:admin@example.com +LOG_LEVEL=INFO diff --git a/deploy/nginx-reverse-proxy.conf.example b/deploy/nginx-reverse-proxy.conf.example new file mode 100644 index 0000000..18bdbcf --- /dev/null +++ b/deploy/nginx-reverse-proxy.conf.example @@ -0,0 +1,59 @@ +# cc-remote Relay behind nginx. +# +# The official deploy uses a managed Caddy that terminates TLS and reverse +# proxies the WebSocket (deploy/Caddyfile). If you already run nginx as the +# only front on this host, use this file as the alternative. The relay +# deliberately trusts forwarded transport metadata only from loopback peers, so +# nginx MUST run on the same host and proxy to the loopback address where the +# container published port 8765 (see docker-compose.yml). +# +# Setup: +# 1. Point an A/AAAA record for cc-remote.example.com at this VPS and open +# 80/443. +# 2. Install a certificate. The .well-known/acme-challenge location below is +# already present, so `certbot --nginx -d cc-remote.example.com` will work +# out of the box and fill in the ssl_ lines for you. +# 3. Save this file as /etc/nginx/sites-available/cc-remote, symlink it into +# /etc/nginx/sites-enabled/, then `nginx -t && systemctl reload nginx`. +# (Skip the symlink on distros that read /etc/nginx/conf.d/ directly.) + +server { + listen 80; + listen [::]:80; + server_name cc-remote.example.com; + + # Let certbot complete the ACME challenge on the HTTP listener before it + # adds the 443 block. Can be removed after your certificate is in place. + location /.well-known/acme-challenge/ { + root /var/www/html; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name cc-remote.example.com; + + ssl_certificate /etc/letsencrypt/live/cc-remote.example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/cc-remote.example.com/privkey.pem; + + # The /ws WebSocket carries wrapper and browser connections, so the upgrade + # headers and long timeouts are required — keep-alive settings alone are + # not enough. + location / { + proxy_pass http://127.0.0.1:8765; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + } +} From 20ef3efc7ac991175b0e07c65e7860a6fcff00a3 Mon Sep 17 00:00:00 2001 From: Hachiwa0 <140583188+Hachiwa0@users.noreply.github.com> Date: Thu, 27 Aug 2026 11:08:28 +0800 Subject: [PATCH 2/2] deploy: address review feedback (env leak, loopback peer, nginx bootstrap, sudo env, release download) --- .dockerignore | 5 ++ README.md | 8 ++- README_en.md | 11 ++-- deploy/Dockerfile | 2 +- deploy/docker-compose.yml | 12 ++-- deploy/env.relay.docker.example | 2 +- deploy/install.sh | 8 +++ deploy/nginx-reverse-proxy.conf.example | 73 +++++++++++++------------ 8 files changed, 72 insertions(+), 49 deletions(-) diff --git a/.dockerignore b/.dockerignore index 826c954..13c53b7 100644 --- a/.dockerignore +++ b/.dockerignore @@ -10,3 +10,8 @@ web/node_modules/ web/dist/ web/test-results/ *.log +# Never leak a populated env file into the image. `cp env.relay.docker.example +# env.relay` is the documented first step; the real env holds the login +# password, session secret, and wrapper token, so it must never reach the +# build context. +deploy/env.relay diff --git a/README.md b/README.md index 14daaa7..60bbe14 100644 --- a/README.md +++ b/README.md @@ -440,9 +440,11 @@ export UV_PYTHON_INSTALL_MIRROR=https://registry.npmmirror.com/-/binary/python-b ./install.sh wrapper --relay https://remote.example.com --pair XXXXX-XXXXX-XXXXX-XXXXX --name "MacBook Pro" ``` -若 `install.sh` 本身下载角色包超时,可先在别处下好 release 包再重试。Relay 用 -Docker 容器部署时,构建阶段同样可用 `--build-arg PIP_INDEX_URL=...` 加速 -(见 deploy/README.md 的容器章节)。 +若 `install.sh` 下载角色包超时:脚本每次都会从 +`CC_REMOTE_RELEASE_BASE_URL`(默认 GitHub Release)重新下载、没有本地缓存, +可把该变量指向镜像源或本地 `file://` 路径后重试。Relay 用 Docker 容器部署时, +构建阶段同样可用 `--build-arg PIP_INDEX_URL=...` 加速(见 deploy/README.md 的 +容器章节)。 ## 生产部署(公网 VPS 中继 + 你机器上的 wrapper) diff --git a/README_en.md b/README_en.md index 29398d1..9ac976d 100644 --- a/README_en.md +++ b/README_en.md @@ -519,11 +519,12 @@ export UV_PYTHON_INSTALL_MIRROR=https://registry.npmmirror.com/-/binary/python-b ./install.sh wrapper --relay https://remote.example.com --pair XXXXX-XXXXX-XXXXX-XXXXX --name "MacBook Pro" ``` -If `install.sh` itself times out fetching the role bundle, pre-download the -release archive elsewhere and retry. When deploying the Relay in a Docker -container, the build stage can use the same mirror idea via -`--build-arg PIP_INDEX_URL=...` (see the container section of -[deploy/README.md](deploy/README.md)). +If `install.sh` times out fetching the role bundle, note that it always +re-downloads from `CC_REMOTE_RELEASE_BASE_URL` (GitHub Release by default) with +no local cache - point that variable at a mirror or a local `file://` path and +retry. When deploying the Relay in a Docker container, the build stage can use +the same mirror idea via `--build-arg PIP_INDEX_URL=...` (see the container +section of [deploy/README.md](deploy/README.md)). ## Production deploy (public VPS relay + wrapper on your machine) diff --git a/deploy/Dockerfile b/deploy/Dockerfile index 27f1d2b..19c802b 100644 --- a/deploy/Dockerfile +++ b/deploy/Dockerfile @@ -30,7 +30,7 @@ FROM python:3.13-slim-bookworm AS relay ENV PYTHONUNBUFFERED=1 \ PIP_NO_CACHE_DIR=1 \ PIP_DISABLE_PIP_VERSION_CHECK=1 \ - RELAY_HOST=0.0.0.0 \ + RELAY_HOST=127.0.0.1 \ RELAY_PORT=8765 \ WEB_STATIC_DIR=/app/web/dist \ PUSH_DB_PATH=/app/state/relay-push.sqlite3 \ diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 1a0eace..de9ce02 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -21,10 +21,14 @@ services: image: cc-remote-relay:latest container_name: cc-remote-relay restart: unless-stopped - ports: - # Loopback-only publish: the host nginx/Caddy terminates the public TLS - # and WebSocket connection and proxies it to 127.0.0.1:8765. - - "127.0.0.1:8765:8765" + # Host networking so the relay sees loopback peers. The relay only trusts + # forwarded transport metadata (X-Forwarded-Proto / X-Forwarded-For) from + # 127.0.0.1 and ::1; the Docker bridge gateway is not one of those, so a + # normal `ports:` mapping would make it ignore the proxy headers and reject + # the HTTPS PUBLIC_ORIGIN. With host networking the relay binds loopback + # only (RELAY_HOST=127.0.0.1), which the host nginx/Caddy reaches directly. + # Linux hosts only; the `ports:` mapping is ignored under host networking. + network_mode: host env_file: - env.relay volumes: diff --git a/deploy/env.relay.docker.example b/deploy/env.relay.docker.example index 57f6c38..49e231a 100644 --- a/deploy/env.relay.docker.example +++ b/deploy/env.relay.docker.example @@ -10,7 +10,7 @@ # # The Dockerfile already sets the container-appropriate defaults, so these do # not need to appear in this file: -# RELAY_HOST=0.0.0.0 (published to the host loopback by compose) +# RELAY_HOST=127.0.0.1 (host networking; loopback-bound, see compose) # RELAY_PORT=8765 # WEB_STATIC_DIR=/app/web/dist # PUSH_DB_PATH=/app/state/relay-push.sqlite3 diff --git a/deploy/install.sh b/deploy/install.sh index 368ec50..65b601f 100755 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -124,6 +124,14 @@ if [ "$system" = linux ] && [ "$(id -u)" -ne 0 ]; then if [ -n "${CC_REMOTE_LOGIN_PASSWORD_FILE:-}" ]; then sudo_env+=(CC_REMOTE_LOGIN_PASSWORD_FILE="$CC_REMOTE_LOGIN_PASSWORD_FILE") fi + # Preserve mirror-acceleration variables for the bundled uv across sudo. + # Without this, `export UV_DEFAULT_INDEX=... ./install.sh wrapper ...` on + # Linux silently loses the mirror because sudo filters the environment. + for uv_var in UV_DEFAULT_INDEX UV_PYTHON_INSTALL_MIRROR; do + if [ -n "${!uv_var:-}" ]; then + sudo_env+=("$uv_var=${!uv_var}") + fi + done sudo env "${sudo_env[@]}" \ "$installer" "$bundle" "$@" else diff --git a/deploy/nginx-reverse-proxy.conf.example b/deploy/nginx-reverse-proxy.conf.example index 18bdbcf..9b0c128 100644 --- a/deploy/nginx-reverse-proxy.conf.example +++ b/deploy/nginx-reverse-proxy.conf.example @@ -4,26 +4,27 @@ # proxies the WebSocket (deploy/Caddyfile). If you already run nginx as the # only front on this host, use this file as the alternative. The relay # deliberately trusts forwarded transport metadata only from loopback peers, so -# nginx MUST run on the same host and proxy to the loopback address where the -# container published port 8765 (see docker-compose.yml). +# nginx MUST run on the same host and proxy to the relay's loopback address +# (with the Docker compose file, that is 127.0.0.1:8765 on host networking). # -# Setup: +# Bootstrap on a fresh host: # 1. Point an A/AAAA record for cc-remote.example.com at this VPS and open # 80/443. -# 2. Install a certificate. The .well-known/acme-challenge location below is -# already present, so `certbot --nginx -d cc-remote.example.com` will work -# out of the box and fill in the ssl_ lines for you. -# 3. Save this file as /etc/nginx/sites-available/cc-remote, symlink it into -# /etc/nginx/sites-enabled/, then `nginx -t && systemctl reload nginx`. -# (Skip the symlink on distros that read /etc/nginx/conf.d/ directly.) +# 2. Install this file with ONLY the HTTP block active (the 443 block below +# is commented out until a certificate exists - otherwise `nginx -t` +# fails before the ACME challenge can even be served). +# 3. Issue the certificate: `certbot certonly --webroot -w /var/www/html +# -d cc-remote.example.com`, then uncomment the 443 block (or let +# `certbot --nginx` fill it in once the challenge can be served). +# 4. `nginx -t && systemctl reload nginx`. server { listen 80; listen [::]:80; server_name cc-remote.example.com; - # Let certbot complete the ACME challenge on the HTTP listener before it - # adds the 443 block. Can be removed after your certificate is in place. + # Serves the ACME challenge so certbot can complete the certificate + # issuance before the TLS block below is enabled. location /.well-known/acme-challenge/ { root /var/www/html; } @@ -33,27 +34,29 @@ server { } } -server { - listen 443 ssl; - listen [::]:443 ssl; - server_name cc-remote.example.com; - - ssl_certificate /etc/letsencrypt/live/cc-remote.example.com/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/cc-remote.example.com/privkey.pem; - - # The /ws WebSocket carries wrapper and browser connections, so the upgrade - # headers and long timeouts are required — keep-alive settings alone are - # not enough. - location / { - proxy_pass http://127.0.0.1:8765; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_read_timeout 3600s; - proxy_send_timeout 3600s; - } -} +# TLS block. Keep it commented out until the certificate exists (see the +# bootstrap steps above), then uncomment it - or let certbot --nginx manage it. +#server { +# listen 443 ssl; +# listen [::]:443 ssl; +# server_name cc-remote.example.com; +# +# ssl_certificate /etc/letsencrypt/live/cc-remote.example.com/fullchain.pem; +# ssl_certificate_key /etc/letsencrypt/live/cc-remote.example.com/privkey.pem; +# +# # The /ws WebSocket carries wrapper and browser connections, so the upgrade +# # headers and long timeouts are required - keep-alive settings alone are +# # not enough. +# location / { +# proxy_pass http://127.0.0.1:8765; +# proxy_http_version 1.1; +# proxy_set_header Upgrade $http_upgrade; +# proxy_set_header Connection "upgrade"; +# proxy_set_header Host $host; +# proxy_set_header X-Real-IP $remote_addr; +# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +# proxy_set_header X-Forwarded-Proto $scheme; +# proxy_read_timeout 3600s; +# proxy_send_timeout 3600s; +# } +#}