Running NextJS in a Docker Container

Photo by Bruno Thethe on Unsplash

Introduction

This is a guide on how to run a NextJS application in a Docker container.

NextJS apps usually require some kind of backend server to run.

NextJS is created and maintained by Vercel so it works great on their hosting service. Vercel is an amazing service that allows you to deploy your NextJS application with ease.

However, there are times when you may want to run your application in a Docker container. Using a docker container will allow you to host a NextJS application on Azure, AWS, DigitalOcean, Fly.io or any modern cloud provider.

This guide will show you how to dockerize your NextJS app in minutes.

Prerequisites

You will need the following installed on your machine:

  • Docker Desktop
  • NodeJS
  • A package manager. I use pnpm as my package manager, but you can use npm or yarn. They have similar commands.

Just want to see the code?

You can see how this works in a real app at Use Miller on GitHub

Getting Started

You must configure your NextJS application to build a standalone application. This is required for running NextJS in a Docker container.

In your next.config.js file, add the following:

const nextConfig = {
  output: "standalone",
  // ... other config
};

Configuring Docker

.dockerignore

Add a .dockerignore file to the root of your project. This file will contain the files and folders that you do not want to copy into the Docker container.

I say root of the project here because I build in a monorepo. But you can put this file in the root of your NextJS application if you're not using a monorepo.

I use this .dockerignore file for my NextJS projects. You can use it as a starting point.

Dockerfile*
.dockerignore
node_modules
npm-debug.log
README.md
.next
.git
.jest_cache
.docker-compose
.vscode
.terraform
.husky

Dockerfile

Add a Dockerfile to the root of your project. This file will contain the instructions for building your Docker image.

This is a multi-stage Dockerfile. This means that we will build the NextJS application in one stage and then copy the built files into a new stage. It results in a smaller final image size for deployment.

  • STAGE 1: A container with pnpm and python3 is required
  • STAGE 2: Fetch deps into the pnpm store
  • STAGE 3: Copy the application code and install all deps from cache into the application
  • STAGE 4: Build the NextJS app
  • STAGE 5: Create a clean production image - only take pruned assets

Each stage is stored on the build machine's cache. This means that if you make a change to your code, only the last 3 stages will be rebuilt. This is a huge time saver.

# STAGE 1: A container with pnpm and python3 is required
FROM node:18-alpine as pnpm_base

WORKDIR /app
# install pnpm
RUN npm i --global --no-update-notifier --no-fund pnpm@7
# install python3 and other deps
RUN apk add --no-cache g++ make py3-pip libc6-compat

# STAGE 2: fetch deps into the pnpm store
# We run pnpm fetch in a separate step to avoid re-fetching deps on every code change
# fetch is a pnpm command that downloads all dependencies to the local store
# You could remove or skip this step if using npm or yarn (but make sure to copy your lock file)
FROM pnpm_base as fetched_deps
WORKDIR /app
# setting production env usually speeds up install for your package manager
ENV NODE_ENV production
# copy the lock file that you use
COPY pnpm-lock.yaml ./
# set the store dir to a folder that is not in the project
RUN pnpm config set store-dir /workdir/.pnpm-store
RUN pnpm fetch

# STAGE 3: Copy the application code and install all deps from cache into the application
FROM fetched_deps as with_all_deps
# I use mono repo so I copy the whole project code (except for ignored things)
COPY . ./
# finally, install all the deps
RUN pnpm install --offline

# STAGE 4: Build the NextJS app
# Here we use pnpm filtering to only build the frontend app
# Then we use pnpm deploy command to prune the dependencies
FROM with_all_deps as builder
RUN pnpm --filter='*frontend' build
RUN pnpm --filter='*frontend' deploy pruned --prod

# STAGE 5: Create a clean production image - only take pruned assets
FROM node:18-alpine AS runner
WORKDIR /app
# We set the NODE_ENV to production to make sure that the NextJS app runs in production mode
ENV NODE_ENV=production
# We add a non-root user to run the app for security reasons
RUN addgroup --system --gid 1001 app
RUN adduser --system --uid 1001 app
USER app

# We copy the built NextJS app assets from the builder stage
# NextJS produces a backend server and a frontend app
COPY --chown=app:app --from=builder /app/apps/frontend/.next/standalone src/
COPY --chown=app:app --from=builder /app/apps/frontend/public src/apps/frontend/public
COPY --chown=app:app --from=builder /app/apps/frontend/.next/static src/apps/frontend/.next/static

# Set the port that the NextJS app will run on
# You should choose a port that is supported by your cloud provider
ENV PORT 5000
# Expose the port to the outside world
EXPOSE 5000

# Finally, we run the NextJS app
CMD ["node", "src/apps/frontend/server.js"]

Building the Docker Image

Now that you have your Dockerfile, you can build the Docker image.

Here we build with a tag of my-frontend-app. You can name it whatever you want. But a named tag makes it easier to run the image later.

docker build -t my-frontend-app .

Running the Docker Image

Now that you have your NextJS Docker image, you can run it.

docker run -p 5000:5000 my-frontend-app

This will run the NextJS app on port 5000. You can now access the NextJS app at http://localhost:5000.

NextJS Environment Variables

When using NextJS with docker you will have to understand how environment variables work.

Next is a bit messy compared to the last 5-10 years of frontend and backend app separation because it mixes client and server together.

You can use the .env.* files and process.env to access environment variables during build time for frontend and backend.

You can use process.env to access environment variables during runtime for backend.

But you cannot use runtime process.env environment variables in the frontend. You can only use them in the backend functions run by nodejs.

Frontend NEXT_PUBLIC_ environment variables are only available at build time and they are "baked" into the output files. This is quite common for frontend web app builds.

The values here are going to be made public so you can safely place them in .env.production file during CICD or save them in your repo if you like. NextJs will automatically load them from the relevant file at build time. If you are going to use env vars then you must set the env vars in the docker file.

You will have to come up with your own solution to use runtime variables in the frontend if you need that. A common approach is to read them from process.env on the backend and pass them in to FE as serversideprops to the frontend on request.

At the time of writing this post, you can read more commentary and solutions in this feature request: https://github.com/vercel/next.js/discussions/34894

Conclusion

That's it! You now have a NextJS app running in a Docker container.

You can now deploy this Docker image to any cloud provider that supports Containers (All of them do).

I describe how to run a node container of Dokku in this blog post.

If you have any questions, please leave a comment below.

If you want to see a real world example of NextJs in a Container, check out my NextJS Starter project. It uses Docker to deploy NextJs to Dokku on Digital Ocean.