mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 20:07:04 +01:00
static_site_caddy.sh is the one site-block body for the personal website and kcd2 dice Caddies: prerendered paths, real 404s and the ai-catalog Link header instead of an SPA catch-all. Both setup scripts gain a sudo-free `serve` that recreates only their container; kcd2's readiness wait needs a 200. redeploy_agent_ready_sites.sh serves both, restarts the popes site and runs the AgentReady checker against all three public URLs. Tested by test_static_site_serve.sh, test_redeploy_agent_ready_sites.sh and the caddy:2.8 e2e_agent_ready_caddy.sh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NBScELfMUiieK9ManFbS3Y
242 lines
7.8 KiB
Bash
Executable File
242 lines
7.8 KiB
Bash
Executable File
#!/bin/bash
|
|
# CV build, website generation and the Caddy stack snippets.
|
|
#
|
|
# Sourced by setup_personal_website.sh; split out to keep it under the 250-line cap.
|
|
# Sourced rather than run, so it inherits the caller's strict mode and
|
|
# the helper functions and variables defined above the source line.
|
|
|
|
# shellcheck source=static_site_caddy.sh
|
|
source "${BASH_SOURCE[0]%/*}/static_site_caddy.sh"
|
|
|
|
# --- Phase 5: build the website --------------------------------------------
|
|
build_website() {
|
|
log_info "Installing website dependencies and building…"
|
|
(
|
|
cd "$WEBSITE_SRC" || die "Cannot enter ${WEBSITE_SRC}."
|
|
pnpm install --frozen-lockfile
|
|
pnpm run build
|
|
)
|
|
[[ -f "${WEBSITE_SRC}/dist/index.html" ]] ||
|
|
die "Build did not produce ${WEBSITE_SRC}/dist/index.html."
|
|
log_ok "Website built to ${WEBSITE_SRC}/dist."
|
|
}
|
|
|
|
# --- Phase 6: write the website's static-serve stack ------------------------
|
|
write_website_stack() {
|
|
ensure_dir "$WEBSITE_DATA_DIR"
|
|
cat >"$WEBSITE_COMPOSE" <<EOF
|
|
# Managed by setup_personal_website.sh — do not edit by hand.
|
|
services:
|
|
personal-website:
|
|
image: caddy:2.8
|
|
container_name: personal-website
|
|
restart: unless-stopped
|
|
network_mode: host
|
|
volumes:
|
|
- ${WEBSITE_SRC}/dist:/srv:ro
|
|
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
|
EOF
|
|
|
|
cat >"$WEBSITE_INNER_CADDY" <<EOF
|
|
# Managed by setup_personal_website.sh — do not edit by hand.
|
|
# Static file server on loopback; fronted by gitea-caddy on the public domain.
|
|
#
|
|
# admin off is REQUIRED: this container shares the host network namespace with
|
|
# gitea-caddy, so without it both Caddies would listen on the admin API (:2019)
|
|
# and a 'caddy reload' aimed at gitea-caddy could be applied to this one instead
|
|
# — which would make it grab 80/443 and run ACME. Disabling admin keeps this
|
|
# process strictly a loopback static server on ${WEBSITE_PORT}.
|
|
{
|
|
admin off
|
|
}
|
|
|
|
:${WEBSITE_PORT} {
|
|
$(static_site_caddy_body)
|
|
}
|
|
EOF
|
|
log_ok "Wrote ${WEBSITE_COMPOSE} and ${WEBSITE_INNER_CADDY}."
|
|
}
|
|
|
|
# --- Phase 7: front the website on the root domain --------------------------
|
|
write_website_snippet() {
|
|
cat >"$WEBSITE_SNIPPET" <<EOF
|
|
# Managed by setup_personal_website.sh — do not edit by hand.
|
|
${WEBSITE_DOMAIN} {
|
|
reverse_proxy 127.0.0.1:${WEBSITE_PORT}
|
|
}
|
|
EOF
|
|
log_ok "Wrote ${WEBSITE_SNIPPET}."
|
|
}
|
|
|
|
# --- Phase 8: start the website container -----------------------------------
|
|
# Last HTTP status seen by wait_for_website, so a failure can say whether the
|
|
# server was unreachable or merely answering wrongly. Those are different bugs
|
|
# and the old message named only the first one.
|
|
WEBSITE_LAST_CODE=""
|
|
|
|
wait_for_website() {
|
|
local _
|
|
for _ in $(seq 1 30); do
|
|
WEBSITE_LAST_CODE="$(curl -s -o /dev/null -w '%{http_code}' \
|
|
--max-time 5 "http://127.0.0.1:${WEBSITE_PORT}/" || echo "000")"
|
|
if [[ "$WEBSITE_LAST_CODE" == "200" ]]; then
|
|
return 0
|
|
fi
|
|
sleep 1
|
|
done
|
|
return 1
|
|
}
|
|
|
|
start_website() {
|
|
log_info "Starting the personal-website container…"
|
|
# --force-recreate is required, not tidiness. Two failure modes need it,
|
|
# and compose cannot see either one:
|
|
#
|
|
# 1. The Caddyfile is a BIND MOUNT, so its contents are not part of the
|
|
# container's config hash. Editing it changes nothing compose compares,
|
|
# it reports "Running", and the new directives never load. A blog post
|
|
# served as the landing page is exactly this bug.
|
|
# 2. A bind mount is resolved once, at container start. Anything that
|
|
# replaces dist/ rather than emptying it (rm -rf, a git clean, moving
|
|
# the checkout) leaves the container mounted on the old, unlinked
|
|
# inode -- /srv reads as empty and every request 404s while the
|
|
# container still looks healthy.
|
|
#
|
|
# Recreating is under a second for a static file server, so it is cheaper
|
|
# than either failure being diagnosed by hand.
|
|
docker compose -f "$WEBSITE_COMPOSE" up -d --force-recreate
|
|
if wait_for_website; then
|
|
log_ok "Website answering on http://127.0.0.1:${WEBSITE_PORT}/."
|
|
else
|
|
if [[ "$WEBSITE_LAST_CODE" == "000" ]]; then
|
|
die "Website unreachable on 127.0.0.1:${WEBSITE_PORT} (no response)."
|
|
fi
|
|
die "Website on 127.0.0.1:${WEBSITE_PORT} answered HTTP ${WEBSITE_LAST_CODE}, not 200. \
|
|
A 404 here usually means the container's /srv bind mount is stale or ${WEBSITE_SRC}/dist is empty."
|
|
fi
|
|
}
|
|
|
|
# --- Phase 9: reload the edge -----------------------------------------------
|
|
reload_caddy() {
|
|
if docker exec "$CADDY_CONTAINER" caddy validate --config /etc/caddy/Caddyfile >/dev/null 2>&1; then
|
|
docker exec "$CADDY_CONTAINER" caddy reload --config /etc/caddy/Caddyfile
|
|
log_ok "Reloaded ${CADDY_CONTAINER}."
|
|
else
|
|
die "Caddy config invalid — aborting reload. Check ${SITES_DIR}/*.caddy."
|
|
fi
|
|
}
|
|
|
|
# --- Phase 10: firewall -----------------------------------------------------
|
|
ensure_firewall() {
|
|
log_info "Ensuring ports 80/443 are open (idempotent)…"
|
|
sudo bash "$WG_SCRIPT" allow-web
|
|
}
|
|
|
|
# --- Phase 11: report -------------------------------------------------------
|
|
print_report() {
|
|
cat <<EOF
|
|
|
|
============================================================================
|
|
Personal website deployed.
|
|
============================================================================
|
|
Website : https://${WEBSITE_DOMAIN}/
|
|
Gitea : https://${GITEA_DOMAIN}/ (moved from the root)
|
|
|
|
Gitea moved to a subdomain. Re-point any local clones/mirrors:
|
|
git remote set-url <name> https://${GITEA_DOMAIN}/<owner>/<repo>.git
|
|
|
|
Acceptance test (do this on your phone):
|
|
Turn Wi-Fi OFF (use cellular) and open https://${WEBSITE_DOMAIN}/ —
|
|
it should load over HTTPS with the hero, live GitHub projects, and CV,
|
|
and be usable at phone width.
|
|
============================================================================
|
|
EOF
|
|
}
|
|
|
|
setup_cmd() {
|
|
print_setup_header "Personal website setup"
|
|
preflight
|
|
migrate_caddy_snippets
|
|
relocate_gitea
|
|
build_cv_json
|
|
build_website
|
|
write_website_stack
|
|
write_website_snippet
|
|
start_website
|
|
reload_caddy
|
|
ensure_firewall
|
|
print_report
|
|
deploy_admin_service
|
|
log_ok "Personal website setup complete."
|
|
}
|
|
|
|
# --- status: self-diagnose --------------------------------------------------
|
|
status_line() {
|
|
if [[ $1 -eq 0 ]]; then
|
|
log_ok "$2"
|
|
else
|
|
log_warn "$2"
|
|
fi
|
|
}
|
|
|
|
check_http() {
|
|
# Prints 0 to stdout if the URL returns 200, else 1.
|
|
local url="$1" code
|
|
code="$(curl -sk -o /dev/null -w '%{http_code}' --max-time 10 "$url" 2>/dev/null || echo 000)"
|
|
[[ $code == "200" ]] && echo 0 || echo 1
|
|
}
|
|
|
|
status_cmd() {
|
|
print_setup_header "Personal website status"
|
|
admin_status_lines
|
|
|
|
if has_cmd node; then
|
|
status_line 0 "node present"
|
|
else
|
|
status_line 1 "node missing"
|
|
fi
|
|
if has_cmd pnpm; then
|
|
status_line 0 "pnpm present"
|
|
else
|
|
status_line 1 "pnpm missing"
|
|
fi
|
|
if [[ -f "${WEBSITE_SRC}/dist/index.html" ]]; then
|
|
status_line 0 "build present (dist/index.html)"
|
|
else
|
|
status_line 1 "build missing — run setup"
|
|
fi
|
|
if docker ps --format '{{.Names}}' | grep -qx personal-website; then
|
|
status_line 0 "personal-website container running"
|
|
else
|
|
status_line 1 "personal-website container not running"
|
|
fi
|
|
|
|
status_line "$(check_http "http://127.0.0.1:${WEBSITE_PORT}/")" \
|
|
"local static server (127.0.0.1:${WEBSITE_PORT})"
|
|
|
|
if [[ -f $WEBSITE_SNIPPET ]]; then
|
|
status_line 0 "Caddy website snippet present"
|
|
else
|
|
status_line 1 "Caddy website snippet missing"
|
|
fi
|
|
if grep -qE '^[[:space:]]*import[[:space:]]+/etc/caddy/sites' "$CADDYFILE" 2>/dev/null; then
|
|
status_line 0 "root Caddyfile uses snippet imports"
|
|
else
|
|
status_line 1 "root Caddyfile not migrated to imports"
|
|
fi
|
|
if getent hosts "$WEBSITE_DOMAIN" >/dev/null; then
|
|
status_line 0 "${WEBSITE_DOMAIN} resolves"
|
|
else
|
|
status_line 1 "${WEBSITE_DOMAIN} does not resolve"
|
|
fi
|
|
if getent hosts "$GITEA_DOMAIN" >/dev/null; then
|
|
status_line 0 "${GITEA_DOMAIN} resolves"
|
|
else
|
|
status_line 1 "${GITEA_DOMAIN} does not resolve"
|
|
fi
|
|
status_line "$(check_http "https://${WEBSITE_DOMAIN}/")" \
|
|
"external https://${WEBSITE_DOMAIN}/"
|
|
status_line "$(check_http "https://${GITEA_DOMAIN}/")" \
|
|
"external https://${GITEA_DOMAIN}/"
|
|
}
|