#!/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 <