Awwwards Nominee
← Back to all posts9 min read

Dockerize a Next.js app


Introduction

Docker is a platform that allows developers to create, deploy, and run applications in a containerized environment. Containerization helps to isolate the application from the host operating system and provides a consistent environment for the application to run in. In this blog post, I will discuss how to use Docker to containerize your Next.js application, show you a simple Dockerfile, show you a multi-stage Dockerfile, and how to use a Dockerfile with docker-compose.

Benefits of using Next.js and Docker

By using Next.js and Docker together, you can take advantage of the benefits of both technologies. Some of these benefits include:

  • Scalability:
  • Portability:
    • Docker containers can be easily moved between environments, making it easy to deploy your application to different environments.
    • Next.js makes it easy to share your application with others by providing a consistent experience across different devices as well as the ability to deploy on a CDN.
  • Performance:
    • Docker containers provide a consistent environment for your application to run in, which can also improve performance.
    • Next.js provides a multitude of options like caching, server side rendering, and incremental static regeneration, which can improve the performance of your application.

Prerequisities

Before we dive into the process of doing that, make sure that you have the following installed on your system:

Creating a Next.js application

If you don't have an existing Next.js application, you can create one by running the following commands in your terminal:

npx create-next-app@latest

On installation you will see the following prompts:

What is your project named?
    docker-next-js
 Would you like to use TypeScript with this project?
    No
 Would you like to use ESLint with this project?
    Yes
 Would you like to use Tailwind CSS with this project?
    No
 Would you like to use `src/` directory with this project?
    Yes
 Use App Router (recommended)?
    Yes
 Would you like to customize the default import alias?
    No

This will create a new Next.js application in the docker-next-js directory. We can navigate into this directory and start the development server by running. Where we can see it running locally on localhost:3000

 cd docker-next-js
  npm run dev

Dockerizing Next.js

To dockerize a Next.js application, we need to do a couple of things. First we need to create a Dockerfile in the root directory of our project. The Dockerfile contains instructions on how to build the Docker image similar to how a software engineer would start and build the app for production.

Simple Dockerfile

To get a simple Dockerfile up and running we can create a Dockerfile with the following content:

  # Base Image
  FROM node:14-alpine

  # Set working directory
  WORKDIR /app

  # Install app dependencies
  COPY package*.json ./
  RUN npm install

  # Copy app files
  COPY . .

  # Build app
  RUN npm run build

  # Expose port
  EXPOSE 3000

  # Start app
  CMD ["npm", "run", "start"]

Let's go through the Dockerfile step by step:

  1. FROM node:14-alpine
    • This sets the base image to use for the Docker image. In this case, the node:14-alpine image is pulled from Dockerhub
  2. WORKDIR /app
    • This sets the working directory to /app inside the container.
  3. COPY package*.json ./
    • This copies the local repository's package.json and package-lock.json files into the Docker container.
  4. RUN npm install
    • This installs the application dependencies inside the container.
  5. COPY . .
    • This copies all the files from the current local directory to the container.
  6. RUN npm run build
    • This builds the Next.js application inside the Docker container.
  7. EXPOSE 3000
    • This exposes the Docker network's port 3000 within the container.
  8. CMD ["npm", "run", "start"]
    • This is the final step that starts the Next.js application from inside the container.

Now that we have our Dockerfile, we can build the Docker image by running the following command in the terminal:

# This will build the Docker image and tag it with the name `docker-next-js`
docker build -t docker-next-js .

# To run the Docker image we will use
docker run -p 3000:3000 docker-next-js

This will start the container and use Docker's internal network to map the default Next.js port 3000 on the host machine. You should be able to navigate to localhost:3000 or 0.0.0.0:3000 to see your built Next.js app.

While most Software Engineers would be fine that Docker is up-and-running, if we take a look at the size of the Docker image it's a HUMONGOUS 1.17GB BEAST. I've done this when I was early in my career and didn't know any better 😬. Trust me you don't want to be the engineer that pushes up this beast of a container.

So how do we reduce the container size? Simple we use the multi-stage build as recommended by Docker.

Multi-stage Dockerfile

