Skip to content

v2 line: root takeover (compose-first) + release plumbing - #54

Open
thomasboni wants to merge 1 commit into
mainfrom
release-v2-prep
Open

v2 line: root takeover (compose-first) + release plumbing#54
thomasboni wants to merge 1 commit into
mainfrom
release-v2-prep

Conversation

@thomasboni

@thomasboni thomasboni commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Layout ruling: v2 takes over the repository root; v1 (Helm chart, Docker Compose with bundled
Traefik, podman, install.sh) moves into legacy-v1/ unchanged (see legacy-v1/README.md).
v2 is compose-first: a minimal root compose.yml (backend, frontend, postgres:18-alpine,
redis:7-alpine, no build: sections, named volumes, healthchecks) plus .env.example and a new
quick-start README.md. A v2 Helm chart is follow-up work, not in this PR - the README points at
legacy-v1/charts as the closest reference in the meantime.

What moved into legacy-v1/ (git-mv, byte-identical content): compose.yml,
compose.local.yml, .env.example, .env.local.example, configmap.yml.example,
configmap.local.yml.example, podman.yml.example, podman.local.yml.example, .docker/,
charts/, install.sh, scripts/, versions.env. .github/workflows/ci.yml and
.github/workflows/release.yml (v1's own Helm lint/chart-release pipeline) had their paths to
scripts//charts/ updated to the new legacy-v1/ location - no other change to either workflow,
v1's own CI/release behavior is otherwise untouched.

New at root: compose.yml, .env.example (PLATFORM_VERSION=v2.0.0 as the first line - this
is the exact contract the monorepo's release.yml sed step targets, do not rename that variable on
either side without updating the other), README.md (quick start: configure, docker compose up -d, plumber-bootstrap, then log in; the upgrade story; the legacy-v1 pointer).

New: .github/workflows/publish-release.yml - on a push to main where latest.json
changed, it reads the version, tags it, and runs gh release create with releases/<version>.md
as the notes file. This reacts to Thomas merging the monorepo's own release PR (which is the one
that bumps .env.example/latest.json/releases/<version>.md) - it never opens or approves
anything itself. releases/.gitkeep added so the directory exists ahead of the first release PR.

One deliberate deviation from the brief's literal wording: hardening comments in
compose.yml/.env.example are written self-contained rather than pointing at
docs/ops-notes.md - that file lives in the private getplumber/monorepo, unreachable from this
public repo, so a literal path reference would be broken/confusing for an external operator.

