Files
ChatApp/scripts/migrate/01-bootstrap-new-server.sh
T
byGalax 5bc30c950c feat(infra): migrate self-hosted backend to netralax.de
Move Supabase + LiveKit from the netralax.cloud VPS to a new netralax.de server. Adds the migration runbook (docs/), one-time move scripts (scripts/migrate/), and prod Caddy/LiveKit config templates (infra/). Repoints the desktop publish/changelog URLs and prod ops config to .de. JWT_SECRET + VAPID copied identically so already-installed clients keep working; the new server also serves the legacy .cloud hostnames.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 19:39:04 +02:00

245 lines
11 KiB
Bash

#!/usr/bin/env bash
#
# Bootstrap a fresh netralax.de VPS so it can host the Supabase + LiveKit stack.
#
# COPY THIS SCRIPT TO THE NEW SERVER AND RUN IT THERE as root (or via sudo):
# scp scripts/migrate/01-bootstrap-new-server.sh debian@141.95.34.204:/tmp/
# ssh debian@141.95.34.204 'sudo bash /tmp/01-bootstrap-new-server.sh'
#
# It is idempotent: re-running it only fills in what is missing. It installs
# Docker CE + the compose plugin, opens the firewall, clones supabase/supabase,
# prepares /opt/livekit, installs Caddy, creates the update host + deploy user,
# and writes placeholder config. It NEVER fabricates secret values — those you
# copy from the old server (see the NEXT STEPS block it prints at the end).
set -euo pipefail
# --- must run as root ------------------------------------------------------
if [[ "${EUID}" -ne 0 ]]; then
echo "this script must run as root (use: sudo bash $0)" >&2
exit 1
fi
SUPABASE_DIR="/opt/supabase"
LIVEKIT_DIR="/opt/livekit"
UPDATES_DIR="/var/www/updates/windows"
DEPLOY_USER="chatapp-deploy"
log() { echo "==> $*"; }
# --- base packages ---------------------------------------------------------
log "updating apt and installing base packages"
export DEBIAN_FRONTEND=noninteractive
apt-get update -y
apt-get install -y \
ca-certificates curl gnupg lsb-release git ufw rsync apt-transport-https
# --- Docker CE + compose plugin -------------------------------------------
if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
log "docker + compose plugin already installed — skipping"
else
log "installing Docker CE + compose plugin (official repo)"
install -m 0755 -d /etc/apt/keyrings
if [[ ! -f /etc/apt/keyrings/docker.gpg ]]; then
curl -fsSL https://download.docker.com/linux/debian/gpg \
| gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
fi
. /etc/os-release
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/${ID} ${VERSION_CODENAME} stable" \
> /etc/apt/sources.list.d/docker.list
apt-get update -y
apt-get install -y \
docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
systemctl enable --now docker
fi
# Let the login user run docker/compose without sudo (effective on next login).
usermod -aG docker "${SUDO_USER:-debian}" || true
# --- firewall (ufw) --------------------------------------------------------
# Media + TURN ports bypass Caddy entirely and MUST be open or calls have no A/V.
log "configuring ufw"
ufw allow 22/tcp comment 'ssh'
ufw allow 80/tcp comment 'http (caddy / lets encrypt)'
ufw allow 443/tcp comment 'https (caddy)'
ufw allow 7880/tcp comment 'livekit signaling ws (behind caddy)'
ufw allow 7881/tcp comment 'livekit rtc tcp fallback'
ufw allow 50000:50100/udp comment 'livekit rtc udp'
ufw allow 3478/tcp comment 'coturn'
ufw allow 3478/udp comment 'coturn'
ufw allow 5349/tcp comment 'coturn turns (tls)'
ufw allow 50200:50300/udp comment 'coturn turn relay'
# Enable non-interactively (idempotent — re-enabling is a no-op).
ufw --force enable
ufw status verbose || true
# --- Supabase (clone upstream, prepare .env) -------------------------------
if [[ -d "${SUPABASE_DIR}/.git" || -f "${SUPABASE_DIR}/docker-compose.yml" ]]; then
log "${SUPABASE_DIR} already populated — skipping clone"
else
log "cloning supabase/supabase into a temp dir and laying out ${SUPABASE_DIR}"
tmp="$(mktemp -d)"
git clone --depth 1 https://github.com/supabase/supabase "${tmp}/supabase"
mkdir -p "${SUPABASE_DIR}"
# The runnable self-hosted stack lives in supabase/docker.
cp -r "${tmp}/supabase/docker/." "${SUPABASE_DIR}/"
rm -rf "${tmp}"
fi
# Prepare .env from the example WITHOUT inventing secrets.
#
# IMPORTANT: Supabase's upstream .env.example does NOT ship blank secrets — it
# ships well-known PUBLIC default values (JWT_SECRET=your-super-secret..., the
# matching default ANON_KEY/SERVICE_ROLE_KEY, POSTGRES_PASSWORD, etc.). Booting
# with those is both a security hole AND wrong: the baked anon key in installed
# clients is signed with the OLD server's JWT_SECRET, so a default secret makes
# the gateway reject every token and drop all sessions — silently. So we
# OVERWRITE the security-critical keys with a loud sentinel that fails fast if
# someone forgets to fill them from the old server.
SENTINEL="__COPY_FROM_OLD_SERVER__"
CRIT_KEYS=(POSTGRES_PASSWORD JWT_SECRET ANON_KEY SERVICE_ROLE_KEY \
SECRET_KEY_BASE VAULT_ENC_KEY DASHBOARD_PASSWORD)
if [[ -f "${SUPABASE_DIR}/.env" ]]; then
log "${SUPABASE_DIR}/.env already exists — leaving it untouched"
elif [[ -f "${SUPABASE_DIR}/.env.example" ]]; then
cp "${SUPABASE_DIR}/.env.example" "${SUPABASE_DIR}/.env"
for k in "${CRIT_KEYS[@]}"; do
sed -i "s|^${k}=.*|${k}=${SENTINEL}|" "${SUPABASE_DIR}/.env" || true
done
log "wrote ${SUPABASE_DIR}/.env — critical secrets set to ${SENTINEL}."
log "These are NOT blank by default upstream; you MUST copy the real values"
log "1:1 from the OLD server's /opt/supabase/.env (esp. JWT_SECRET + VAPID)."
else
log "WARNING: no .env.example found in ${SUPABASE_DIR}; create .env by hand"
fi
# --- LiveKit dir -----------------------------------------------------------
log "preparing ${LIVEKIT_DIR}"
mkdir -p "${LIVEKIT_DIR}"
if [[ ! -f "${LIVEKIT_DIR}/livekit.yaml" ]]; then
cat > "${LIVEKIT_DIR}/livekit.yaml" <<'YAML'
# PLACEHOLDER — replace with infra/livekit/livekit.prod.yaml.example contents.
# Prod config MUST set rtc.use_external_ip: true and must NOT hardcode
# node_ip: 127.0.0.1 (that is dev-only). Fill the keys: block with the SAME
# API key/secret as LIVEKIT_API_KEY / LIVEKIT_API_SECRET in /opt/supabase/.env.
YAML
log "wrote placeholder ${LIVEKIT_DIR}/livekit.yaml"
fi
if [[ ! -f "${LIVEKIT_DIR}/coturn.conf" ]]; then
cat > "${LIVEKIT_DIR}/coturn.conf" <<'CONF'
# PLACEHOLDER — replace with infra/livekit/coturn.prod.conf.example contents.
# Set external-ip to this VPS's public IP, point cert/pkey at the TLS cert for
# turn.netralax.de, and set a real lt-cred-mech user/password.
CONF
log "wrote placeholder ${LIVEKIT_DIR}/coturn.conf"
fi
if [[ ! -f "${LIVEKIT_DIR}/docker-compose.yml" ]]; then
cat > "${LIVEKIT_DIR}/docker-compose.yml" <<'YAML'
# PLACEHOLDER — replace with infra/livekit/docker-compose.prod.yml.example.
# The dev infra/livekit/docker-compose.yml is NOT suitable for prod (coturn runs
# with --no-tls, no 5349, no cert). The prod compose uses network_mode: host,
# mounts ./livekit.yaml + ./coturn.conf, runs coturn with -c turnserver.conf,
# and mounts /etc/letsencrypt for the turn.netralax.de TURNS cert.
YAML
log "wrote placeholder ${LIVEKIT_DIR}/docker-compose.yml"
fi
# --- Caddy (official apt repo) --------------------------------------------
if command -v caddy >/dev/null 2>&1; then
log "caddy already installed — skipping"
else
log "installing Caddy (official repo)"
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
| gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
> /etc/apt/sources.list.d/caddy-stable.list
apt-get update -y
apt-get install -y caddy
systemctl enable caddy
fi
# Write a placeholder Caddyfile if none exists (do not clobber a real one).
if [[ ! -s /etc/caddy/Caddyfile ]] || grep -q 'PLACEHOLDER' /etc/caddy/Caddyfile 2>/dev/null; then
cat > /etc/caddy/Caddyfile <<'CADDY'
# PLACEHOLDER Caddyfile — replace with infra/caddy/Caddyfile from the repo.
# Serve the .de vhosts now; add the legacy .cloud vhosts only at cutover (after
# the .cloud DNS is repointed) so they keep already-installed clients working:
# supabase.netralax.de { reverse_proxy localhost:8000 }
# livekit.netralax.de { reverse_proxy localhost:7880 }
# update.netralax.de { root * /var/www/updates # NOT .../windows — see Caddyfile
# file_server }
CADDY
log "wrote placeholder /etc/caddy/Caddyfile"
fi
# --- update host + deploy user --------------------------------------------
log "preparing update host at ${UPDATES_DIR}"
mkdir -p "${UPDATES_DIR}"
if id "${DEPLOY_USER}" >/dev/null 2>&1; then
log "user ${DEPLOY_USER} already exists — skipping"
else
log "creating deploy user ${DEPLOY_USER}"
useradd --create-home --shell /bin/bash "${DEPLOY_USER}"
mkdir -p "/home/${DEPLOY_USER}/.ssh"
chmod 700 "/home/${DEPLOY_USER}/.ssh"
touch "/home/${DEPLOY_USER}/.ssh/authorized_keys"
chmod 600 "/home/${DEPLOY_USER}/.ssh/authorized_keys"
chown -R "${DEPLOY_USER}:${DEPLOY_USER}" "/home/${DEPLOY_USER}/.ssh"
fi
# Let the deploy user write release artifacts.
chown -R "${DEPLOY_USER}:${DEPLOY_USER}" "${UPDATES_DIR}"
# --- next steps ------------------------------------------------------------
cat <<EOF
============================================================================
BOOTSTRAP DONE — manual NEXT STEPS (this script invents NO secrets):
============================================================================
1. Fill ${SUPABASE_DIR}/.env. Copy these 1:1 from the OLD server's
/opt/supabase/.env so baked-in client tokens + push keep working:
POSTGRES_PASSWORD, JWT_SECRET, ANON_KEY, SERVICE_ROLE_KEY,
SECRET_KEY_BASE, VAULT_ENC_KEY, PG_META_CRYPTO_KEY,
SMTP_*, VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT,
PUSH_FANOUT_SHARED_SECRET, LIVEKIT_API_KEY, LIVEKIT_API_SECRET.
Set these to the NEW host:
SITE_URL / API_EXTERNAL_URL / SUPABASE_PUBLIC_URL = https://supabase.netralax.de
SUPABASE_URL = https://supabase.netralax.de
LIVEKIT_URL = wss://livekit.netralax.de
ADDITIONAL_REDIRECT_URLS must include (comma-separated, no spaces):
chatapp://auth/callback,netralax://auth/callback,
https://supabase.netralax.de,https://supabase.netralax.cloud
2. Drop the real LiveKit + coturn config in ${LIVEKIT_DIR}:
docker-compose.yml <- infra/livekit/docker-compose.prod.yml.example
livekit.yaml <- infra/livekit/livekit.prod.yaml.example
coturn.conf <- infra/livekit/coturn.prod.conf.example
Set rtc.use_external_ip: true, NO node_ip: 127.0.0.1, coturn external-ip
= this VPS's public IP, and a TLS cert for turn.netralax.de.
The LiveKit keys: block MUST match LIVEKIT_API_KEY/SECRET in .env.
(The on-server filenames are livekit.yaml / coturn.conf — same names
rotate-livekit-keys.sh expects.)
3. Place the real Caddyfile:
cp infra/caddy/Caddyfile /etc/caddy/Caddyfile && systemctl reload caddy
(serves both .de and .cloud vhosts).
4. Bring the Supabase DB up ONCE so init scripts create the roles, then
restore data from the laptop:
cd ${SUPABASE_DIR} && docker compose up -d db && sleep 20
# then on the laptop: ./scripts/migrate/02-migrate-data.sh
5. Repoint DNS A-records to THIS VPS's IP for BOTH domains:
supabase.netralax.de / .cloud, livekit.netralax.de / .cloud,
turn.netralax.de, update.netralax.de / .cloud.
6. Add the chatapp-deploy public key to
/home/${DEPLOY_USER}/.ssh/authorized_keys
and mirror electron-updater artifacts (latest.yml, *.exe, changelog.json)
under ${UPDATES_DIR} so BOTH update.netralax.de and .cloud serve them.
============================================================================
EOF