refactor(dev): convert dev-* recipes to Docker Compose #115
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/dev-recipes-to-compose"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Move every
dev-*recipe onto Docker Compose, with a 1:1 mapping from recipe name to compose file name. Fixes the panic-invisible footgun indev/server-up(baredocker run --rm --detachswept the container off the moment the server panicked, leaving the operator with no logs - the SQLiteCANTOPENregression on/datawas hidden behind it).just dev-webcompose.dev-web.ymlci-build/Dockerfile.webbehind Traefikjust dev-web-saascompose.dev-web-saas.ymlBUILD_MODE=saasjust dev-web-localcompose.dev-web-local.ymlcargo runfrom source on127.0.0.1:18080just dev-web-local-saascompose.dev-web-local-saas.yml--no-default-features --features saasjust dev-desktopcompose.dev-desktop.ymlEach start recipe gets a matching
-down, and where data volumes exist, a-cleanrecipe.Why this fixes "the server doesn't start"
Compose's default keeps the container around after exit, so
docker compose logs appalways has the real error anddocker compose downis the explicit teardown. The baredocker run --rm --detachindev/server-upswept the container off the moment the server panicked, leaving the operator with no logs.The
compose.dev-web-local{,-saas}.ymlandcompose.dev-desktop.ymlfiles also mirrordev/cargo's chown-then-setpriv pattern: the container starts as root, idempotently chowns the named volumes to$HOST_UID:$HOST_GID(passed in by the just recipe fromid -u/id -g), thensetpriv --reuid --regid --clear-groupsdrops to the host user before exec'ing cargo. That's what unblocks the SQLiteCANTOPENon a freshdatavolume.Compose interpolation gotcha
Bash variable references in the inline entrypoints have to be written as
$$VARso they survive Compose's own interpolation pass and reach bash literal at container runtime. Without the escape,for d in ...; do ... "$d" ...printsstat: cannot statx '': No such file or directorybecause Compose ate$dfirst. Comment in each of the three local-* / desktop compose files points at compose.dev-web-local.yml as the canonical reference.Cleanup
compose.dev.ymlremoved (renamed-with-BUILD_MODE-pinned successors arecompose.dev-web.yml+compose.dev-web-saas.yml).dev/server-logsremoved (unused;docker compose --file <file> logs --followreplaces it).dev/server-upanddev/server-downare kept becausejust verify(in the[test]group, out of scope here) still uses them. Converting verify is a clean follow-up.Test plan
just dev-web-localend-to-end: compiles the server insideghcr.io/niceguyit/rust-builder-glibc:v1.0.0-rust1.94-trixie, chowns the newdatavolume, drops to host uid via setpriv, the server starts, andcurl http://127.0.0.1:18080/loginreturns 200.just dev-web-local-downremoves the container cleanly.docker compose --file <file> config --quietparses cleanly on all five new compose files.just dev-webagainst a fresh checkout still builds the production image and is reachable athttps://${USER}-chat.a8n.runthrough the Traefik network.just dev-desktopagainst a runningdev-web-localopens the lets-chat window on the host's desktop (X11 or Wayland).just dev-web-saasproduces the saas binary and serves at the same Traefik host without colliding with a paralleldev-webbecause the compose project names are now distinct.Migration note
Existing developers have data in the
chat-${USER}_lets-chat-datavolume from the oldcompose.dev.ymlproject name. The newdev-web-${USER}project creates a freshdev-web-${USER}_lets-chat-data, so the old dev data is orphaned (not deleted -docker volume ls | grep chat-still shows it). If you want it back,docker run --rm -v chat-${USER}_lets-chat-data:/old -v dev-web-${USER}_lets-chat-data:/new alpine cp -a /old/. /new/moves it over. If you don't care (dev data is usually throwaway), no action needed.The dev/* tooling was a mix: `just dev-web` / `dev-web-saas` already ran via Compose against `compose.dev.yml`, but `dev-web-local` / `dev-web-local-saas` shelled out to `dev/server-up` (bare `docker run --rm --detach`) and `dev-desktop` shelled out to `dev/cargo-desktop run`. The bare `docker run` path silently swept the container off the moment the server panicked at startup, leaving the operator with no logs - the recent SQLITE_CANTOPEN regression on `/data` was hidden behind this until I dug through `docker inspect` to find that the container was already gone. Move every `dev-*` recipe onto Compose, with a 1:1 mapping from recipe name to file name so the relationship is obvious without having to grep the justfile: | Just recipe | Compose file | |--------------------------|------------------------------------| | `just dev-web` | `compose.dev-web.yml` | | `just dev-web-saas` | `compose.dev-web-saas.yml` | | `just dev-web-local` | `compose.dev-web-local.yml` | | `just dev-web-local-saas`| `compose.dev-web-local-saas.yml` | | `just dev-desktop` | `compose.dev-desktop.yml` | Compose's default behaviour (no `--rm` on the service container) keeps the container around after exit, so `docker compose logs app` always has the real error and `docker compose down` is the explicit teardown - directly addressing the panic-invisible footgun from `dev/server-up`. Every recipe gets a matching `-down` and (where data volumes exist) a `-clean` recipe. ### Compose file shapes `compose.dev-web.yml` and `compose.dev-web-saas.yml` are the renames-with-BUILD_MODE-pinned successors to `compose.dev.yml`: each builds the production multi-stage `ci-build/Dockerfile.web` with the right `BUILD_MODE` arg, attaches to the external `network-traefik-public`, carries the Traefik labels for `${USER}-chat.a8n.run`, and binds the `lets-chat-data` volume. Keeping them as two separate files (instead of one parameterised file as before) is what gives `just dev-web` and `just dev-web-saas` distinct compose project names so both can run simultaneously without colliding on `container_name`. `compose.dev-web-local{,-saas}.yml` are the compose equivalents of the deleted `dev/server-up`. They run `cargo run -p lets-chat-server` (the saas variant adds `--no-default-features --features saas`) inside `ghcr.io/niceguyit/rust-builder-glibc:v1.0.0-rust1.94-trixie`, bind-mount the repo at `/work`, persist `/usr/local/cargo/{registry,git}`, `/cargo-target`, and `/data` as named volumes, and publish container port 8080 on `127.0.0.1:18080`. An inline entrypoint mirrors `dev/cargo`'s chown-then-setpriv dance: container starts as root, idempotently chowns the named volumes to `$HOST_UID:$HOST_GID` (passed in by the just recipe from `id -u` / `id -g`), then `setpriv --reuid --regid --clear-groups` drops to the host user before exec'ing cargo. This is what unblocks the SQLite `CANTOPEN` you'd hit yesterday on a fresh `lets-chat-rewrite-data` volume. `compose.dev-desktop.yml` shares the same cargo volumes as the local-server files (so a `cargo` triggered by either picks up the other's compiled deps) and adds the X11/Wayland forwarding `dev/cargo-desktop` previously did imperatively in bash: `network_mode: host`, `ipc: host`, env-forwarded `DISPLAY` / `WAYLAND_DISPLAY` / `XDG_RUNTIME_DIR`, and bind-mounted `/tmp/.X11-unix` + `$XDG_RUNTIME_DIR`. Defaults assume a typical Linux desktop (uid 1000, DISPLAY=:0, /run/user/1000); users on non-standard setups can override any of them via their shell env. ### Compose interpolation gotcha Bash variable references in inline entrypoints have to be written as `$$VAR` so they survive Compose's own interpolation pass and reach bash literal at container runtime. Got bit by this on the first smoke run - the `for d in ...; do ... "$d" ...` loop printed `stat: cannot statx '': No such file or directory` because Compose ate `$d` before bash ever saw it. All three local entrypoints carry a one-line comment pointing at compose.dev-web-local.yml as the canonical example. ### Cleanup - `compose.dev.yml` deleted (superseded by the two `compose.dev-web*.yml` files). - `dev/server-logs` deleted (unused; `docker compose --file compose.dev-web-local.yml logs --follow` replaces it). - `dev/server-up` and `dev/server-down` are kept because `just verify` still uses them. Converting verify is a clean follow-up - it's in the `[test]` group not `[dev]`, so out of scope here. ### Verification `just dev-web-local` smoke tested end to end: `cargo run` compiles the server inside the builder image, the entrypoint chowns the new `data` volume to the host uid, setpriv drops to that uid, the server starts, and `curl http://127.0.0.1:18080/login` returns 200. `docker compose --file <file> config --quiet` parses cleanly on all five new compose files. `just dev-web-local-down` removes the container cleanly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>