TypeScript: `encore build docker` bundles full workspace (devDeps + non-runtime files) with no opt-out
Summary
For TypeScript apps, encore build docker bundles the entire workspace into /workspace in the image, with only .git excluded. There is no public configuration to filter what gets included — node_modules ships as-is (devDependencies + dev tooling), and arbitrary repo files (docs/, scripts/, README.md, tsconfig.tsbuildinfo, …) ship too.
This inflates image size dramatically and slows down build / push / Cloud Run cold-start, with no warning to the user.
Reproduce
git cloneany TS Encore app that has heavy devDependencies (e.g.typescript,vitest,@biomejs/biome,promptfoo,firebase-tools).pnpm install(ornpm install— both pull devDeps by default).encore build docker --base node:24-bookworm app:test.docker run --rm --entrypoint sh app:test -c 'ls /workspace && du -sh /workspace/node_modules'.
Observed
/workspace/node_modulescontains every devDependency./workspace/contains every non-.gitfile from the repo, regardless of whether it's needed at runtime.
In our case the image carried ~50+ MB of pure devDeps (typescript 31 MB, vitest, @biomejs/biome, firebase-tools, etc.) plus a 26 MB promptfoo. Verified inside an image: pnpm why typescript --prod returns empty in the host repo, yet /workspace/node_modules/typescript exists in the image.
Expected
For TS apps, either:
- Default to production-only installs (
npm/pnpm/yarn install --prod), or - Provide a documented opt-out via
encore.app, e.g.:{ "build": { "docker": { "exclude_paths": ["node_modules/.cache", "docs", "scripts"], "prod_only_node_modules": true } } }
Source references
pkg/dockerbuild/spec.go→DetermineIncludes— copiesnode_modulesandpackage.jsonfrom every ancestor between workspace root and app root.cli/daemon/export/export.go—if buildSettings.Docker.BundleSource || appLang == appfile.LangTSforces source bundling for TS apps;ExcludeSourceis hardcoded to[".git"].
So the user-facing bundle_source flag in encore.app has no effect for TS apps — the bundling happens regardless.
Real-world impact
We observed a ~2× deploy-time regression on Cloud Run after a normal dependency-bump cycle:
| Phase | Before | After |
|---|---|---|
| Encore compile | 67 s | 142 s |
| "saving image to local daemon" | 36 s | 102 s |
| Final layer push to GCR | 4 s | 61 s |
| Cloud Run revision rollout | 40 s | 124 s |
| Total deploy | ~4.5 min | ~10 min |
Root cause was a single new 26 MB devDependency silently shipping to production. A user has no signal this would happen.
Workaround
Run `pnpm install --prod --frozen-lockfile` (or `npm ci --omit=dev`) in CI before `encore build docker`, so the workspace's `node_modules` only contains production deps when Encore copies it. Confirmed effective.
Ask
- Document the current bundling behavior for TS apps in docs/ts/self-host/build.
- Either default to prod-only installs, or expose `exclude_paths` / `prod_only` in `encore.app` build config.
Encore version: v1.57.4 Host: macOS 25.3.0 (arm64), GitHub Actions `ubuntu-24.04` runner Package manager: pnpm 10
Source: encoredev/encore