Published on

Containerising Next.js using Docker and Bun

Authors

Intro

Bun is a new performant JavaScript runtime and toolkit containing its own bundler, test runner and package manager, while aiming to be a drop-in replacement for existing projects using Node.js. At the time of writing it has reached version 1.0.2, and I had the chance to test it with some personal and work projects, including using it in combination with Next.js in Docker containers.

So far I have been really impressed with its performance and its compatibility with existing projects. Below I am showing you how to containerise a Next.js project using Bun, with minimal changes from the official documentation.

Installing Bun and scaffolding a new Next.js project

The official documentation offers many alternatives on how to install Bun, the quickest is:

curl -fsSL https://bun.sh/install | bash

After that, creating a new Next.js project is as easy as:

bunx create-next-app

Containerising our new Next.js project

Vercel offers guidelines in their official documentation on how to create a Docker image along with a sample repository.

  • First we need to modify the next.config.js file so that the build output can be standalone, as shown in the official documentation.

    // next.config.js
    module.exports = {
      // ... rest of the configuration.
      output: 'standalone',
    }
    

After that, we can reuse the existing example by making the following modifications in the provided Dockerfile:

  • The official base image is defined as FROM oven/bun AS base and is released by Oven, "the company behind Bun" in DockerHub.

  • Bun is using by default a binary lockfile bun.lockb for performance. In order to install a project's dependencies we need to copy first the package.json and the generated bun.lockb lockfile.

    COPY package.json bun.lockb ./
    
  • Installing using reproducible dependencies can be done with: bun install --frozen-lockfile

    RUN bun install --frozen-lockfile
    
  • The build step can be run using: bun run build

    RUN bun run build
    
  • The base image is creating a unix group called bun. We don't need to create a new separate group nodejs as it was done in the previous version of the Dockerfile. We only need to change the ownership of the generated files from the build step to nextjs:bun.

    RUN chown nextjs:bun .next
    COPY --from=builder --chown=nextjs:bun /app/.next/standalone ./
    COPY --from=builder --chown=nextjs:bun /app/.next/static ./.next/static
    
  • Finally, the server can be run with: bun server.js

    CMD ["bun", "server.js"]
    
  • Optionally, my personal preference is to disable the collection of anonymous telemetry during the build step and for the final produced Docker image.

    ENV NEXT_TELEMETRY_DISABLED 1
    

Full Dockerfile

The full Dockerfile with the above changes can now be added to the root of the Next.js project and is as followed:

FROM oven/bun AS base

# Install dependencies only when needed
FROM base AS deps

WORKDIR /app

# Install dependencies
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile

# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Disable telemetry during the build
ENV NEXT_TELEMETRY_DISABLED 1

RUN bun run build

# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app

ENV NODE_ENV production

# Disable telemetry
ENV NEXT_TELEMETRY_DISABLED 1

RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public

# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:bun .next

# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:bun /app/.next/standalone ./
COPY --from=builder --chown=nextjs:bun /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT 3000

# Set hostname to localhost
ENV HOSTNAME "0.0.0.0"

CMD ["bun", "server.js"]

Building the Docker image and running it as a container

As before, the image can be built and tagged using:

docker build -t my-app .

Finally, running a container of the image:

docker run -p 3000:3000 my-app