The script the Viewer adds to every prototype it serves is dist/bridge-bundle.js at the repo root. The Dockerfile does not build it: it copies the file from your checkout as it is. The file is committed, and CI fails any commit where it differs from a fresh build of the bridge source, so a clean checkout already has the right one.
Rebuild it only if you changed the bridge source yourself. From the repo root:
npm install
npm run build:bridge
If the file is missing outright, the image builds cleanly and then exits on startup.
The Dockerfile copies from repo-root paths (viewer/package.jsonand the root package.json), because the Viewer's Next.js dashboard resolves next and react from the checkout root rather than vendoring its own copies. So you build from the repo root and point -f at the Dockerfile, not the other way around:
The ignore rules for this build live in viewer/Dockerfile.dockerignore. Naming an ignore file <Dockerfile>.dockerignore next to its Dockerfile is a BuildKit feature. The classic builder looks only for a .dockerignore at the context root, and this repo does not have one.
So if the build runs on the classic builder (an old Docker Engine, or DOCKER_BUILDKIT=0), nothing is excluded and COPY . . copies your entire working checkout into the image. That includes .git, every node_modules tree, and viewer/.env.local, which holds your GitHub App private key, the App client secret, the session secret, and your SMTP password.
BuildKit is the default in Docker Desktop, which bundles the buildx plugin BuildKit needs.
Note
On a Linux host, DOCKER_BUILDKIT=1 can fail the build outright:
ERROR: BuildKit is enabled but the buildx component is missing or broken.
The variable does not turn on a builder that is already there. It selects one that ships as a separate plugin, and an engine without it stops rather than falling back. Install the plugin (apt install docker-buildx-plugin, or your distribution's equivalent), or drop the variable and read the paragraph below: the classic builder produces a correct image, it just copies more into the context on the way.
The build does not rely on you getting that right. After COPY . ., the Dockerfile asserts that no .env, .env.local, .env.*.local or .env.production reached the build context, and fails the build if one did. The ignore file is the optimization; the assertion is the control. (.env.example is exempt: it is a tracked template with no real values.) To confirm after a build anyway: