diff --git a/docs/explanations/2026-07-13-explanation-personal-website-edge.html b/docs/explanations/2026-07-13-explanation-personal-website-edge.html new file mode 100644 index 00000000..a884b474 --- /dev/null +++ b/docs/explanations/2026-07-13-explanation-personal-website-edge.html @@ -0,0 +1,487 @@ +
+ How a personal website took the root of kuhy.duckdns.org — moving
+ Gitea aside, teaching a shared reverse proxy to stop overwriting itself, and
+ keeping a second Caddy from stealing the ports.
+
+ This home server hosts several things at once: a self-hosted
+ Gitea (a GitHub-like git host), a dufs
+ file cloud, a SyncYomi manga-sync service — and now a
+ personal website. They all share a single public IP and a single pair of
+ open ports (80/443).
+
+ The trick that lets one IP serve many sites is a reverse
+ proxy. One program — here, a Caddy container
+ called gitea-caddy — owns ports 80/443, terminates HTTPS, and
+ routes each incoming request to the right backend based on the
+ hostname the visitor asked for:
+
127.0.0.1:3000127.0.0.1:5000127.0.0.1:8282+ Caddy reads a config file, the Caddyfile. Each service is + one block: a hostname, then where to send its traffic. +
+ +kuhy.duckdns.org { reverse_proxy 127.0.0.1:3000 } # Gitea (the root!)
+kuhy-cloud.duckdns.org{ reverse_proxy 127.0.0.1:5000 } # dufs
+kuhy-sync.duckdns.org { reverse_proxy 127.0.0.1:8282 } # SyncYomi
+ Two details matter for this change. First, the bare root
+ kuhy.duckdns.org was Gitea's — and the goal
+ was to give that root to a new website. Second, every container runs with
+ network_mode: host: instead of Docker's private network, each
+ container shares the host's network directly. (The home firewall drops
+ Docker's normal bridge forwarding, so host networking is the reliable way to
+ reach the internet for TLS renewals.) Host networking will come back to bite
+ us in section 2.
+
+ Finally, that Caddyfile isn't hand-written — it's generated by
+ setup_gitea.sh. SyncYomi and dufs each appended their
+ block to the same file from their own setup scripts. Hold that thought.
+
+ setup_gitea.sh is meant to be safe to re-run. But its way of
+ writing the Caddyfile was cat > Caddyfile — it
+ regenerated the whole file from scratch, containing only Gitea's
+ block. dufs and SyncYomi had appended their blocks later.
+ So every time you re-ran the Gitea setup, those blocks silently vanished and
+ two live services went dark. A latent trap, waiting for the next re-run.
+
+ The fix is the shape of the change: instead of one big file that one script
+ owns, the root Caddyfile becomes a single line —
+ import /etc/caddy/sites/*.caddy — and each service owns its
+ own snippet file in a sites/ folder. Now re-running any one
+ setup script only touches its own snippet. Nothing can clobber a neighbour.
+
Play: re-run the Gitea setup and watch what survives.
+All four services are up. Now re-run the Gitea setup…
+
+ The website is served by a second tiny Caddy container, listening
+ only on loopback 127.0.0.1:8088, with
+ gitea-caddy proxying the root domain to it. But both Caddies use
+ host networking — so they share every port on the host, including
+ Caddy's admin API on :2019. When the deploy ran
+ caddy reload against gitea-caddy, the reload was
+ delivered over :2019… and sometimes landed on the
+ website's Caddy instead. That Caddy then adopted the edge config,
+ grabbed 80/443, started failing to issue certificates, and abandoned 8088 —
+ the site went blank.
+
:2019admin off
+ The one-word fix: give the website's Caddy admin off. With no
+ admin endpoint, it can never be reconfigured from outside — it stays a plain
+ static server on 8088, and reloads only reach gitea-caddy.
+
+ One constant flips. It flows into Gitea's DOMAIN,
+ ROOT_URL, and its Caddy block — freeing the bare root for the
+ website. The DuckDNS wildcard already resolves the new subdomain, so no DNS
+ work is needed.
+
-readonly GITEA_DOMAIN="kuhy.duckdns.org" ++readonly GITEA_DOMAIN="gitea.kuhy.duckdns.org"
+ The generated root Caddyfile stops holding blocks and just imports a folder; + Gitea writes its own snippet; the Caddy container mounts that folder. +
+ volumes:
+ - ./Caddyfile:/etc/caddy/Caddyfile:ro
++ - ./sites:/etc/caddy/sites:ro # mount the snippet folder
+
+- cat >"$CADDYFILE" <<EOF
+-${GITEA_DOMAIN} { reverse_proxy 127.0.0.1:3000 }
+-EOF
++ ensure_dir "$SITES_DIR"
++ cat >"$CADDYFILE" <<'EOF' # root file: just an import
++import /etc/caddy/sites/*.caddy
++EOF
++ cat >"${SITES_DIR}/gitea.caddy" <<EOF # Gitea owns ONE file
++${GITEA_DOMAIN} { reverse_proxy 127.0.0.1:3000 }
++EOF
+ setup_personal_website.sh orchestrates the whole cutover. The
+ order is the safety property: existing blocks are migrated
+ into snippets before the root file is switched to the import line —
+ otherwise dufs and SyncYomi would blink out during the migration itself.
+
preflight # deps, docker up, gitea-caddy present +migrate_caddy_snippets # split TODAY's Caddyfile → sites/*.caddy (keep dufs, sync) +relocate_gitea # re-run setup_gitea.sh (now writes import + gitea.caddy) +build_website # pnpm build → ~/personal-website/dist +write_website_stack # loopback caddy compose + inner Caddyfile +write_website_snippet# sites/website.caddy → kuhy.duckdns.org +start_website # container up on 127.0.0.1:8088 +reload_caddy # single reload, now that every snippet exists +ensure_firewall # 80/443 already open — idempotent
+ The website's inner Caddyfile disables the admin API so a reload aimed at the + edge can never hijack it (Problem B). +
+{
+ admin off # host networking shares :2019 with gitea-caddy
+}
+:8088 {
+ root * /srv
+ try_files {path} /index.html # SPA fallback
+ file_server
+}
+ The React site itself was later extracted to its own repo
+ (github.com/kuhyx/personal-website); this commit is the
+ edge + deploy half that lives in testsAndMisc.
+
Five questions on the why. Score: 0 / 5.
+ +4 explanations · interactive walkthroughs of notable changes, newest first.
+5 explanations · interactive walkthroughs of notable changes, newest first.