Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions docs/superpowers/specs/2026-07-24-relay-deploy-cn-mirrors-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Relay Deploy: China Mirror Acceleration

**Date:** 2026-07-24
**Status:** Approved for implementation (user: design + implement end-to-end)

## Problem

One-click / `deploy.sh` relay deployment pulls Docker Hub images, Debian apt
packages, crates.io crates, GitHub source, and `get.docker.com`. On mainland
China hosts these endpoints are slow or unreliable, so deploy often stalls.

## Goals

1. Auto-detect mainland China at deploy start; allow force override.
2. Cover the full path: Desktop SSH (Docker install, GitHub sync) + `deploy.sh`
+ Dockerfile (apt + cargo) + Docker Hub pulls.
3. Persist host-level apt and Docker mirror config; keep Cargo mirroring scoped
to the relay Docker build so deployment never rewrites the SSH user's Cargo
configuration.
4. Ship built-in default CN mirrors; allow env overrides.
5. Keep non-CN hosts unchanged.

## Non-goals

- Changing relay runtime / account / port behavior
- Building a private mirror service
- Guaranteeing third-party public mirror uptime (defaults + overrides only)

## Detection

Priority:

1. `BITFUN_MIRROR=cn|global` or flags `--cn-mirror` / `--global-mirror`
2. Auto (`BITFUN_MIRROR=auto` default):
- Public IP country lookup (short timeout)
- Timezone `Asia/Shanghai` / `Asia/Chongqing` / `Asia/Urumqi`
- Connectivity heuristic: GitHub slow/fail + Aliyun mirror reachable → CN
3. On ambiguity → `global` (safe default)

## Default CN mirrors (overridable)

| Surface | Default | Override env |
|---|---|---|
| Docker Hub registry-mirrors | `https://docker.1ms.run`, `https://dockerproxy.net`, `https://docker.m.daocloud.io` | `BITFUN_DOCKER_REGISTRY_MIRRORS` (space/comma separated) |
| Debian/Ubuntu apt | `mirrors.aliyun.com` | `BITFUN_APT_MIRROR` |
| RHEL/CentOS yum/dnf docker-ce | Aliyun docker-ce | same family |
| Relay Docker build Cargo / crates.io | `sparse+https://rsproxy.cn/index/` | `BITFUN_CARGO_SPARSE_URL` |
| Rustup (host, if used) | `https://rsproxy.cn` | `BITFUN_RUSTUP_DIST_SERVER` |
| GitHub git / tarball | `https://ghfast.top/` prefix | `BITFUN_GITHUB_PROXY` |
| Docker Engine install | Aliyun docker-ce packages; fallback proxied `get.docker.com` | `BITFUN_DOCKER_INSTALL_URL` |

## Architecture

Canonical script: `src/apps/relay-server/mirror.sh`

- Sourced by `deploy.sh`
- Embedded into Desktop orchestration via `include_str!` from
`relay_deploy.rs` (single source of truth; no Cargo crate dependency on apps)
- Idempotent apply with backups under `/etc/bitfun/mirror-backup-*` and
`$HOME/.bitfun/mirror-backup-*`

Flow:

```
detect mode → if cn: apply host mirrors → export build/env vars
→ Docker install / GitHub sync use CN URLs
→ deploy.sh passes BITFUN_USE_CN_MIRROR=1 build-args
→ Dockerfile rewrites apt + writes build-local cargo config
```

## Persistence / merge rules

- **apt:** backup `sources.list` (+ `sources.list.d` bitfun file); write BitFun-owned
`sources.list.d/bitfun-cn-mirror.list` when possible; otherwise rewrite
`deb.debian.org` / `archive.ubuntu.com` hosts in place.
- **Docker daemon.json:** JSON-merge `registry-mirrors` (python3 when available);
never drop unrelated keys; `systemctl restart docker` only if daemon was
already manageable.
- **Cargo:** leave `$HOME/.cargo/config.toml` untouched. The builder writes
`/usr/local/cargo/config.toml` inside Docker for rsproxy sparse.
- Marker file: `$HOME/.bitfun/mirror-mode` = `cn|global` for logs/idempotency.
- **Rollback:** `BITFUN_MIRROR=global` removes the BitFun apt list, restores
source files renamed with `.bitfun-disabled`, removes only Docker mirrors
recorded as BitFun additions, and cleans the legacy managed Cargo block from
early deployments.

## Failure behavior

- Mirror apply failures log warnings and continue with best effort; do not abort
deploy solely because a public mirror endpoint is down.
- GitHub proxy failure keeps existing tarball fallback chain (try CN URL then
upstream if override allows).
- `BITFUN_MIRROR=global` skips CN writes and rolls back BitFun-owned host
mirror changes even on a China IP.

## Verification

