This is me, André König - a software engineer from Hamburg, Germany. André König

  • Dec 06, 2023
  • 5 Min Read
  • Bun

Using Bun as the Package Manager in Production-Ready Docker Images

Bun took the tech community by storm.

It has gained traction from various angles, not just because its runtime is blazingly fast, but also because it follows a battery-included approach and ultimately forms a tool belt for JavaScript & TypeScript projects.

The runtime aims for 100% compatibility with Node.js – that is the mission. At the time of writing, this goal hasn't been achieved yet, so it is not possible to use Bun's runtime as a drop-in replacement for Node.js.

This is why many engineers pick and choose the parts from Bun they can benefit the most from. Bun's package management has gained a lot of popularity as it is also known to be ultra-fast. Engineers are using it as a replacement for their package managers (such as npm, yarn, or pnpm). It not only minimizes package installation times on your development machine but also reduces the runtime of CI/CD pipelines, ultimately saving money.

Here, you can see a benchmark from the Bun website where the team compared the installation of a dependency tree in a Remix application (benchmark repository).

Benchmark Chart visualizing package installation times. It compares: bun, pnpm, npm and yarn. These are the results: bun = 00.36s – pnpm = 06.44s (17x slower) – npm = 10.58s (29x slower) – yarn = 12.08s (33x slower)

The question now is: How can we benefit from this performance during the build of container images by using Bun as the package manager while keeping Node.js as the runtime environment?


In our scenario, our goal is to create a production-ready container image, specifically a Docker container image. If we use Bun as our package manager, how can we ensure that Node.js remains the runtime environment in the end?

😟 .. My container image will contain Node.js and Bun in the end!

This would be problematic, as the goal when building a production-ready container image should always be to minimize the dependency tree in order to reduce the vulnerability surface.


Fortunately, we can utilize Docker's Multi-stage builds feature here. We'll divide the entire process into two stages:

Creating a Build Stage

This stage will be dedicated to constructing the actual build of our application. Our steps will include:

  1. Utilizing the official Node.js image as the base image.
  2. Manually installing Bun via the install script.
  3. Installing all project dependencies, including devDependencies, via Bun.
  4. Executing all build tasks.
  5. Clearing the node_modules and installing only the production dependencies.

Creating a Distribution Stage

As the name implies, this stage is responsible for creating the deployable distribution. In this stage, we will:

  1. Utilizing the official Node.js image as the base image.
  2. Copy the built artifact of our application from the build stage.
  3. Transfer the production dependencies from the build stage.
  4. Establish the start command.

This stage will only include the Node.js runtime but will also contain all the production dependencies and application artifacts from the build stage.

Putting Everything Together – The Final Dockerfile

Now that we've outlined our approach, we can formalize it into a Dockerfile:

FROM node:${NODE_VERSION}-slim as build
WORKDIR /build
# Install Bun in the specified version
RUN apt update && apt install -y bash curl unzip && \
curl | bash -s -- bun-v${BUN_VERSION}
ENV PATH="${PATH}:/root/.bun/bin"
# Copy the lock file and app manifest, then install
# the dependencies, including the dev dependencies
COPY bun.lockb package.json ./
RUN bun install --frozen-lockfile
# Copy the application sources into the build stage
COPY . .
# ADJUST: Build your application
RUN bun run build
# After building the application, we will remove the node_modules
# directory and install only the production dependencies.
# Note that clearing the Bun package cache is necessary because I encountered
# extremely slow install times during building the image. This issue seems to be
# related to:
RUN rm -rf node_modules && \
rm -rf /root/.bun/install/cache/ && \
bun install --frozen-lockfile --production
# Optional step: Here we will prune all unnecessary files from our
# node_modules directory, such as markdown and TypeScript source files,
# to further reduce the container image size.
RUN curl -sf | sh && \
FROM node:${NODE_VERSION}-slim as distribution
ENV NODE_ENV="production"
# ADJUST: Copy application build artifacts.
COPY --from=build --chown=node:node /build/node_modules ./node_modules
COPY --from=build --chown=node:node /build/contents ./contents
COPY --from=build --chown=node:node /build/build ./build
COPY --from=build --chown=node:node /build/public ./public
COPY --from=build --chown=node:node /build/package.json .
RUN chown -R node:node /app
USER node
CMD [ "npm", "run", "start" ]

This Dockerfile generates a production-ready image for deploying a Remix application. Although the Dockerfile is essentially generic, it includes project-specific use cases in certain places that need to be customized to suit your specific needs (see the lines highlighted with ADJUST).


Bun's runtime mission (or should I say buntime mission as well?) is clear: Achieve 100% compatibility with Node.js, enabling applications that previously ran on Node.js to operate directly on Bun - a true drop-in replacement.

However, Bun offers more than just runtime capabilities. Engineers can still utilize the approach outlined above to benefit from its ultra-fast dependency installation during their container image builds.

Thank You

I hope that you found this article insightful and valuable for your journey. If so, and you learned something new or would like to give feedback then let's connect on X at @ItsAndreKoenig. Additionally, if you need further assistance or have any queries, feel free to drop me an email or send me an async message.

One last thing!

Let me know how I'm doing by leaving a reaction.

You might also like these articles