Next.js is an increasingly popular React framework for building visually stunning, high-performing, and easy-to-use webapps. You can take your Next.js app one step further by containerizing it with Docker. Once you have it Dockerized, you can more easily share it across machines, cloud host it, and store config (among many, many other things).
TLDR: Check out this repo for the resulting Docker and Docker Compose files for running a Next.js app with Bun.
Using Bun as your JavaScript runtime
In this tutorial, we’re going to use Bun to initialize, bundle, and run our Next.js application. Bun is this decade’s answer to Node.js — it’s substantially lighter and faster, thanks to its JavaScriptCore-based architecture (less memory usage and quicker startup at the cost of optimized execution speed).
With Next.js, you can (and perhaps should) develop in TypeScript. Node.js doesn’t natively support TypeScript, rather it transpiles it to JavaScript using ts-node
. Since Bun has TypeScript support right out of the box, we won’t need any extra config and we’ll also save on time/compute.
Initializing your Next.js app with Bun
You can skip this section if you already have a Next.js app up and running.
If you haven’t already, check out the docs and install Bun.
To generate a working Next.js base app that builds and runs with Bun, you can use this command:
bun create next-app
You’ll then be presented with a series of configuration options. For our purposes, we can stick with the defaults.
And from there, you can run your app and access its dev server with:
bun --bun run dev
Go to localhost:1313
to check it out.
Writing the Dockerfile for a Next.js / Bun app
Our project’s Dockerfile will be incredibly simple and standard. We’re working with a single-service, classic Next.js app, so we’ll only need to write one Dockerfile with limited config.
Start by creating a Dockerfile
in your project’s root.
Step 1: Setting the base image
Bun offers a lightweight Docker image that we’ll build our app on top of. It contains the Bun runtime bundled with necessary dependencies.
FROM oven/bun
Alternatively, you might want to use a lighter-weight Docker image for this. Consider:
FROM oven/bun:alpine
Step 2: Setting the working directory
Here, we’ll define the working directory inside of the container. That means we can call it whatever we want at this point — it doesn’t need to match the name of the directory on our local machine. But for consistency, I like to set it as /app
:
WORKDIR /app
Step 3: Bringing over our dependencies
Right now, our container’s app
directory is empty. Since Dockerfiles cache in layers, we want to copy over the dependencies first (we’re less likely to edit these files than the rest of our app). Docker’s caching ensures we don’t have to sit through the lengthy copy/install commands every time we edit app files.
COPY package*.json ./
Step 4: Installing dependencies
This command just installs the dependencies defined in package.json
to your working directory:
RUN bun install
Step 5: Copy the rest of your app
Now that we have dependencies set up within the container, we can copy over the remaining app files from our local machine (minus those defined in the .dockerignore
):
COPY . .
Step 6: Running the start command
Lastly, we’ll run this Bun command to start the dev server:
RUN bun --bun run dev
The resulting Dockerfile
And that’s it! Our Next.js project is fully Dockerized now.
FROM oven/bun
WORKDIR /app
# Copy package.json + package-lock.json
COPY package*.json ./
# Install dependencies
RUN bun install
# Copy the rest of the application
COPY . .
# Start the dev server
CMD ["bun", "--bun", "run", "dev"]
Writing the Docker Compose file for a Next.js / Bun app
Now that we have the app containerized with a Dockerfile, we can write a simple Compose file to make it extra quick and easy to run. Once we do this, we can run docker compose up
to get it started instead of copy/pasting complex docker build
and docker run
commands.
Create a file called compose.yml
in your project’s root directory.
Step 1: Defining the Next.js service
This project only has one Dockerfile, so we’ll only need one service. Its name doesn’t exactly matter, but it’s best practice to choose a name relevant to the project:
version: '3.8'
services:
next:
Step 2: Setting build context
The build context defines the path(s) that the Docker build has access to. Setting the context to .
will include our entire repo in the build.
build:
context: .
Step 3: Setting ports
The ports
option maps the host port to the container port. This means you’ll access your app via localhost:3000
and any requests made will be forwarded to the container at port 3000.
ports:
- '3000:3000'
Step 4: Adding volumes
The volumes
option shares data between the host machine and the containerized app. This will persist any data created while the container is running. So next time you launch your app, you’ll see your app’s state just as you left it.
The syntax for a volume will be the directory’s local path in your project, followed by a colon, followed by where you’re mounting the directory within the container.
volumes:
- './public:/app/public'
- './src:/app/src'
The resulting Compose file
There we have it — a complete Docker Compose file. Our project is ready to be spun up now…
version: '3.8'
services:
next:
build:
context: .
ports:
- '3000:3000'
volumes:
- './public:/app/public'
- './src:/app/src'
Running the Next.js app in a Docker container
Now that we have both a Dockerfile and a Docker Compose, we can spin this project up with a single command. Launch the Docker daemon and from the app’s root, you can run docker compose up
to build and run the project as defined. You’ll be able to access it on localhost:3000
.
When you’re done, you can clean up and remove your containers with docker compose down
.
What’s Next?
One of the greatest things about Docker containers is that they live throughout every stage of the dev cycle. If you’re developing locally with Docker and Compose, you should be able to then access your app just as easily in a CDE and then an ephemeral environment, with limited config modifications. Consider kicking off a free 30-day trial of Shipyard to bring production-like testing and orchestration to your Next.js app.