- `bash -n` on `mirror.sh` / `deploy.sh`
- `cargo test -p bitfun-services-integrations --features remote-ssh` (include
embedded mirror script + existing deploy tests)
- `cargo check -p bitfun-desktop` when orchestration wiring changes
55 changes: 54 additions & 1 deletion src/apps/relay-server/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
# downloaded crates and compiled dependency objects across builds.
# - Binaries are copied out of the target cache mount into /out within the same
# RUN (cache mounts are not part of the image filesystem).
#
# China mirrors (optional):
# docker compose build --build-arg BITFUN_USE_CN_MIRROR=1
# Also: BITFUN_APT_MIRROR, BITFUN_CARGO_SPARSE_URL

# Pin a minor toolchain so floating `rust:1-bookworm` updates do not bust the
# entire builder cache on every upstream image refresh.
Expand All @@ -18,10 +22,43 @@ WORKDIR /build/src/apps/relay-server
# docker compose build --build-arg CARGO_BUILD_JOBS=1
# Note: empty value must NOT be set as ENV — cargo chokes on empty string.
ARG CARGO_BUILD_JOBS=
ARG BITFUN_USE_CN_MIRROR=0
ARG BITFUN_APT_MIRROR=mirrors.aliyun.com
ARG BITFUN_CARGO_SPARSE_URL=sparse+https://rsproxy.cn/index/

ENV DEBIAN_FRONTEND=noninteractive \
CARGO_TERM_COLOR=always \
CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse

# Configure apt + cargo mirrors inside the builder when deploying from China.
RUN set -eux; \
if [ "${BITFUN_USE_CN_MIRROR}" = "1" ]; then \
sed -i \
-e "s|deb.debian.org/debian|${BITFUN_APT_MIRROR}/debian|g" \
-e "s|security.debian.org/debian-security|${BITFUN_APT_MIRROR}/debian-security|g" \
/etc/apt/sources.list 2>/dev/null || true; \
if [ -f /etc/apt/sources.list.d/debian.sources ]; then \
sed -i \
-e "s|deb.debian.org/debian|${BITFUN_APT_MIRROR}/debian|g" \
-e "s|security.debian.org/debian-security|${BITFUN_APT_MIRROR}/debian-security|g" \
/etc/apt/sources.list.d/debian.sources; \
fi; \
mkdir -p /usr/local/cargo; \
printf '%s\n' \
'[source.crates-io]' \
'replace-with = "bitfun-rsproxy-sparse"' \
'' \
'[source.bitfun-rsproxy-sparse]' \
"registry = \"${BITFUN_CARGO_SPARSE_URL}\"" \
'' \
'[registries.bitfun-rsproxy-sparse]' \
"index = \"${BITFUN_CARGO_SPARSE_URL}\"" \
'' \
'[net]' \
'git-fetch-with-cli = true' \
> /usr/local/cargo/config.toml; \
fi

RUN apt-get update \
&& apt-get install -y --no-install-recommends \
pkg-config \
Expand Down Expand Up @@ -88,9 +125,25 @@ RUN --mount=type=cache,id=bitfun-relay-cargo-registry,target=/usr/local/cargo/re

FROM debian:bookworm-slim