One thing this PR does NOT solve, flagged for review: compose.yml exposes backend and
frontend on two separate host ports; the frontend's browser-side code calls the API at the same
origin under /api with no client config (platform/frontend's own env.ts), which needs a
reverse proxy in front routing /api/* to backend and everything else to frontend on one public
hostname. This compose file deliberately does not bundle one (kept minimal, no Traefik) - both the
compose.yml comment on the frontend service and the README call this out explicitly so it isn't
silently missed at install time.

This PR is the prerequisite for the first v2.0.0 release run: the monorepo's on-demand
release.yml workflow opens its own release PR against this repo's .env.example/latest.json
using the exact paths this PR establishes.

Test plan

  • docker compose --env-file <filled .env> -f compose.yml config resolves cleanly (verified
    locally with placeholder secrets - all four services, healthchecks, and env interpolation
    resolve as expected).
  • YAML-valid: compose.yml, .github/workflows/publish-release.yml, and the edited
    ci.yml/release.yml all parse.
  • Manual: run docker compose up -d against real images once v2.0.0 is published, confirm
    plumber-bootstrap flags match cmd/bootstrap's current flag set (verified against source,
    not run against a live image in this PR).
  • Confirm legacy-v1/'s CI (ci.yml chart lint/unit tests, release.yml chart-releaser) still
    passes from its new legacy-v1/charts path.

Move everything v1 (compose.yml, compose.local.yml, .env.example,
.env.local.example, configmap/podman examples, .docker/, charts/,
install.sh, scripts/, versions.env) into legacy-v1/ unchanged. Author the
v2 root compose.yml (backend/frontend/postgres:18-alpine/redis:7-alpine,
no build: sections, named volumes, healthchecks), .env.example
(PLATFORM_VERSION=v2.0.0 first line per the release workflow's sed
contract), and README.md (v2 quick start + bootstrap + upgrade story +
legacy-v1 pointer). Add .github/workflows/publish-release.yml, which tags
and creates the GitHub Release once the monorepo's release PR (bumping
latest.json) is merged to main. Update ci.yml/release.yml's chart paths
so v1's own Helm lint/release pipeline keeps working from its new location.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUFtr3m8zC2mVNKVTQvaNz
@thomasboni

Copy link
Copy Markdown
Contributor Author

Field test report: this branch was deployed today on demo.getplumber.tech (v1 stopped and moved aside, images built from monorepo main 7a5f5073 and docker-loaded as v2.0.0-demo, BYO reverse proxy realized as Traefik+LE in a compose.override.yml). The stack is up and serving. Three compose.yml findings from that install, all worked around in the override on the box:

  1. Backend healthcheck cannot ever pass against the distroless image (compose.yml backend service): ["CMD", "wget", ...] needs wget in the container, but since monorepo #255 the backend image is distroless static - no wget, no shell. The frontend's depends_on: condition: service_healthy then wedges the whole stack. Options: drop the in-container healthcheck (external monitoring hits /healthz//readyz through the proxy), switch to k8s-style out-of-container probes, or add a tiny healthcheck subcommand to plumber-backend and exec-probe the binary itself. Demo workaround: healthcheck disabled, frontend dependency relaxed to service_started.

  2. Frontend healthcheck is always unhealthy as written: busybox wget resolves localhost to [::1] first and Next.js standalone binds IPv4 0.0.0.0 only, so the probe gets connection-refused forever. Side effect beyond compose: Traefik's docker provider refuses to route to an unhealthy container, so the whole site 404s. Fix: probe http://127.0.0.1:3000/ instead of localhost (same trap would apply to any backend probe).

  3. postgres:18 + postgres_data:/var/lib/postgresql/data crash-loops on first boot: the postgres 18+ images refuse a mount at .../data ("there appears to be PostgreSQL data in: /var/lib/postgresql/data (unused mount/volume)") - they want the single mount one level up (/var/lib/postgresql) so pg_upgrade --link can cross versions; legacy-v1's compose already does this with an explicit PGDATA. Note the CI smoke can't catch this: deploy/smoke/compose.yml runs postgres with no volume at all.

Everything else held up: docker compose exec backend plumber-bootstrap works as documented against the distroless image (OAuth app configured, secret sealed, login hint printed), migrations ran in-process on first boot, readyz/healthz answer, and the auth boundary gives 401 on /api/v1/version.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LrKoq78WJL2zhtmpEBErwx

@thomasboni

Copy link
Copy Markdown
Contributor Author

One more field finding (Thomas, 2026-09-08): the frontend needs custom CA support too. It makes server-side queries (Next.js server components call the backend via API_INTERNAL_URL today, and any future server-side fetch to a privately-signed origin has the same problem), so an on-prem install behind an internal CA needs the frontend's Node runtime to trust it - Node does not read the OS trust store. The v1 compose already did this by mounting .docker/ca-certificates into BOTH containers.

Suggested v2 shape, mirroring the backend's PLUMBER_PROVIDER_CA_BUNDLE pattern in compose.yml: a commented NODE_EXTRA_CA_CERTS: /etc/plumber/ca-bundle.pem env on the frontend service plus a commented paired volumes: entry mounting the operator's PEM. NODE_EXTRA_CA_CERTS is read once at Node startup and appends to the built-in roots, so public-CA setups keep working - no image change needed, the stock next-server picks it up.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LrKoq78WJL2zhtmpEBErwx

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant