Dockerize a Next.js App

Itsuki
6 min readJun 28, 2023

--

If you want to deploy your Next.js App on Vercel, you will not need a container in this case. Since Next.js is created and maintained by Vercel, you can do your deployment with ease. However, if you are looking into running your App through AWS, Google Cloud Run or other cloud providers, you will need a container.

Most of the articles I found online explains how to dockerize a Node.js App, but not much focusing on a Next.js App. There are solutions out there, but those solutions gives me some errors that took me hours in figuring out. So I am just going to share the way I did it, the problems I encoutered, and my solutions to those.

Getting Started!

Start with any existing Next.Js project you have or simply clone one from one of the examples provided officially by Vercel. Some other solutions I found online suggested that configuring the application to build a standalone application through adding the following lines to next.config.js is a must, but I found everything worked fine without adding it.

const nextCofig = {
output: 'standalone', // mine worked fine without this line
// ... other config
}

DockerFile

Add a Dockerfile to your root repository with exactly this name. In this file, we will add instructions for the docker image we will be building.

I will share two versions of Dockerfile I created, one super simple and basic single stage one just for testing the developemnt stage/server, and another multi-stage one together with a docker-compose.yml that can be used both for production and development.

Single-stage DockerFile

Here is how the file look like and I will explain what I did line by line.

FROM node:18

WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD npm run dev

First of all, We tell the Docker to use the official Docker Node image version 18 through From node:18 , changing what comes after FROM to change the image to the one you want to use. You can find a list of supported ones here.

The WORKDIR sets the context for subsequent commands. You can name it whatever you like, I called mine app . We then copy the package.json and package-lock.json files to the container and RUN npm install will install all dependencies for us.

After that, we copy all the code from our project (current root directory) to our WORKDIR , which is /app in my case.

Expose 3000 tells the container that out app runs on the port 3000.

After all the setting finish, we have CMD npm run dev to ask the container to start the development server.

AND THAT’S IT for the Dockerfile!!

To build the docker Image from the Dockerfile we just created, simply run the following command. I name it my-app, but feel free to change it to what ever you want. Also don’t forget the . at last.

docker build -t my-app .

Once the image is created, we can run it by

docker run -p 3000:3000 my-app

3000:3000 specify the the port you want to run the app on, I will run mine on 3000. And if you access http://localhost:3000 , you will see your working app!

Multi-stage DockerFile

We will now be working on creating a multi-stage Dockerfile that can make our builds faster and more efficient, and allow us to switch between production and developement stage easily.

Delete whatever you have in your Dockerfile and replace it with the following.

FROM node:18-alpine as base
RUN apk add --no-cache g++ make py3-pip libc6-compat
WORKDIR /app
COPY package*.json ./
EXPOSE 3000

FROM base as builder
WORKDIR /app
COPY . .
RUN npm run build


FROM base as production
WORKDIR /app

ENV NODE_ENV=production
RUN npm ci

RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
USER nextjs


COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/public ./public

CMD npm start

FROM base as dev
ENV NODE_ENV=development
RUN npm install
COPY . .
CMD npm run dev

Let’s use node:18-alpine in this case. It is a lot smaller than the main one. For -alpine to work , you will have to have python installed before everything else, and that is why we have that extra line of RUN apk add — no-cache g++ make py3-pip libc6-compat. We put our common settings in the base stage, so that we can reuse it in other stages later on. You can change the name of the stage by changing whatever comes after as .

The builder stage is basically just in charge of npm run build. This stage will be called when we try to COPY — from=builder in the production stage if those files did not already exist. See? Here is where a multi-stage Dockerfile comes in handy. The builder stage is only called when needed.

In our production stage, we set NODE_ENV to production and it is said that doing this will make the performance three times better. Next, we run npm ci, which is trargeting for continuous integration, instead of npm install .

We then add a non-root user to run the app for security reasons. I called by user nextjs and group nodejs because I am lazy.

After that, we copied the assets needed from builder stage by calling COPY — from=builder.

End finally, we start our start our application by calling npm start.

In our dev stage, we are basically doing the same thing as what we did in a single-stage Dockerfile so I am just going to skip it.

Before we moving onto creating a docker-compose.yml, if you want to check if your Dockerfile actually build, you can run docker build -t my-app . and docker run -p 3000:3000 my-app . Don’t forget to comment out the stage you don’t want to test on. For example, if you want to see if your production stage can be built and run successfully, simply comment out everything coming after FROM base as dev .

Docker Compose

With Docker Compose, we don’t need to remember all those long commands to build or run containers. We can simply just use docker-compose build and docker-compose up .

Add a docker-compose.yml file to your root directory with the following conent.

version: '3.8'
services:
app:
image: openai-demo-app
build:
context: ./
target: dev
dockerfile: Dockerfile
volumes:
- .:/app
- /app/node_modules
- /app/.next
ports:
- "3000:3000"

version : '3.8' sepcifies the version of Docker Compose we want to use. I only have one service in this case and that will be app, but you can add more to it based on your needs.

Build context specifies the current directory and target specifies the stage you want to build the Docker image with. If you want to run in production, simply set target:production.

Volume tells Docker to copy contents from the local directory ./ of the host to /app directory on the Docker container.

Finally, we map port 3000 on the host machine to port 3000 of the container. We exposed port 3000 when we built the container, and our app will also runs on 3000.

Testing with Docker Compose

We finally made our way to building the Docker image. We are going to use Docker’s BuildKit feature for a faster build.

COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose build

After the build finish, we can run the image and start the app by

docker-compose up

After that, if you go to http://localhost:3000 on your browser, you will see your app running!!!

That’s it!

Below are just some problems I encountered. I am going to share those with you really quick in case you are struggling on getting your docker container to work and trying to find out where might the problems be.

Problems I encountered

  1. Python is not set from command line or npm configuration: (As a mac user) I tried to add python to path, alias python to python3, uninstall and reinstall python, etc.etc.etc. NONE OF THOSE worked out for me. So here are the two solutions I found. (1) use node:18(or any other number you prefer) instead of node:18-alpine. (2) add RUN apk add — no-cache g++ make py3-pip libc6-compat before you do any of the installations of your packages.
  2. Could not find a production build in the ‘/app/.next’ directory when using docker-compose up: Everything worked as expected if I ran the image through docker run. There are solutions out there suggesting add CMD ["npm","run","build"] right before CMD npm start in the Dockerfile. However, this gave me an error saying no two CMD allowed. My solution was to add — /app/.next to volumes in Docker-compose.yml.

That’s all I have for today!

Thank you for reading! Hope those tips can help!

--

--