#!/bin/bash # # setup_personal_website.sh — self-hosted personal website at kuhy.duckdns.org # # Builds the React/TypeScript personal website (projects showcase + CV) and # serves its static build behind the existing shared Caddy edge (gitea-caddy), # taking the bare root domain kuhy.duckdns.org. Gitea, which used to own that # root, is moved to gitea.kuhy.duckdns.org. # # What it does (idempotent — safe to re-run): # - Installs build/runtime deps (node, pnpm, docker, ...). # - Migrates the shared Caddyfile to a per-service snippet directory so no # service block ever clobbers another (fixes a latent bug where re-running # setup_gitea.sh dropped the dufs/syncyomi blocks). # - Moves Gitea to gitea.kuhy.duckdns.org via setup_gitea.sh (single source). # - Builds the website and serves dist/ from a loopback caddy container on # 127.0.0.1:8088, fronted by a reverse-proxy snippet on the root domain. # - Reuses the existing DuckDNS updater (wildcard already resolves) and opens # the firewall via setup_wireguard_ssh.sh. # # Reboot survival: the website container uses restart: unless-stopped and # docker.service is enabled — no systemd unit is needed. # # Usage: # setup_personal_website.sh [setup] Build + deploy (default). # setup_personal_website.sh status Self-diagnose the deployment. # setup_personal_website.sh help Show this help. # # Prerequisite: setup_gitea.sh must have been run at least once (the gitea-caddy # edge must exist). Run as your normal user, not root. set -euo pipefail SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" readonly SCRIPT_DIR # shellcheck source=../../lib/common.sh source "${SCRIPT_DIR}/../../lib/common.sh" # --- Configuration ---------------------------------------------------------- readonly WEBSITE_DOMAIN="kuhy.duckdns.org" readonly GITEA_DOMAIN="gitea.kuhy.duckdns.org" readonly WEBSITE_PORT="8088" # React/TS source lives in its own standalone repo (github.com/kuhyx/personal-website), # cloned at ~/personal-website; the runtime data dir holds the generated compose + # inner Caddyfile. readonly WEBSITE_SRC="${HOME}/personal-website" readonly WEBSITE_DATA_DIR="${HOME}/personal-website-serve" readonly WEBSITE_COMPOSE="${WEBSITE_DATA_DIR}/docker-compose.yml" readonly WEBSITE_INNER_CADDY="${WEBSITE_DATA_DIR}/Caddyfile" # Shared edge owned by setup_gitea.sh. readonly GITEA_DATA_DIR="${HOME}/gitea" readonly CADDYFILE="${GITEA_DATA_DIR}/Caddyfile" readonly SITES_DIR="${GITEA_DATA_DIR}/sites" readonly WEBSITE_SNIPPET="${SITES_DIR}/website.caddy" readonly CADDY_CONTAINER="gitea-caddy" # Sibling scripts (single sources of truth for their concerns). readonly GITEA_SCRIPT="${SCRIPT_DIR}/setup_gitea.sh" readonly WG_SCRIPT="${SCRIPT_DIR}/setup_wireguard_ssh.sh" # Canonical CV data (rendered by the CV repo's build_cv.py); copied into the # website build. A committed copy in the repo keeps the build standalone. readonly CV_JSON_SRC="${HOME}/CV/generic/cv.json" readonly CV_PDF_SRC="${HOME}/CV/generic/cv-en.pdf" die() { log_error "$1" exit 1 } # Takes the exit status to leave with, so the error path can print the same help # and still fail. It used to hardcode `exit 0`, which made the `exit 1` after the # unknown-command branch unreachable -- so `setup_personal_website.sh bogus` # printed an error and then reported SUCCESS to its caller. usage() { grep -E '^#( |$)' "$0" | sed -E 's/^# ?//' exit "${1:-0}" } # --- Phase 1: preflight ----------------------------------------------------- preflight() { [[ ${EUID} -ne 0 ]] || die "Run as your normal user, not root." [[ -d $WEBSITE_SRC ]] || die "Website source not found at ${WEBSITE_SRC}." install_missing_pacman_packages nodejs pnpm docker docker-compose jq curl if ! is_service_active docker; then log_info "Starting and enabling docker.service…" sudo systemctl enable --now docker fi [[ -f $CADDYFILE ]] || die "Shared Caddyfile ${CADDYFILE} missing — run setup_gitea.sh first." if ! docker ps --format '{{.Names}}' | grep -qx "$CADDY_CONTAINER"; then die "Container ${CADDY_CONTAINER} is not running — run setup_gitea.sh first." fi log_ok "Preflight checks passed." } # --- Phase 2: migrate the shared Caddyfile into per-service snippets --------- # Reads the CURRENT monolithic Caddyfile and writes every top-level block to # sites/