Your Build Gets Less Memory Than Your App — Why Bundles Fail to Compile

·5 min read·Updated
dockerbuildmemorynodejstroubleshooting

If your build dies partway through — a webpack bundle that never finishes, a tsc that stops without an error, a native module that fails to compile — the cause is usually that the build runs in its own container with its own, smaller memory limit. It's capped at roughly 0.75 GB of RAM and half a vCPU by default, and that's independent of whatever you allocated to the app. So the instinct to raise your app's RAM and try again doesn't help: you're raising a limit the build never touches.

Why raising your app's RAM changes nothing

Two different containers are involved, and people naturally assume they're one.

  • The build container compiles your code and produces an image. It gets a fixed, modest allocation.
  • The app container runs the resulting image. It gets whatever preset you selected.

A build failure happens in the first one. Your app's 2 GB allocation is irrelevant to it — the app doesn't exist yet.

That's why the symptom is so confusing: you fix what looks like a memory problem, redeploy, and get the identical failure. The number you changed wasn't the number that was binding.

What typically blows the limit

Almost always a build step that holds a large graph in memory:

  • JavaScript bundlers. webpack, Vite, or Rollup on a large app — bundling is memory-hungry and scales with your dependency graph, not your source size.
  • TypeScript compilation. tsc on a big codebase, especially with strict type checking across many files.
  • Native modules. node-gyp compiling something like sharp, bcrypt, or canvas, plus their toolchain.
  • Source-built Python packages. A pip install that can't find a wheel and falls back to compiling — scientific packages are the usual suspects.
  • Large compiled projects. Rust and heavily-generic Go builds can spike well past what you'd expect.

The tell is where it fails: the build log stops during a compile or bundle step rather than at install or startup. A process killed for memory often leaves no error message at all — just an abrupt end — which is why it reads as a mystery rather than an out-of-memory failure.

Four ways around it

1. Build somewhere else and push the image

The definitive fix. Build on your own machine or in CI where memory isn't constrained, then push the finished image — that's the Docker Push path, and it sidesteps the build container entirely.

If you already have CI, pushing from a workflow is a few lines and gets you a build environment with room to work. The Docker image deploy path then just runs what you built.

2. Tell your toolchain to use less

Most build tools will fit into a smaller space if you ask:

ENV NODE_OPTIONS=--max-old-space-size=512

That caps Node's heap below the container limit so V8 garbage-collects rather than being killed. Counter-intuitively, lowering it often makes a build succeed that was failing.

Similarly, reduce parallelism — parallel compilation multiplies peak memory:

cargo build -j 1
make -j1

3. Avoid compiling altogether

  • Python: prefer wheels. pip install --only-binary=:all: fails loudly rather than silently falling back to a source build, which at least turns a mysterious OOM into a clear message.
  • Node: pick pure-JS alternatives to native modules where one exists, or use a base image that ships the compiled artefacts.
  • .NET: publish locally and upload the output — that path expects prebuilt publish output, so no compilation happens server-side at all.

4. Move the heavy work out of the image build

If a step is expensive and its output rarely changes — a large asset pipeline, a generated client — build it once, commit the artefact or publish it, and have the image copy it in.

While you're in the Dockerfile

One detail that trips people occasionally: the build looks for a file named exactly Dockerfile, at the root. Not dockerfile, not Dockerfile.prod, and there's no way to point at an alternative filename. If you keep several, make the one you deploy the canonical Dockerfile and include the others by another route.

Intermediate build containers are cleaned up automatically, so you don't need to worry about build leftovers accumulating.

Sizing your expectations

The build allocation is deliberately modest because builds are short and bursty, and it's plenty for the common case: install dependencies, copy files, done. It's specifically compilation and bundling that strain it.

So a useful heuristic when choosing a deploy path: if your project has a heavy build step, build it elsewhere. If it doesn't — a Python bot, a Go binary compiled from a small source tree, a Node service with no bundler — the server-side build is simpler and works fine.

FAQ

Why does my build fail but my app has plenty of RAM?

Builds run in a separate container with their own limit, around 0.75 GB. Your app's allocation doesn't apply to the build.

Why does my webpack or Vite build hang or die?

Bundlers are memory-hungry and scale with your dependency graph. Cap Node's heap with NODE_OPTIONS=--max-old-space-size, or build in CI and push the image.

How do I know it was an out-of-memory failure?

The build log usually stops abruptly during a compile or bundle step with no clear error. A killed process rarely gets to explain itself.

Can I increase the build memory?

Not from the app's settings — it's a platform-level limit. Build elsewhere and push the image if your build needs more room.

Does --max-old-space-size need to be higher or lower?

Lower, usually. Setting it below the container limit makes V8 collect garbage instead of growing until the kernel kills it.

Does my Dockerfile have to be called Dockerfile?

Yes, and it must be at the root. There's no option to specify a different filename.

Wrapping up

Build-time and run-time are different environments with different limits, and almost every confusing "it works locally" build failure comes from treating them as one. If your build compiles or bundles anything substantial, do it where memory is cheap — your laptop or a CI runner — and ship the result. If it just installs dependencies and copies files, the server-side build is the simpler path and the limit will never come up.