# Building the image with a pre-built frontend > **Where this sits:** a variant of Step 2 of [DEPLOY](../DEPLOY.md), not a > separate deployment. Everything else on that page still applies. The shipped `Dockerfile` is a multi-stage build: stage 1 runs `npm ci` and `npm run build` under Node, stage 2 copies the compiled SPA into the Python image. Behind a proxy that re-signs HTTPS, stage 1 is where it dies - npm inside the container does not trust the corporate root CA, and the failure reads `npm ci failed` or the unhelpful `Exit handler never called`. The fix is to build the frontend where npm already works - the host, which has the corporate CA in its trust store - and give the image the finished `frontend/dist/`. It is also much faster: two to three minutes instead of fifteen. A site with no proxy problem at all may want it for that reason alone. ## Do it ```bash # 1. Build the frontend on the host cd frontend npm ci npm run build # writes frontend/dist/ cd .. # 2. Build and start the stack from the pre-built dist docker compose -f docker-compose.prebuilt.yml build docker compose -f docker-compose.prebuilt.yml up -d ``` The rest is [DEPLOY](../DEPLOY.md) unchanged: the `migrate` service runs the schema, the plugin bootstrap and the seeders before `api` starts, nginx terminates TLS on `API_PORT`, and the first admin comes from `/setup` or `flask seed admin`. ## The files this uses | File | What it is | |---|---| | `Dockerfile.prebuilt` | single-stage: Python dependencies plus a copy of `frontend/dist/`. No Node, no npm | | `docker-compose.prebuilt.yml` | the same stack, building from `Dockerfile.prebuilt` | | `.dockerignore.prebuilt` | permits `frontend/dist/` to be copied, which the normal `.dockerignore` excludes | ## What it costs - **npm has to exist on the build host.** That is the whole trade: the trust problem moves from the container to a machine that has already solved it. - **It is two steps, and forgetting the first one ships a stale UI.** The image copies whatever is in `frontend/dist/` at build time. A frontend change that was not rebuilt is simply not in the image, and nothing warns you. If the UI does not show your change, rebuild `dist/` before debugging anything else. - **`frontend/dist/` has to reach the build.** Either it is committed, or the pipeline builds it immediately before the image. ## The version of this that scales The pre-built approach is a workaround for building at the deployment site. The end state is not building there at all: ```yaml # CI: build the frontend, build the image, push it - run: cd frontend && npm ci && npm run build - run: docker build -f Dockerfile.prebuilt -t shopdb:$GIT_SHA . - run: docker push /shopdb:$GIT_SHA ``` A site then pulls a tagged image and runs it - seconds, no build step, no npm anywhere near the facility. `docker-compose.pull.yml` is the compose file for that shape today, and `docker-compose.airgap.yml` is the same idea for a site that loads an image tarball off removable media ([DEPLOY-AIRGAP](../DEPLOY-AIRGAP.md)). Finishing that path is the distributed release item on [ROADMAP](../ROADMAP.md). | Approach | Build time | Behind a re-signing proxy | Where it belongs | |---|---|---|---| | Multi-stage `Dockerfile` | 15+ min | fails unless the CA is staged | a developer box with a clean network | | Pre-built frontend (this page) | 2-3 min | works | a site that must build locally | | Pull a published image | seconds | works | where every site should end up |