feat(modules): mount the modules directory as a volume (phase 2, PR 9)
All checks were successful
PR Checks / bot-install (pull_request) Successful in 21s
PR Checks / client-build (pull_request) Successful in 32s
PR Checks / server-tests (pull_request) Successful in 1m39s

Closes Phase 2. Modules live on a mount, never in the image — that is what
lets an operator add one to a pull-only deployment without building anything.

`./modules` is a bind mount rather than a named volume: placing a module
directory by hand is a supported install (MODULE_SYSTEM.md §2.5), and that has
to be doable from the host rather than through `docker cp`. Read-write, because
the admin panel's install/uninstall unpacks and removes directories there.

The directory is tracked via its README so it exists in the checkout with the
operator's own ownership — Docker recreates a missing bind-mount source as
root:root, which the container user could not then write. `.dockerignore`
excludes it so a module in the builder's working tree can never ship inside an
image.

Also corrects the route-manifest generator's list of filesystem-conditional
mounts, which never picked up `/modules` when PR 7 added it. Comment only; the
generator filters on an allowlist, so its behaviour was already right.

Verified against a real container, not just a parsed compose file: image
carries an empty node-owned /app/modules despite a module in the build context;
a module on the bind mount loads, mounts, replays and reaches `started`;
`/api/v1/public/modules` lists it; the chunk serves from the entry's directory
only (server source and module.json 404) with `no-cache`; the injected tag
follows core's bundle; and in Chrome the page renders on first paint inside
core's PublicLayout with its nav row interleaved into core's public nav, under
enforced `script-src 'self'` with zero CSP reports and no console errors.
Removing the directory by hand reconciles the row to `startup_failed`/`require`
and leaves core healthy with no injection.

933 server + 160 client tests pass, manifest unchanged at 230 routes, swagger
regenerates byte-identical.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-11 00:48:48 -05:00
parent 0a3f1eb9fa
commit a103e0ce10
7 changed files with 130 additions and 8 deletions

View File

@@ -21,6 +21,12 @@ RUN if [ -f client/package.json ]; then \
# Persistent uploads + logs live on mounted volumes.
RUN mkdir -p /app/uploads /app/logs && chown -R node:node /app/uploads /app/logs
# Installed modules are mounted in too (docker-compose.yml), and .dockerignore
# keeps any local modules/ OUT of the image — a module must never be baked in.
# The directory is still created here so a container run without the mount finds
# an empty, writable modules dir rather than no directory at all.
RUN mkdir -p /app/modules && chown node:node /app/modules
USER node
EXPOSE 3000