We're going to break this down into two separate chunks (or stages if you like that terminology).The 1st chunk is the installation and build portion, the 2nd chunk is just copying over the necessary built Next.js files to serve (everything in the ./public and ./standalone directories)

# Chunk 1
#
# Step 1. Use node-18-alpine as the base image
FROM node:18-alpine AS base

# Step 2. Rebuild the source code only when needed
FROM base AS builder

# Step 3. Set working directory to `/app`
WORKDIR /app

# Step 4. Copy over the `package.json` and any lock files for a users' preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./

# Step 5. Install dependencies based on the preferred package manager
RUN \
  if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
  elif [ -f package-lock.json ]; then npm ci; \
  elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i; \
  # Allow install without lockfile, so example works even without Node.js installed locally
  else echo "Warning: Lockfile not found. It is recommended to commit lockfiles to version control." && yarn install; \
  fi

# Step 6. Copy all the files from the local repo into the Docker container
COPY . .

# Step 7. Optional, disables Nextjs telemetry
ENV NEXT_TELEMETRY_DISABLED 1

# Step 8. Build Next.js based on the preferred package manager
RUN \
  if [ -f yarn.lock ]; then yarn build; \
  elif [ -f package-lock.json ]; then npm run build; \
  elif [ -f pnpm-lock.yaml ]; then pnpm build; \
  else yarn build; \
  fi
# End of Chunk 1

# Chunk 2
# Step 1. Get the production image we just built in the Chunk 1
FROM base AS runner

# Step 2. Set the working directory to `/app`
WORKDIR /app

# Step 3. We don't want to run production as the `root` user
# so we create a `nextjs` user and add it to the `nodejs` group.
# This is recommended to prevent unauthorized use if your app is compromised
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# Step 4. We change from `root` to `nextjs` user
USER nextjs

# Step 5. Copy all the necessary files generated by Next.js
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

# Step 6. Optional, disable nextjs telemetry
ENV NEXT_TELEMETRY_DISABLED 1

# Step 7. Optional but recommended to set the production environment
# PORT and HOSTNAME environment variables
ENV PORT 3001
ENV HOSTNAME localhost

# Step 8. We run the `server.js` that is generated from `./next/standalone/server.js`
CMD ["node", "server.js"]

Let's go ahead and build our multi-stage Dockerfile to compare against our original.

# This will build the Docker image and tag it with the name `docker-next-js`
docker build -t docker-next-js-2 .

So while our individual Dockerfile looks like a lot more lines, what we have done is actually trimmed unnecessary files. By doing so our 1.17 GB becomes a mere 210 MB which is a decrease of almost 82% of the container 😎(and yes, your DevOps Engineers will thank you)

Setting up our docker-compose.yml

So now that we have a smaller container let's setup a docker-compose.yml file which we can use to define and run our containers. This also helps to not have to remember all the nuances and commands of the docker CLI.

First let's create a docker-compose.yml in the root directory right next to our Dockerfile

version: "3"
services:
  next-app:
    container_name: docker-next-js
    build:
      context: .
      dockerfile: Dockerfile
    restart: always
    ports:
      - 3001:3001

This configuration defines a single service called next-app. The build property specifies the context and location of the Dockerfile, while the ports property marries the Docker network ports to the ports on the local machine.

Now all we have to run to build our application and start it is:

docker compose up --build

Conclusion

In this blog post, we discussed how to use Docker to dockerize a Next.js application. Dockerizing applications provides many benefits, including portability and consistency across different environments. By using Next.js and Docker together, you can take advantage of the benefits of both of these awesome technologies.

With the Dockerfile and commands provided, you can easily dockerize your own Next.js applications. By using Docker, you can easily share your application with others, without needing to worry about the environment in which the application will be run. This makes it easier to collaborate with other developers, as well as deploy your application to different environments.

Overall, Dockerizing a Next.js application is a straightforward process that can provide many benefits. As more and more developers adopt Docker as a way to deploy their applications, it's important to stay up-to-date with the latest best practices and trends in the industry.

Good luck Dockerizing your Next.js application!

Here are some other resources for further reading:

If you enjoyed this article please feel free to connect with me on Dev.to or on LinkedIn