feat(dev): self-contained dev container, and setup docs to match - #3039
Open
cqnykamp wants to merge 8 commits into
Open
feat(dev): self-contained dev container, and setup docs to match#3039cqnykamp wants to merge 8 commits into
cqnykamp wants to merge 8 commits into
Conversation
The existing .devcontainer was left over from the PHP/Apache era: it built apache and php services and mounted paths like ./public/api that no longer exist, so it could not start. Replace it with one that matches the current monorepo — Node pinned to .nvmrc, MySQL, an S3 mock, and Chrome, so the API, both frontends, and all three test suites run without anything on the host except Docker. The dev servers are published on the host's loopback and Vite/Astro bind to all interfaces via DEV_SERVER_HOST, so the app is reachable in a browser without an editor's port forwarding. DEV_SERVER_HOST is unset on a normal checkout, which leaves host-based development on localhost as before. Database and media addresses are supplied as environment variables rather than written into apps/api/.env. Both dotenv and the Prisma CLI leave already-set variables alone, so the container's values win while the checkout's .env — shared with the host through the bind mount — is never rewritten. Consequently dev-preflight now resolves the database host the same way, instead of assuming 127.0.0.1. node_modules directories are named volumes so the container's Linux-native installs never collide with the host's. The list is limited to the workspaces that npm actually populates, and initializeCommand pre-creates those mount points as the host user: Docker would otherwise create them as root, leaving directories in the checkout that a host-side npm ci cannot write to. Verified in the container: 408 api tests, 7 shared tests, Cypress e2e (including the s3mock image-upload path) and component specs, plus lint, format:check, and a full build. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LSJtzuUZ75ikxdZHXFPwu5
Setup lived entirely in the README, offered one path, and pointed `git clone` at the old DoenetTools repository. Follow the usual split instead: a short quick start in the README, the details in CONTRIBUTING.md, which GitHub also surfaces from issue and pull request pages. Both present the three environments easiest first — Codespaces, then the dev container, then installing the toolchain — so a newcomer needs a browser to get started and reads about Node and MySQL only if they want them. CONTRIBUTING.md gathers what a contributor needs beyond that first command: what runs on which port, how to run each test suite, the format and lint pass CI expects, and the fork and expand-migrate-contract conventions previously documented only in AGENTS.md. .devcontainer/README.md drops its getting-started half and keeps what is genuinely container-specific. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LSJtzuUZ75ikxdZHXFPwu5
Creating a codespace failed: on a 2-core machine s3mock, which is a JVM service, took longer than the healthcheck budget of 100s to answer, so the dev service's depends_on condition was never met and Codespaces fell back to a recovery container. MySQL was healthy in 27s; only s3mock timed out. Nothing needs s3mock until the first image upload, minutes after the container is created, so depend on it having started rather than being healthy. Both healthchecks also get a start_period and more retries, so a slow start on a small machine reports as starting rather than unhealthy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LSJtzuUZ75ikxdZHXFPwu5
The image is based on a slim Node image, which has no SSH server, so `gh codespace ssh`, `logs`, and `cp` all fail against a codespace built from it. Add the sshd dev container feature, which is the documented remedy. No effect on local use. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LSJtzuUZ75ikxdZHXFPwu5
Adds the CLI to the image and the VS Code extension to the recommended set, so `claude` is available in any terminal in the container. Credentials and settings live in a named volume, with CLAUDE_CONFIG_DIR pointing at it so the config file lands there too rather than in ~/.claude.json outside the volume. A login therefore survives a container rebuild — verified by recreating the dev service and confirming the directory's contents remain. ANTHROPIC_API_KEY is passed through from the host when set; Codespaces exposes a secret of that name automatically. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LSJtzuUZ75ikxdZHXFPwu5
Opening a forwarded port in a codespace could land on the API's debug root route, which renders "Express + TypeScript Serverundefined" — a confusing first impression when the app is what you want. The app proxies /api to the API and /blog to Astro, so it is the only port worth opening. Auto-open it, keep the other two labelled but silent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LSJtzuUZ75ikxdZHXFPwu5
Navigating from a blog page back to the app in a codespace went to localhost:8000, which is nothing in a browser pointed at *.app.github.dev. The blog builds absolute links from PUBLIC_APP_URL, and apps/web/.env ships the local default. Detect Codespaces in post-create and write apps/web/.env.local with the forwarded URLs. Vite and Astro load .env.local ahead of .env, the same mechanism `npm run setup` already uses to override these for worktrees. CODESPACES is in the container environment, but CODESPACE_NAME and the forwarding domain are only exported to login shells, so they are read from the file Codespaces writes rather than from the environment. Verified in a live codespace: the rendered header logo links to the forwarded app origin, and no localhost URLs remain in the blog HTML. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LSJtzuUZ75ikxdZHXFPwu5
In a codespace, both the magic-link email and the auto-login banner printed by `npm run dev` pointed at http://localhost:8000, which a browser on *.app.github.dev cannot follow — clicking sign-in produced an unusable link. The API already builds its links from APP_URL, so post-create now points that at the forwarded origin alongside the blog's PUBLIC_APP_URL. The dev banner hardcoded localhost instead, so it now reads the same value through a new appUrl export, which falls back to localhost on the current port exactly as before for a normal checkout. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LSJtzuUZ75ikxdZHXFPwu5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two commits: a working dev container, and a reorganization of the setup instructions around it.
The dev container
The existing
.devcontainer/was left over from the PHP/Apache era of DoenetTools — it builtapacheandphpservices and mounted paths like./public/apiand./docker/mysqlthat no longer exist, so it could not start at all. Replaced with one that matches the current monorepo.Everything needed to run and test the repo lives in the container; the only host requirement is Docker.
dev.nvmrc), Chrome + Cypress runtime libs, the workspacemysqls3mockadobe/s3mock, so image upload works — the same image CI usesWorks with Codespaces, VS Code, the
devcontainerCLI, or plaindocker compose.Notable decisions
The app is browsable without an editor. Vite and Astro bind to
127.0.0.1, which Docker's published ports cannot reach, so both configs now read an optionalDEV_SERVER_HOST. It is unset on a normal checkout — host-based development stays on localhost exactly as before — and the container sets it to0.0.0.0. Ports are published on the host's loopback only.apps/api/.envis never rewritten. The database and media addresses are supplied as environment variables in the compose file. Bothdotenvand the Prisma CLI leave already-set variables alone, so the container's values win while the checkout's.env— shared with the host through the bind mount — is left alone. That lets the same checkout be used on the host and in the container.dev-preflight.jsnow resolves the database host the same way instead of assuming127.0.0.1; behaviour on the host is unchanged, sinceDATABASE_HOSTislocalhostthere.node_modulesare named volumes, so the container's Linux-native installs never collide with the host's (which matters on macOS/arm64, given the platform-specificoptionalDependenciesinapps/apiandapps/app). The list is limited to the workspaces npm actually populates, andinitializeCommandpre-creates those mount points as the host user — Docker otherwise creates them as root, leaving directories in the checkout that a later host-sidenpm cicannot write to.The docs
Setup lived entirely in the README, offered one path, and pointed
git cloneat the oldDoenetToolsrepository. Now split the conventional way — a short quick start in the README, detail inCONTRIBUTING.md, which GitHub also surfaces from issue and pull request pages — with the three environments presented easiest first:CONTRIBUTING.mdalso gathers what a contributor needs past that first command: what runs on which port, how to run each test suite, the format/lint pass CI expects, and the fork and expand-migrate-contract conventions that were previously documented only inAGENTS.md..devcontainer/README.mdkeeps only what is genuinely container-specific.Verification
Run inside the container, on a fresh clone:
npm test --workspace @doenet-tools/api— 408 passed, 30 filesnpm test --workspace @doenet-tools/shared— 7 passedimageUpload.cy.ts— 2 passed, exercising browser → Vite → API → MySQL → s3mockChatConversation.cy.tsx— 3 passednpm run dev— api, app, and blog all serving; app and/blogreachable from a host browserformat:check,lint:check,build— cleanThe full setup path was verified twice end to end via
devcontainer upon fresh clones, and every internal doc link and anchor was checked.Note for reviewers
The Codespaces path is the one thing not verified end to end — launching a codespace bills someone's account, so it was reasoned about rather than run. The config uses no features Codespaces does not support, but the first person with quota should confirm it before we lean on that badge.
Only three suites were run as single specs rather than in full — enough to prove Chrome, the s3mock upload path, and both Cypress modes work in the container, without the full suite runtime. CI covers the rest.
On arm64 hosts Google ships no Chrome build, so Chromium is installed instead and Cypress needs
-b chromium; the package scripts hardcode-b chrome. Documented rather than changed, since changing them would affect CI.🤖 Generated with Claude Code