shahriyar.dev
Back to blog
Next.jsDockerMulti-stage DockerfileBunPrismaContainerization

Optimizing Your Next.js Stack: A Multi-Stage Dockerfile for Bun, Prisma, and Production-Ready Deployments

·6 min read

Containerizing a Next.js application has become a standard practice for ensuring consistency across development, staging, and production environments. But when you add tools like Bun (a fast all-in-one JavaScript runtime and package manager) and Prisma (a modern ORM for Node.js and TypeScript), the Dockerfile can quickly become bloated, slow to build, or insecure if not structured carefully. A multi-stage Dockerfile solves these problems by separating build-time dependencies from runtime artifacts, reducing image size, improving caching, and keeping secrets safe. In this post, we’ll walk through how to build an optimized multi-stage Dockerfile for a Next.js application that uses Bun for package management and Prisma for database access, then deploy it in a production-ready manner.

Why Multi-Stage Dockerfiles?

A multi-stage Dockerfile uses multiple FROM statements in a single file. Each stage can have a different base image and perform different tasks—install dependencies, compile code, generate Prisma client, etc. Only the final stage retains the files needed to run the application. This approach offers several advantages:

  • Smaller final image – By discarding build tools, source maps, and dev dependencies, the production image can be as small as 100–200 MB instead of 1+ GB.
  • Improved security – If a build-time dependency has a vulnerability, it never makes it into the final container.
  • Faster buildsDocker layer caching works more effectively when stages are logically separated.
  • Cleaner separation of concerns – Each stage has a clear responsibility (deps, build, production).

Setting Up with Bun

Bun is a drop-in replacement for Node.js that includes its own package manager, bundler, and test runner. It’s significantly faster than npm or yarn for installing dependencies, and it can also run your Next.js application. However, Bun’s runtime is not a perfect mirror of Node.js in all edge cases, so test thoroughly before switching.

For the Dockerfile, we’ll use official Bun images (oven/bun). The base image for the build stage can be oven/bun:1.1 (or the version your project needs), and the production stage can use a minimal Debian or Alpine image with only the Bun runtime installed.

Integrating Prisma

Prisma requires database schema generation (the Prisma client) and sometimes migrations. During a Docker build, you have two options:

  1. Generate the Prisma client during the build stage and copy it to the final image.
  2. Generate it at runtime using a startup script.

The first option is faster and more reliable for production because it avoids runtime generation overhead. However, you must ensure the Prisma client is compatible with the production environment (e.g., correct architecture). We’ll generate it during the build stage and also run prisma migrate deploy (if using migrations) at container startup using an entrypoint script.

Building a Production-Ready Dockerfile

Below is a complete multi-stage Dockerfile for a Next.js app using Bun and Prisma. It includes:

  • Stage 1 (deps): Install all dependencies (including dev) with Bun.
  • Stage 2 (build): Run Prisma generate and next build.
  • Stage 3 (production): Copy only the runtime essentials.
Dockerfile
# Stage 1: Install dependencies
FROM oven/bun:1.1 AS deps
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile --production=false

# Stage 2: Build the application
FROM oven/bun:1.1 AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Generate Prisma client and run migrations (if needed)
RUN bunx prisma generate
# Build Next.js app
RUN bun run build

# Stage 3: Production runtime
FROM oven/bun:1.1-slim AS production
WORKDIR /app
ENV NODE_ENV=production

# Copy only necessary files from build stage
COPY --from=build /app/.next ./.next
COPY --from=build /app/public ./public
COPY --from=build /app/package.json ./package.json
# Copy Prisma schema and generated client (needed for runtime queries)
COPY --from=build /app/prisma ./prisma
COPY --from=build /app/node_modules/.prisma ./node_modules/.prisma
# Copy the node_modules but only the production dependencies (we'll prune later)
COPY --from=build /app/node_modules ./node_modules

# Remove dev dependencies (optional, but reduces size further)
RUN bun install --production --frozen-lockfile

# Expose port and start Next.js
EXPOSE 3000
CMD ["bun", "run", "start"]

Key Points in the Dockerfile

  • Lockfilebun.lockb is Bun’s lockfile. Always use --frozen-lockfile to ensure reproducible installs.
  • Prisma generate – Runs in the build stage after copying the entire source. The generated client ends up in node_modules/.prisma and must be copied to the final stage.
  • Prisma schemaprisma folder is copied because Prisma may need it at runtime for connection management (though the client is self-contained, some features like prisma studio need the schema).
  • Production install – After copying production node_modules, we run bun install --production to strip dev dependencies. This is done in the production stage to avoid including heavy dev packages.
  • Slim imageoven/bun:1.1-slim is smaller than the full Bun image. If you need system libraries (e.g., OpenSSL for Prisma), the slim image already includes them.

Best Practices and Optimization Tips

To further optimize your Docker build, consider these tips:

  • Use .dockerignore – Prevent unnecessary files (.git, node_modules, cypress, test) from being copied into the build context. This speeds up the initial upload to the Docker daemon.

Example .dockerignore:

.git
node_modules
.next
*.log
cypress/
tests/
Dockerfile*
  • Leverage multi-architecture builds – If deploying to ARM (e.g., Apple Silicon or AWS Graviton), use --platform linux/amd64 or build for both using Docker Buildx.
  • Cache Prisma generation – The Prisma client generation command can be cached by separating it from the Next.js build. You can add a dedicated layer for Prisma copy and generate before bun run build.
  • Use build arguments – Pass environment variables like DATABASE_URL (for Prisma generate) via ARG but be careful: build-time secrets are embedded in image layers if used with COPY. Instead, only use build arguments for non-sensitive values (e.g., NODE_ENV) and pass runtime secrets via Docker secrets or environment injection at container startup.
  • Consider using next start with standalone outputNext.js can output a standalone folder (set output: 'standalone' in next.config.js). This reduces the copy overhead by eliminating unnecessary node_modules. With Bun, you may need to adapt the standalone output path.

Conclusion

A multi-stage Dockerfile is not just an optimization—it’s a necessity for production Next.js applications when using Bun and Prisma. By separating dependency installation, build, and runtime stages, you achieve smaller and more secure images, faster CI/CD pipelines, and a clear separation between development and production toolchains. The Dockerfile provided here gives you a solid foundation; adapt it to your project’s specific needs (e.g., adding a database migration script, using a different base image, or integrating with a cloud service). With this setup, you can confidently deploy your Next.js app in any container orchestration environment.

Comments