Nuxt 3 Deployment with Bun: A Docker-Based Approach

Nuxt 3 Deployment with Bun: A Docker-Based Approach

Vamsi Madduluri

Deploying modern web applications efficiently is crucial for developers aiming to achieve optimal performance and scalability. This blog post introduces a cutting-edge method for deploying Nuxt 3 applications using Bun and Docker, focusing on efficiency, security, and flexibility.

Introduction to Bun and Nuxt 3

Bun is a fast, modern JavaScript runtime, package manager, and bundler rolled into one. It’s designed for speed and efficiency, making it an excellent choice for web development. Nuxt 3, on the other hand, is a versatile Vue.js framework that enables developers to create highly dynamic, server-side rendered applications with ease. Combining Bun with Nuxt 3 in a Dockerized environment offers a streamlined deployment process that leverages the best of both technologies.

Why Docker?

Docker provides a consistent and isolated environment for your applications, ensuring that they run seamlessly across different environments. By containerizing Nuxt 3 applications with Bun, you can achieve faster builds, smaller image sizes, and easier configuration management, which is especially beneficial for production deployments.

The Dockerfile Explained

The Dockerfile provided outlines a multi-stage build process designed to optimize the deployment of Nuxt 3 applications. Let’s break down each stage:

Base Stage

FROM oven/bun:1 as base
WORKDIR /usr/src/app

We start with the official Bun image as our base. This image includes Bun, which will be used for installing dependencies and running our Nuxt 3 application. The working directory is set to /usr/src/app.

Dependency Installation

FROM base AS install
RUN mkdir -p /temp/dev
COPY package.json bun.lockb /temp/dev/
RUN cd /temp/dev && bun install --frozen-lockfile

RUN mkdir -p /temp/prod
COPY package.json bun.lockb /temp/prod/
RUN cd /temp/prod && bun install --frozen-lockfile

Dependencies are installed in a temporary directory to cache them efficiently, speeding up future builds. This step ensures that both development and production dependencies are installed, facilitating the build process.

Pre-release Stage

FROM base AS prerelease
COPY --from=install /temp/dev/node_modules /usr/src/app/node_modules
COPY . /usr/src/app
ENV NODE_ENV=production
RUN bun --bun run build

In this stage, we copy the installed dependencies and the application code into the image. We then set the environment to production and build the application using Bun.

Release Stage

FROM base AS release
WORKDIR /usr/src/app
COPY --from=install /temp/prod/node_modules /usr/src/app/node_modules
COPY --from=prerelease /usr/src/app/.output /usr/src/app/.output
COPY package.json /usr/src/app/
USER bun
EXPOSE 3000/tcp
ENV NUXT_HOST=0.0.0.0
ENTRYPOINT ["sh", "-c", "bun run /usr/src/app/.output/server/index.mjs --host $NUXT_HOST"]

The final stage prepares the production image. It copies only the production dependencies and the built files to keep the image size minimal. The application is configured to run as a non-root user (bun) for enhanced security. We expose port 3000 for web access and use an environment variable NUXT_HOST to allow dynamic configuration of the host. The application is started with Bun, using the environment variable to set the host.

Dockerfile

# Use the official Bun image for the initial stages
# See all versions at https://hub.docker.com/r/oven/bun/tags
FROM oven/bun:1 as base
WORKDIR /usr/src/app

# Install dependencies into a temp directory
# This will cache them and speed up future builds
FROM base AS install
RUN mkdir -p /temp/dev
COPY package.json bun.lockb /temp/dev/
RUN cd /temp/dev && bun install --frozen-lockfile

# Install dependencies without excluding devDependencies
# Since some dependencies might be needed for the build process
RUN mkdir -p /temp/prod
COPY package.json bun.lockb /temp/prod/
RUN cd /temp/prod && bun install --frozen-lockfile

# Copy node_modules from the temp directory
# Then copy all (non-ignored) project files into the image
FROM base AS prerelease
COPY --from=install /temp/dev/node_modules /usr/src/app/node_modules
COPY . /usr/src/app

# Set NODE_ENV to production and run the build
ENV NODE_ENV=production
RUN bun --bun run build

# Copy production dependencies and built files into the final image
# Start fresh from the base to reduce the final image size
FROM base AS release
WORKDIR /usr/src/app
COPY --from=install /temp/prod/node_modules /usr/src/app/node_modules
COPY --from=prerelease /usr/src/app/.output /usr/src/app/.output
COPY package.json /usr/src/app/

# Ensure the container runs as a non-root user
USER bun

# Expose the port your app runs on
EXPOSE 3000/tcp

# Get Host from environment variable
# This is used to allow the container to be run on any host
ENV NUXT_HOST=0.0.0.0

# Adjusted command to directly run the server in production
# Following Nuxt's recommendation for production deployments
# Use ENTRYPOINT to ensure the environment variable is evaluated
ENTRYPOINT ["sh", "-c", "bun run /usr/src/app/.output/server/index.mjs --host $NUXT_HOST"]

Benefits of This Approach

  • Efficiency: Caching dependencies and separating the build environment from the production environment reduce build times and image sizes.
  • Security: Running the application as a non-root user minimizes security risks.
  • Flexibility: Using environment variables for configuration allows for easy adjustments without modifying the code or Dockerfile.

Conclusion

This Docker-based deployment strategy for Nuxt 3 applications using Bun optimizes the deployment process, making it faster, more secure, and more adaptable. By following the steps outlined in this post, developers can leverage the power of Docker, Bun, and Nuxt 3 to streamline their deployment workflows and achieve better performance and reliability in their web applications.