Skip to content

Run all nine examples at once with docker compose - #12

Merged
evsinev merged 6 commits into
mainfrom
feat/docker-compose
Sep 11, 2026
Merged

evsinev merged 6 commits into
mainfrom
feat/docker-compose

Conversation

@evsinev

@evsinev evsinev commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

docker compose up --build builds all nine examples into their own images and runs them behind
one nginx on http://localhost:8080/. That is the whole prerequisite list — no Go, Node, PHP,
Python, Ruby, JDK, Rust, .NET or yarn to install.

The part that is worth more than the convenience

Every example ships a deploy/nginx.conf, they are written to "share a single server block", they
go out in the release archives and the READMEs tell people to install them — and not one of them
had ever been executed
. This mounts all nine unchanged and routes the demo through them.

It found two real bugs on the first run. Both are fixed here:

  • nextjs/deploy/nginx.conf redirected into an infinite loop. Next normalises the opposite way
    from the other eight: with trailingSlash at its default, {prefix}/ is answered with a 308 to
    {prefix}. The snippet redirected {prefix} to {prefix}/, so following it bounces forever.
    Anyone who installed that file as documented got a payment page that never loads. Separate
    commit, a32a5b0.

  • rust-axum-js did not build on the Rust it claims. src/main.rs used a let chain, which
    is unstable before 1.88, while Cargo.toml says rust-version = "1.85" and four documents say
    "Rust 1.85 or newer". Fixed the code rather than the claim — the two ifs are nested now, so
    the advertised minimum stays 1.85 and nobody's toolchain has to move. Commit bb2cdc0.

    The reason CI missed it is fixed too (060ac2f): the rust job now builds once more on the
    version Cargo.toml declares, after everything else has run on stable. Verified on the
    runner — the job compiles under rustc 1.98.1 and again under rustc 1.85.1.

How it is wired

Every app service joins the nginx container's network namespace:

x-app: &app
  network_mode: "service:nginx"

so the snippets' own proxy_pass http://127.0.0.1:300x is simply true, and they are mounted
read-only exactly as they ship. The ordinary alternative — service names and
LISTEN_ADDR=0.0.0.0 — would have meant nine second copies drifting quietly away from the
originals, which is the one thing shared/ exists to prevent. It also keeps each app's shipped
LISTEN_ADDR=127.0.0.1 default, needs no DNS in nginx, and leaves ports 3000-3008 free.

The price, documented in the file: a shared namespace forbids ports/networks/hostname, and
docker compose restart does not work — restart the stack, not one service.

Two apps need more than an environment variable, both for reasons already in their own CLAUDE.md:
php-js gets a compose pool because the shipped one sets clear_env = yes and names every setting
as an env[] line, and nextjs needs BASE_PATH as a build argument because next build bakes
basePath in.

One root .env and one private_key.pem (PKCS#8) feed all nine. PUBLIC_URL is the nginx origin;
each app appends its own BASE_PATH.

Verified, against the QA gateway

All nine, through nginx:

go / nodejs-express-js / php / python / ruby / java / rust / dotnet    bare=301  page=200
nextjs                                                                bare=200  page=200
  • config.js is no-store and carries a live ephemeralTicket from the gateway in all eight that
    have one — so the OAuth signature, the credentials and the gateway call all work in every image.
    nextjs has no config.js by design and carries the ticket in its page instead.
  • The payment page is byte-identical to shared/views/checkout.html through all eight prefixes,
    and styles.css is byte-identical through all nine.
  • A forged control in a /result query is 403 in all nine; /result with no query serves.
  • The optional alias blocks in the express, python and ruby snippets really do serve those four
    assets off disk — they come back with Expires, the other six do not.
  • docker/nginx/index.html at / lists the nine.

This is the first time the .NET example has been started anywhere.

Not done, on purpose

  • Images are not built in CI. Nine builds per push to re-check what nine existing jobs already
    check. Easy to add later as its own workflow.
  • e2e-tests/ is untouched. Pointing it at containers needs the emulator behind the same nginx
    so API_URL and SDK_URL keep one origin, plus a rework of apps.ts and
    playwright.config.ts. Separate task.

One more inconsistency found, and fixed

dotnet-aspnetcore-js answered 405 to HEAD where the other eight answer it like a GET —
minimal APIs' MapGet maps exactly the verb named. Every GET route now goes through a one-line
Get helper that is MapMethods(…, ["GET", "HEAD"], …); Kestrel drops the body itself, so the
handlers are untouched. HEAD is 200 across all nine now. Commit 330e9fd.

Cost

Adding a language now also costs a Dockerfile, a .dockerignore, a compose service and a mount
line. That is written into CLAUDE.md beside the existing "one line in sync-shared.sh" rule
rather than left to be discovered.

@evsinev
evsinev merged commit 5deeddc into main Sep 11, 2026
20 checks passed
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