Skip to content

feat(dev): self-contained dev container, and setup docs to match - #3039

Open
cqnykamp wants to merge 8 commits into
Doenet:mainfrom
cqnykamp:devcontainer
Open

feat(dev): self-contained dev container, and setup docs to match#3039
cqnykamp wants to merge 8 commits into
Doenet:mainfrom
cqnykamp:devcontainer

Conversation

@cqnykamp

@cqnykamp cqnykamp commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

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 built apache and php services and mounted paths like ./public/api and ./docker/mysql that 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.

Service Purpose
dev Node 24.15.0 (matching .nvmrc), Chrome + Cypress runtime libs, the workspace
mysql MySQL 8.0, migrated and seeded by the post-create step
s3mock adobe/s3mock, so image upload works — the same image CI uses

Works with Codespaces, VS Code, the devcontainer CLI, or plain docker 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 optional DEV_SERVER_HOST. It is unset on a normal checkout — host-based development stays on localhost exactly as before — and the container sets it to 0.0.0.0. Ports are published on the host's loopback only.

apps/api/.env is never rewritten. The database and media addresses are supplied as environment variables in the compose file. 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 left alone. That lets the same checkout be used on the host and in the container. dev-preflight.js now resolves the database host the same way instead of assuming 127.0.0.1; behaviour on the host is unchanged, since DATABASE_HOST is localhost there.

node_modules are named volumes, so the container's Linux-native installs never collide with the host's (which matters on macOS/arm64, given the platform-specific optionalDependencies in apps/api and apps/app). The list is limited to the workspaces npm actually populates, and initializeCommand pre-creates those mount points as the host user — Docker otherwise creates them as root, leaving directories in the checkout that a later host-side npm ci cannot write to.

The docs

Setup lived entirely in the README, offered one path, and pointed git clone at the old DoenetTools repository. Now split the conventional way — a short quick start in the README, detail in CONTRIBUTING.md, which GitHub also surfaces from issue and pull request pages — with the three environments presented easiest first:

  1. GitHub Codespaces — a browser, one click
  2. Dev container locally — Docker, one command
  3. The toolchain on your machine — Node 24 + Docker, full control

CONTRIBUTING.md also 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 in AGENTS.md. .devcontainer/README.md keeps only what is genuinely container-specific.

Verification

Run inside the container, on a fresh clone:

  • npm test --workspace @doenet-tools/api — 408 passed, 30 files
  • npm test --workspace @doenet-tools/shared — 7 passed
  • Cypress e2e imageUpload.cy.ts — 2 passed, exercising browser → Vite → API → MySQL → s3mock
  • Cypress component ChatConversation.cy.tsx — 3 passed
  • npm run dev — api, app, and blog all serving; app and /blog reachable from a host browser
  • format:check, lint:check, build — clean

The full setup path was verified twice end to end via devcontainer up on 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

cqnykamp and others added 2 commits August 24, 2026 16:41
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
@cqnykamp cqnykamp changed the title feat(dev): self-contained dev container feat(dev): self-contained dev container, and setup docs to match Aug 24, 2026
cqnykamp and others added 6 commits August 24, 2026 17:15
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
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