@@ -321,10 +321,18 @@ liveness because the worker has no HTTP listener.
321321
322322## Bootstrap And Migrations
323323
324- Postgres migrations are applied by ` rend-api ` through SQLx when
325- ` REND_API_AUTO_MIGRATE=true ` . In local Compose, the worker waits for the API and
326- sets ` REND_API_AUTO_MIGRATE=false ` to avoid duplicate startup migration work.
327- For production, deploy or run the API migration step before starting workers.
324+ Postgres migrations are applied through the explicit one-shot
325+ ` rend-api migrate ` command. Local Compose still sets
326+ ` REND_API_AUTO_MIGRATE=true ` on the local API for developer convenience and sets
327+ the worker to ` false ` to avoid duplicate startup migration work. Production
328+ serving API and worker containers must use ` REND_API_AUTO_MIGRATE=false ` ; the
329+ control-plane deploy helper runs the candidate image's ` rend-api-migrate `
330+ service before any Caddy traffic promotion.
331+
332+ Treat production Postgres migrations as expand/contract and rollback-hostile
333+ unless a tested rollback migration exists. A failed pre-promotion candidate
334+ keeps the old API slot serving, but an already-applied schema migration is not
335+ automatically reverted.
328336
329337ClickHouse schema is applied by the local ` clickhouse-init ` one-shot service on
330338every Compose startup. The schema uses ` CREATE DATABASE IF NOT EXISTS ` and
@@ -370,18 +378,20 @@ scripts/preflight-edge-host.sh \
370378 --manifest .rend/releases/production-001.json
371379```
372380
373- The control-plane preflight checks Docker/Compose, compose/env files, manifest
374- digest refs, manifest platform metadata, manifest image pull readiness, pulled
375- image OS/architecture, managed dependency connectivity where local tools allow
376- it, and host bind ports. The edge preflight checks Docker/Compose, edge env,
381+ The control-plane preflight checks Docker/Compose, compose/env files, Caddy
382+ upstream template wiring, manifest digest refs, manifest platform metadata,
383+ manifest image pull readiness, pulled image OS/architecture, and managed
384+ dependency connectivity where local tools allow it. The active blue/green API
385+ slot is expected to keep one private port bound, so control-plane preflight does
386+ not require API ports to be free. The edge preflight checks Docker/Compose, edge env,
377387manifest digest ref, manifest platform metadata, manifest image pull readiness,
378388pulled image OS/architecture,
379389private-by-default direct port publishing, uid/gid ` 10001 ` cache and spool
380390writeability, object-store health, control-plane register/heartbeat
381391reachability, telemetry ingest reachability, and host bind ports.
382392
383- Use deploy helpers in dry-run mode first to print the exact Compose commands
384- with manifest image refs:
393+ Use deploy helpers in dry-run mode first to print the exact Compose/Caddy
394+ transaction with manifest image refs:
385395
386396``` sh
387397scripts/deploy-control-plane-host.sh \
@@ -393,6 +403,44 @@ scripts/deploy-edge-host.sh \
393403 --dry-run
394404```
395405
406+ The control-plane helper is transactional on the host. It takes
407+ ` /var/lock/rend-control-plane-deploy.lock ` , records active/previous slot state
408+ under ` /var/lib/rend/control-plane ` , runs the one-shot migration, recreates only
409+ the inactive API slot (` rend-api-blue ` or ` rend-api-green ` ), probes candidate
410+ ` /readyz ` and ` /healthz ` directly, then atomically replaces
411+ ` /etc/caddy/rend-control-plane-upstream.caddy ` and reloads Caddy. If promotion
412+ or post-promotion checks fail, it restores the previous upstream. The previous
413+ API slot remains running after a successful promotion for immediate rollback.
414+ When invoked by ` scripts/deploy-release-over-ssh.sh ` , the control-plane
415+ transaction runs under ` sudo systemd-run --wait --collect --pipe ` so rollback can
416+ continue on the host if the GitHub runner or SSH session dies after the unit is
417+ started.
418+
419+ The SSH wrapper also bootstraps production host files before preflight: it
420+ installs the current control-plane Compose template, patches an existing
421+ concrete Caddyfile to use the managed upstream snippet, creates
422+ ` /etc/caddy/rend-control-plane-upstream.caddy ` only when missing, removes
423+ legacy ` admin off ` Caddy settings, and preserves an existing upstream target.
424+ The bootstrap reloads Caddy while the upstream still points at the current slot;
425+ if an older running config cannot reload because admin was disabled, it performs
426+ one restart before the transaction starts so later blue/green promotions can use
427+ normal Caddy reloads. The managed upstream snippet must stay ` 0644 ` ; preflight
428+ fails if the file is root-only because the ` caddy ` service user imports it on
429+ reload.
430+
431+ The edge helper remains an in-place per-host deploy. For production, deploy
432+ edges serially and keep at least one edge serving while the other updates, then
433+ run the multi-edge verifier/readiness gate. A future edge hardening pass should
434+ mirror the control-plane slot model: ` rend-edge-blue ` /` rend-edge-green ` , private
435+ candidate probes, a managed Caddy upstream snippet, and automatic rollback on
436+ post-promotion failures.
437+
438+ The production workflow derives each edge host's ` REND_EDGE_ID ` ,
439+ ` REND_EDGE_REGION ` , ` REND_EDGE_BASE_URL ` , and shared ` REND_EXPECTED_EDGES ` from
440+ ` REND_READINESS_EDGES ` before restarting that edge. Keep those entries aligned
441+ with the intended registry IDs; the verifier treats the registry as authoritative
442+ after deploy.
443+
396444After deploy, verify the first-host path:
397445
398446``` sh
@@ -528,24 +576,46 @@ usage or watch accounting.
5285765 . Run ` scripts/validate-production-env.sh ` and the relevant preflight script on
529577 each host.
5305786 . Run the deploy helper with ` --dry-run ` , then run it without ` --dry-run ` .
531- 7 . Deploy ` rend-api ` with ` REND_API_AUTO_MIGRATE=true ` for the migration step.
532- 8 . Start ` rend-api ` serving traffic after ` /readyz ` passes.
579+ 7 . On the control-plane host, let ` scripts/deploy-control-plane-host.sh ` run the
580+ candidate image's one-shot ` rend-api migrate ` service.
581+ 8 . Let the control-plane helper start the inactive API slot, verify private
582+ ` /readyz ` and ` /healthz ` , then promote Caddy to the candidate slot.
5335839 . Start ` rend-edge ` nodes with unique ` REND_EDGE_ID ` , ` REND_EDGE_REGION ` ,
534584 API-reachable ` REND_EDGE_BASE_URL ` , cache volume, and telemetry spool volume.
535- 10 . Start ` rend-media-worker ` with ` REND_API_AUTO_MIGRATE=false ` .
585+ 10 . Start or update ` rend-media-worker ` with ` REND_API_AUTO_MIGRATE=false ` .
53658611 . Run ` scripts/verify-first-host-deploy.sh ` with a provided ` hls_ready ` asset
537587 to confirm edge registration, signed playback, and telemetry analytics.
53858812 . Run ` bun run playback:readiness -- --target configured --skip-local-stack `
539589 or pass ` --run-readiness-gate ` to the verifier before promoting traffic.
540590
591+ The production GitHub workflow runs the first-host verifier when
592+ ` run_first_host_verifier=true ` . It first verifies edge registry rows from the
593+ control-plane host with ` scripts/verify-edge-registry-over-ssh.sh ` , using the
594+ host's deployed ` /etc/rend/rend-api.env ` instead of a separate GitHub
595+ ` DATABASE_URL ` . It then rewrites private edge targets through SSH tunnels before
596+ running API/edge health, ClickHouse, and public deny checks with
597+ ` scripts/verify-first-host-deploy.sh --skip-registration ` .
598+ When ` REND_VERIFY_ASSET_ID ` or ` verify_asset_id ` points at an existing
599+ synthetic/non-customer ` hls_ready ` asset, the verifier also runs warmed
600+ playback and analytics checks. If no asset id is configured, the workflow runs
601+ the verifier with ` --skip-playback ` and relies on the synthetic playback
602+ readiness gate for upload, playback, and telemetry proof.
603+
541604## Rollback Basics
542605
543606Roll back services in dependency order from the edge inward:
544607
5456081 . Roll back ` rend-edge ` first if playback cache behavior regresses.
5466092 . Roll back ` rend-media-worker ` if artifact generation or warming regresses.
547- 3 . Roll back ` rend-api ` last. Treat Postgres migrations as forward-only unless a
548- tested rollback migration exists.
610+ 3 . Roll back ` rend-api ` last. For the control plane, prefer
611+ ` scripts/deploy-control-plane-host.sh --rollback ` to switch Caddy back to the
612+ previous slot without pulling or rebuilding. Treat Postgres migrations as
613+ forward-only unless a tested rollback migration exists.
614+
615+ For a production rollback drill in GitHub Actions, run the workflow manually
616+ with ` verify_control_plane_rollback=true ` . The workflow switches Caddy back to
617+ the previous control-plane slot, verifies public ` /readyz ` , then deploys the
618+ current digest manifest again to re-promote the candidate slot.
549619
550620Edge cache can be purged or discarded during rollback. Telemetry spool files can
551621be retained for replay or deleted if the ingest contract changed incompatibly.
@@ -562,3 +632,14 @@ US East and London edge nodes differ only by environment and attached volumes:
562632- local telemetry spool volume
563633
564634The same ` rend-edge ` image and command run in both regions.
635+
636+ ## Residual SPOFs
637+
638+ The blue/green control-plane transaction prevents a failed deploy, failed
639+ candidate, failed Caddy reload, or failed post-promotion check from taking down
640+ the currently serving API process. It does not remove single-host or
641+ single-daemon failure modes. A kernel panic, VM outage, host network loss, disk
642+ failure, Docker daemon failure, or Caddy process failure on the control-plane
643+ host can still cause downtime. The current edge model is resilient only at the
644+ multi-edge operational level; each individual edge host still updates
645+ ` rend-edge ` in place.
0 commit comments