ARG BITFUN_USE_CN_MIRROR=0
ARG BITFUN_APT_MIRROR=mirrors.aliyun.com

ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update \
RUN set -eux; \
if [ "${BITFUN_USE_CN_MIRROR}" = "1" ]; then \
sed -i \
-e "s|deb.debian.org/debian|${BITFUN_APT_MIRROR}/debian|g" \
-e "s|security.debian.org/debian-security|${BITFUN_APT_MIRROR}/debian-security|g" \
/etc/apt/sources.list 2>/dev/null || true; \
if [ -f /etc/apt/sources.list.d/debian.sources ]; then \
sed -i \
-e "s|deb.debian.org/debian|${BITFUN_APT_MIRROR}/debian|g" \
-e "s|security.debian.org/debian-security|${BITFUN_APT_MIRROR}/debian-security|g" \
/etc/apt/sources.list.d/debian.sources; \
fi; \
fi; \
apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl \
&& rm -rf /var/lib/apt/lists/*

Expand Down
26 changes: 26 additions & 0 deletions src/apps/relay-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,32 @@ memory VPS (common on arm64), use:
RELAY_CARGO_BUILD_JOBS=1 bash deploy.sh
```

### Mainland China hosts

`deploy.sh` (and Desktop one-click deploy) auto-detects mainland China and
configures host mirrors for apt, Docker Hub, and GitHub source retrieval plus
a build-local Cargo/crates.io mirror. Docker Engine installation also uses a
mainland mirror. Override when needed:

```bash
BITFUN_MIRROR=cn bash deploy.sh # force China mirrors
BITFUN_MIRROR=global bash deploy.sh # restore BitFun-managed upstream sources
bash deploy.sh --cn-mirror
bash deploy.sh --global-mirror
```

Defaults (overridable via env): Aliyun apt, Docker registry mirrors
(`docker.1ms.run` / `dockerproxy.net` / `docker.m.daocloud.io`),
rsproxy Cargo sparse index, `ghfast.top` GitHub prefix, Aliyun docker-ce
for Engine install (fallback: jsDelivr docker-install). See `mirror.sh`
for the full list (`BITFUN_APT_MIRROR`, `BITFUN_DOCKER_REGISTRY_MIRRORS`,
`BITFUN_CARGO_SPARSE_URL`, `BITFUN_GITHUB_PROXY`, …).

China mode does not modify the SSH user's global `~/.cargo/config.toml`; Cargo
mirroring is scoped to the relay image build. Switching to `global` restores
apt files disabled by BitFun and removes only Docker registry mirrors recorded
as BitFun additions.

`deploy.sh` enables Docker BuildKit so the Dockerfile can reuse Cargo
registry/git/`target` cache mounts across redeploys. Keep BuildKit enabled
(`DOCKER_BUILDKIT=1`, the deploy default) and avoid `docker builder prune`
Expand Down
47 changes: 45 additions & 2 deletions src/apps/relay-server/deploy.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# BitFun Relay Server — one-click deploy script.
# Usage: bash deploy.sh [--skip-build] [--skip-health-check]
# Usage: bash deploy.sh [--skip-build] [--skip-health-check] [--cn-mirror|--global-mirror]
#
# Run this script on the target server itself after SSH login.
# It deploys to the current machine only; it does not SSH to a remote host.
Expand All @@ -11,15 +11,21 @@
#
# Low-memory VPS tip (especially arm64):
# RELAY_CARGO_BUILD_JOBS=1 bash deploy.sh
#
# China hosts: auto-detects mainland China and configures apt/Docker/cargo/GitHub
# mirrors (override with BITFUN_MIRROR=cn|global or --cn-mirror/--global-mirror).

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=common.sh
source "${SCRIPT_DIR}/common.sh"
# shellcheck source=mirror.sh
source "${SCRIPT_DIR}/mirror.sh"

SKIP_BUILD=false
SKIP_HEALTH_CHECK=false
MIRROR_ARGS=()

usage() {
cat <<'EOF'
Expand All @@ -38,19 +44,29 @@ Supported architectures:
Options:
--skip-build Skip docker compose build, only recreate/start services
--skip-health-check Skip post-deploy health check
--cn-mirror Force China mirrors (apt/Docker/cargo/GitHub)
--global-mirror Force global upstream mirrors
-h, --help Show this help message

Environment:
RELAY_HOST_BIND_IP Host bind address for published port (default 0.0.0.0)
RELAY_CARGO_BUILD_JOBS Limit rustc parallelism inside Docker (e.g. 1 on small VPS)
DOCKER_DEFAULT_PLATFORM Leave unset for native host builds (recommended)
BITFUN_MIRROR auto|cn|global (default auto)
BITFUN_APT_MIRROR Debian/Ubuntu apt host (default mirrors.aliyun.com)
BITFUN_DOCKER_REGISTRY_MIRRORS Space/comma-separated Docker Hub mirrors
BITFUN_CARGO_SPARSE_URL Cargo sparse registry URL (default rsproxy)
BITFUN_GITHUB_PROXY GitHub HTTPS proxy prefix (default https://ghfast.top/)
EOF
}

for arg in "$@"; do
case "$arg" in
--skip-build) SKIP_BUILD=true ;;
--skip-health-check) SKIP_HEALTH_CHECK=true ;;
--cn-mirror|--global-mirror|--no-cn-mirror|--skip-mirror-apply)
MIRROR_ARGS+=("$arg")
;;
-h|--help)
usage
exit 0
Expand All @@ -70,13 +86,31 @@ echo "Target: current machine ($(uname -s) / ${HOST_ARCH}, uname=$(uname -m))"
echo "Note: run this script on the target server after SSH login."

assert_supported_arch
# Detect region and persist host mirrors before Docker pulls / image build.
# Validate the host first so unsupported machines are not modified.
bitfun_mirror_init "${MIRROR_ARGS[@]+"${MIRROR_ARGS[@]}"}"
require_docker_daemon
resolve_compose
warn_if_forced_foreign_platform

echo "Compose: ${COMPOSE[*]}"
cd "$SCRIPT_DIR"

# Persist compose build-args for CN builds (and subsequent restarts).
touch .env
chmod 600 .env 2>/dev/null || true
# Refresh BitFun-managed mirror keys without wiping unrelated .env entries.
if [ -f .env ]; then
tmp_env="$(mktemp)"
grep -Ev '^(BITFUN_USE_CN_MIRROR|BITFUN_APT_MIRROR|BITFUN_CARGO_SPARSE_URL)=' .env >"$tmp_env" || true
mv "$tmp_env" .env
fi
{
echo "BITFUN_USE_CN_MIRROR=${BITFUN_USE_CN_MIRROR:-0}"
echo "BITFUN_APT_MIRROR=${BITFUN_APT_MIRROR:-mirrors.aliyun.com}"
echo "BITFUN_CARGO_SPARSE_URL=${BITFUN_CARGO_SPARSE_URL:-sparse+https://rsproxy.cn/index/}"
} >>.env

# Build first so a compile failure does not take down a running relay.
if [ "$SKIP_BUILD" = true ]; then
echo "[1/2] Skipping Docker build (--skip-build)"
Expand All @@ -87,6 +121,12 @@ else
BUILD_ARGS+=(--build-arg "CARGO_BUILD_JOBS=${RELAY_CARGO_BUILD_JOBS}")
echo " Using CARGO_BUILD_JOBS=${RELAY_CARGO_BUILD_JOBS}"
fi
BUILD_ARGS+=(--build-arg "BITFUN_USE_CN_MIRROR=${BITFUN_USE_CN_MIRROR:-0}")
BUILD_ARGS+=(--build-arg "BITFUN_APT_MIRROR=${BITFUN_APT_MIRROR:-mirrors.aliyun.com}")
BUILD_ARGS+=(--build-arg "BITFUN_CARGO_SPARSE_URL=${BITFUN_CARGO_SPARSE_URL:-sparse+https://rsproxy.cn/index/}")
if [ "${BITFUN_USE_CN_MIRROR:-0}" = "1" ]; then
echo " Using China mirrors inside Docker build (apt + cargo)"
fi
# BuildKit is required for Dockerfile cargo registry/git/target cache mounts.
# Plain progress so nohup/file-redirected deploys still stream build lines.
export DOCKER_BUILDKIT=1
Expand All @@ -99,11 +139,14 @@ else
case "${BITFUN_DOCKER_MODE:-direct}" in
sudo)
sudo env DOCKER_BUILDKIT=1 COMPOSE_DOCKER_CLI_BUILD=1 BUILDKIT_PROGRESS="${BUILDKIT_PROGRESS}" \
BITFUN_USE_CN_MIRROR="${BITFUN_USE_CN_MIRROR:-0}" \
BITFUN_APT_MIRROR="${BITFUN_APT_MIRROR:-mirrors.aliyun.com}" \
BITFUN_CARGO_SPARSE_URL="${BITFUN_CARGO_SPARSE_URL:-sparse+https://rsproxy.cn/index/}" \
docker compose --progress=plain build "${BUILD_ARGS[@]}"
;;
sg)
# shellcheck disable=SC2086
sg docker -c "env DOCKER_BUILDKIT=1 COMPOSE_DOCKER_CLI_BUILD=1 BUILDKIT_PROGRESS='${BUILDKIT_PROGRESS}' docker compose --progress=plain build ${BUILD_ARGS[*]}"
sg docker -c "env DOCKER_BUILDKIT=1 COMPOSE_DOCKER_CLI_BUILD=1 BUILDKIT_PROGRESS='${BUILDKIT_PROGRESS}' BITFUN_USE_CN_MIRROR='${BITFUN_USE_CN_MIRROR:-0}' BITFUN_APT_MIRROR='${BITFUN_APT_MIRROR:-mirrors.aliyun.com}' BITFUN_CARGO_SPARSE_URL='${BITFUN_CARGO_SPARSE_URL:-sparse+https://rsproxy.cn/index/}' docker compose --progress=plain build ${BUILD_ARGS[*]}"
;;
*)
if [ "${#COMPOSE[@]}" -ge 2 ] && [ "${COMPOSE[0]}" = "docker" ] && [ "${COMPOSE[1]}" = "compose" ]; then
Expand Down
4 changes: 4 additions & 0 deletions src/apps/relay-server/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ services:
# Pass through optional parallelism limit for low-memory hosts:
# RELAY_CARGO_BUILD_JOBS=1 docker compose build
CARGO_BUILD_JOBS: ${RELAY_CARGO_BUILD_JOBS:-}
# China mirror switch (set by deploy.sh / BITFUN_MIRROR=cn):
BITFUN_USE_CN_MIRROR: ${BITFUN_USE_CN_MIRROR:-0}
BITFUN_APT_MIRROR: ${BITFUN_APT_MIRROR:-mirrors.aliyun.com}
BITFUN_CARGO_SPARSE_URL: ${BITFUN_CARGO_SPARSE_URL:-sparse+https://rsproxy.cn/index/}
# Intentionally no `platform:` pin — build/run for the host CPU
# (linux/amd64 on x86_64, linux/arm64 on aarch64).
container_name: bitfun-relay
Expand Down
Loading