The production stage copied the entire build stage `/app` wholesale,
including every workspace member's devDependencies (typescript, vite,
vitest, storybook, rolldown, ...) and unrelated workspace members' own
dependencies (ui's client-side bundle deps like mermaid, lucide-react),
none of which the running server touches. That alone accounted for
~1.5GB of the shipped image.
Add a `pruned-app` stage, forked from `build` after all builds finish,
that re-resolves node_modules with `pnpm install --prod --frozen-lockfile
--filter='@paperclipai/server...'` -- server plus everything it actually
depends on, transitively, production-only. `production` now copies from
`pruned-app` instead of `build`.
The prune has to live in its own stage rather than in `build` directly:
`cloud-plugins` and `cloud-server-deps` (declared later in the file) both
fork from `build` and still need its full devDependency toolchain
(typescript, etc.) to compile their own TypeScript at image build time.
Move `tsx` from server's devDependencies to dependencies. It looked like
a dev tool, but the image's own ENTRYPOINT imports it directly
(`--import ./server/node_modules/tsx/...`) to transpile the workspace
packages the server consumes by TypeScript source (their `exports` field
points at `./src/*.ts`, not a prebuilt `dist`) -- it's genuinely required
at runtime, and a naive prod-prune would have deleted it and broken every
boot.
Verified locally: the `cloud` target drops from 7.5GB to 5.53GB and the
`production` target lands at 5.26GB. Boot-tested the built `cloud` image:
embedded Postgres initializes, all migrations apply, `/api/health`
returns 200, the UI serves, `@sentry/node` resolves to the version
`server/package.json` declares, and tini/orphan-reaping passes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RD53WaVFqm8pbntKZ5seWy