fix(docker): give the development image the web source it compiles from

`docker compose -f docker-compose.dev.yml up` came up with a dead frontend:
the development stage's supervisor program runs `node scripts/dev.mjs`, and
that path does not exist in the image. The stage builds `FROM production`,
whose ./web is `.next/standalone/`, then cherry-picked three files on top.

Verified against a real standalone build — its root holds exactly
`server.js`, `package.json`, a traced `node_modules` and the dist dir. Every
source file and every config is absent, so `scripts/` was only the first
thing to fail; `tsconfig.json`, `tailwind.config.js`, `postcss.config.js`,
`features/`, `types/` and `proxy.ts` were missing too, and the compose mount
list covers none of them either.

So take the builder's web tree wholesale instead of naming files that keep
drifting as web/ grows top-level entries. `--chown` during the copy keeps
node_modules from being re-layered, and the inherited production build is
cleared so `next dev` starts from an empty cache rather than a half-valid
one.

#906 also reported this as two `[program:frontend]` blocks in one supervisor
config, with the fix being to delete the dev.mjs one. That reading is wrong:
the blocks are in different build stages (`FROM production AS development`),
the production image has exactly one and is unaffected, and deleting the dev
block would have made the development image serve a production build with no
hot reload. The production stage is untouched here.

Not build-verified — no Docker daemon available on this machine.
This commit is contained in:
Bingxi Zhao (Frank)
2026-08-21 16:29:06 +08:00
parent c4a6f1b228
commit 083ff4fe3d
2 changed files with 18 additions and 7 deletions
+14 -6
View File
@@ -443,16 +443,24 @@ ENTRYPOINT ["/app/entrypoint.sh"]
# ============================================
FROM production AS development
# Re-add full node_modules for development hot-reload
# (Production uses standalone output which doesn't include full node_modules)
COPY --from=frontend-builder /app/web/node_modules ./web/node_modules
COPY --from=frontend-builder /app/web/package.json ./web/package.json
COPY --from=frontend-builder /app/web/next.config.js ./web/next.config.js
# `next dev` compiles from source, so the development image needs the whole
# web tree. This used to cherry-pick node_modules, package.json and
# next.config.js on top of the production stage — but that stage's ./web is
# `.next/standalone/`, a compiled server bundle carrying no sources, no
# tsconfig and no scripts/. So the supervisor program below launched
# `node scripts/dev.mjs` against a path that never existed, the dev frontend
# went FATAL, and the image looked broken (#906). Taking the builder's tree
# wholesale also stops the list from drifting each time web/ grows a
# top-level entry. `--chown` during the copy avoids re-layering node_modules.
COPY --chown=deeptutor:deeptutor --from=frontend-builder /app/web ./web
# `next dev` runs as the unprivileged deeptutor user (via `user=deeptutor` in
# the supervisord config) and must create/write its build cache under
# /app/web/.next, so give that user ownership of the web dir and the cache.
RUN mkdir -p /app/web/.next \
# The production build copied in above is not reusable by `next dev`, so it
# starts from an empty cache rather than a half-valid one.
RUN rm -rf /app/web/.next \
&& mkdir -p /app/web/.next \
&& chown deeptutor:deeptutor /app/web /app/web/.next
# Install development tools
+4 -1
View File
@@ -24,9 +24,12 @@ services:
- ./deeptutor_cli:/app/deeptutor_cli:ro
- ./scripts:/app/scripts:ro
# Mount frontend source for hot-reload
# Mount frontend source for hot-reload. Anything not listed here still
# ships inside the image (see the development stage in Dockerfile) — it
# just will not hot-reload.
- ./web/app:/app/web/app:ro
- ./web/components:/app/web/components:ro
- ./web/features:/app/web/features:ro
- ./web/lib:/app/web/lib:ro
- ./web/hooks:/app/web/hooks:ro
- ./web/context:/app/web/context:ro