Create a Docker image of a Next.js application

Create a Docker image of a Next.js application
Photo by Joanna Kosinska / Unsplash

Photo by Joanna Kosinska / Unsplash

Next.js is the most popular React Framework for building a web application with remarkable UX and performance. Developed by Vercel, which gives massive importance to the developer experience.

Deploying applications is challenging and time-consuming; developers should focus on building features that bring value to their customers. Vercel makes it easy to deploy your application by connecting from GitHub, GitLab, and BitBucket.

Vercel also supports the deployment of many libraries and Frameworks.

Front-end libraries and Framework supported by Vercel
Front-end libraries and Frameworks are supported by Vercel.

When do you need a Docker Image?

With Vercel making the deployment of front-end applications and particularly Next.js easier, you might wonder why you need to create a Docker image? Here are some use cases:

  • If you want to show a demo of the whole application to teammates, dockerizing the project can be faster than setting up an entire environment.
  • You deploy your application on an enterprise server and don't want to host it on a third-party cloud hosting.
  • A web hosting platform like Vercel requires a Team plan when the project is inside an organization. For a non-profit organization like OSS Cameroon with a limited resources, building a Docker image can be a solution.

Prerequisites

You must need these tools installed on your computer to follow this tutorial.

Create a Next.js application

As an example of Next.js application, I found this awesome repository on GitHub tailwind-nextjs-starter-blog. It's a blog template starter built with Tailwind CSS and Markdown. Let's clone it and set it up locally:

git clone https://github.com/timlrx/tailwind-nextjs-starter-blog.git my-blog

cd my-blog

yarn install

yarn dev

You should create a .env from the file .env.example and provide the value for each key; fortunately, the blog still works if you don't give any.

Open your favorite browser and navigate to http://localhost:3000; you get the output below:

Next.js application running locally.
Next.js application running locally.

Write the Dockerfile

The goal is to build the image that we will start a Docker container that exposes a port to serve the application. We will take advantage of a multi-stage build to create a lighter image.

Create a Dockerfile in the project folder and add the code below:

# Install dependencies only when needed
FROM node:16-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile

# Rebuild the source code only when needed
FROM node:16-alpine AS builder

WORKDIR /app

COPY --from=deps /app/node_modules ./node_modules

COPY . .

RUN yarn build

# Production image, copy all the files and run next
FROM node:16-alpine AS runner
WORKDIR /app

ENV NODE_ENV production

RUN addgroup --system --gid 1001 bloggroup
RUN adduser --system --uid 1001 bloguser

COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json

# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=bloguser:bloggroup /app/.next/standalone ./
COPY --from=builder --chown=bloguser:bloggroup /app/.next/static ./.next/static

USER bloguser

EXPOSE 3000

ENV PORT 3000

CMD ["node", "server.js"]

The Docker build has three stages:

  • deps: we install the node dependencies; this has the advantage to speed because this stage will run only if a Node dependency has been added, updated, or removed; otherwise, it will reuse the cached Node modules.
  • builder: We copy the Node modules folder from the previous stage (deps); copy all the project folders and files, and build the application for the production
  • runner: we define a user and a group, set the user as the owner of all the files; this prevents to running the image with the root user. We copy the build output folder, expose the port 3000 and start the Node server.

Note: you can give the name you want to the Docker stages.

Update the file next.config.js to allow standalone output:

{
  // previous command here
  experimental: {
    outputStandalone: true,
  },   
}

Create a file .dockerignore in the root folder; it is helpful to exclude unnecessary directories and files for the image build. Add the content below inside:

node_modules
.next
.github
.husky

Run the command below to build the Docker image:

docker build -t blog-site:v1 .

When the build is complete, you can use the command docker image ls to view the Docker image:

View the Docker image
View the Docker image

The image size would have been bigger if we didn't use the standalone output.

Run the Docker image

The image exposes the port 3000, so we should map it to a port of the physical computer, say 8080. The command to start a Docker container from the image is:


docker run --rm -it -p 8080:3000 --name my-blog blog-site:v1

Open your favorite browser and navigate to http://localhost:8080; you get the same page as before:

Next.js application running in a Docker container.
Next.js application running in a Docker container.

Handle environment variables

We build the image with a local configuration where environment variables aren't required, but for production, you need to set these values like the Google Analytics ID and the website URL. Since the .env file is not versioned, how do we add these variables in the Docker context?

We can use Docker build arguments to provide these variables:

Let's say we want to set these two environment variables:

Variable Value
GOOGLE_ANALYTICS_ID GA-1234567
SITE_URL https://myblog.dev

Let's update the stage builder in the Dockerfile to set the environment variables read from the build arg:

# Rebuild the source code only when needed
FROM node:16-alpine AS builder

ARG GOOGLE_ANALYTICS_ID=""
ENV NEXT_PUBLIC_GA_ID=$GOOGLE_ANALYTICS_ID
ARG SITE_URL="https://myblog.dev"
ENV NEXT_PUBLIC_SITE_URL=$SITE_URL

WORKDIR /app

COPY --from=deps /app/node_modules ./node_modules

COPY . .

RUN yarn build

We use the command ARG to define a build argument and set a default value if the argument is not provided.

We use the command ENV to create an environment variable and set the value to the build argument value.

Now the command to build the image is the following:


docker build -t blog-site:v1 --build-arg GOOGLE_ANALYTICS_ID=ga_id --build-arg SITE_URL=https://myblog.dev .

You can provide as many arguments as you want.

Deploy on a Virtual Private Server

Interested to learn how to deploy this docker image on a Virtual Private Server? Here is the process:

  • Push the image to the Docker Hub
  • Connect to the server through SSH and pull the image from the Docker Hub
  • Start a container from the image
  • Create a DNS Record
  • Configure a Reverse proxy with Nginx or Apache or Caddy

I show how to do these steps in the post below, starting at the section "Push the image to the Docker Hub."

Deploy a Spring Boot application with Docker and Nginx Reverse Proxy
In this post, we will see how to deploy a Spring Boot application packaged with Docker, configure a reverse proxy with Nginx, and finally secure it with Lets Encrypt.

Wrap up

Although the outstanding experience provided by Vercel, you can still need to package your Next.js application on your own and deploy it on a server. Docker has the advantage of giving a reproducible build and is also agnostic of the operating system.

We can use the standalone output to reduce the bundle size; to learn more about it, check out the Next.js documentation-related page.

You can find the code source on the GitHub repository.

Follow me on Twitter or subscribe to my newsletter to avoid missing the upcoming posts and the tips and tricks I occasionally share.