home posts projects
GitHub LinkedIn DEV Community

Dockerizing Next.js

April 20, 2024 (11d ago)

This tutorial will cover the step-by-step process of containerizing a Next.js application using Docker. We will follow these steps:

  1. Installing Necessary Tools: For this tutorial, we will need WSL, Docker, Docker Compose, and a Next.js project.
  2. Dockerfile: We will create two Dockerfiles, one for development environment and the other for production environment.
  3. Docker Compose: We will create a Docker Compose file to manage Docker containers.
  4. Publishing to Docker Hub: We will publish the image to Docker Hub to make it available for others to use.
  5. Docker Cheat Sheet: A list of useful commands for managing Docker containers.

You can find the source code for this tutorial on GitHub

You can also find this article on DEV and Medium.

Installing Necessary Tools

This tutorial assumes that you’re using Windows as your operating system. If you’re using a different operating system, the commands may vary.

Firstly, we need to install WSL (Windows Subsystem for Linux), which is a feature of Windows that allows you to run a Linux environment directly on your Windows computer without the need for a virtual machine. To do this, open PowerShell as an administrator and execute the following command:

 wsl --install 

If you encounter any issues during installation, you can refer to the official tutorial for WSL installation provided by Microsoft.

Now, we can proceed to install Docker and Docker Compose.

Docker is a virtualization platform that encapsulates applications in isolated containers, providing consistency and efficiency in development. This allows the application to run autonomously and consistently in any environment, eliminating conflicts between different software versions.

For installation, follow the comprehensive tutorials created by Digital Ocean for Docker and Docker Compose on Ubuntu.

Finally, create a Next.js application using the command: npx create-next-app@latest or use an existing project. For this tutorial, we will use this template repository that I use for all my projects, using pnpm as the package manager.

Dockerfile

The Dockerfile is a configuration file that contains all the necessary commands to build an image. For this tutorial, we will create two files: one for the development environment and another for the production environment.

First, create a Dockerfile.prod file in the root directory of your project. Then, add the following code:

 FROM node:20 AS base
WORKDIR /app
RUN npm i -g pnpm
COPY package.json pnpm-lock.yaml ./

RUN pnpm install

COPY . .
RUN pnpm build

FROM node:20-alpine3.19 as release
WORKDIR /app
RUN npm i -g pnpm

COPY --from=base /app/node_modules ./node_modules
COPY --from=base /app/package.json ./package.json
COPY --from=base /app/.next ./.next

EXPOSE 3000

CMD ["pnpm", "start"] 

Now, let’s analyze the commands in the Dockerfile.prod file:

 # Define the base image as node:20 and name it as base.
FROM node:20 AS base

# Set the working directory inside the container to /app.
# We need to set the working directory so Docker knows where to run the commands.
WORKDIR /app

# Globally install the package manager pnpm.
RUN npm i -g pnpm

# Copy the package.json and pnpm-lock.yaml files to the working directory in the container.
# This command is necessary for Docker to install project dependencies.
COPY package.json pnpm-lock.yaml ./

# Install project dependencies using pnpm.
RUN pnpm install

# Copy all files from the context directory (where the Dockerfile is located) to the working directory in the container.
COPY . .

# Run the project build command using pnpm.
RUN pnpm build

# Define a second stage of the image based on node:20-alpine3.19 and name it as release.
# Alpine image is a lighter version of node, which helps reduce the final image size.
FROM node:20-alpine3.19 as release

# Set the working directory inside the container to /app.
WORKDIR /app

# Globally install the package manager pnpm.
RUN npm i -g pnpm

# Copy the node_modules folder from the base stage to the node_modules directory in the release stage.
COPY --from=base /app/node_modules ./node_modules

# Copy the package.json file from the base stage to the current directory in the release stage.
COPY --from=base /app/package.json ./package.json

# Copy the .next folder from the base stage to the .next directory in the release stage.
COPY --from=base /app/.next ./.next


# Define the default command to be executed when the container is started with pnpm start.
CMD ["pnpm", "start"] 

Next, create a Dockerfile.dev file in the root of your project.

 FROM node:20 AS base
WORKDIR /app
RUN npm i -g pnpm
COPY package.json pnpm-lock.yaml ./

RUN pnpm install

COPY . .

FROM node:20-alpine3.19 as release
WORKDIR /app
RUN npm i -g pnpm

COPY --from=base /app/node_modules ./node_modules
COPY --from=base /app/package.json ./package.json
COPY --from=base /app/.next ./.next
COPY --from=base /app/src ./src
COPY --from=base /app .

EXPOSE 3000

CMD ["pnpm", "dev"] 

We can test if the Dockerfile is working correctly by building our application using the command:

 docker build -t nextjs:v1 -f Dockerfile.prod . 

To execute, use the command:

 docker run -p 3000:3000 nextjs:v1 

It’s worth highlighting the meaning of the flags used:

Upon accessing http://localhost:3000, you should see the Next.js application running inside a Docker container. You can also verify if the container is running correctly by executing the command:

 docker ps 

If everything is working correctly, you should see something like this:

 CONTAINER ID   IMAGE        COMMAND                  CREATED         STATUS         PORTS                                       NAMES
88c1975e087b   nextjs:v1   "docker-entrypoint.s…"   7 seconds ago   Up 6 seconds   0.0.0.0:3000->3000/tcp, :::3000->3000/tcp   festive_leavitt 

However, hot reload isn’t working when using the Dockerfile.dev because the container is not watching for changes in the files.

Furthermore, manually executing these commands every time we want to run the application is a tedious process. To address these issues, let’s use Docker Compose.

Docker Compose

Docker Compose is a tool that simplifies the management of multiple Docker containers, allowing you to define and run them all with a single command, simplifying the configuration and management of multi-container applications.

Additionally, we have access to the concept of volumes, which allow data persistence between containers and the host, ensuring that data is not lost when containers are restarted or removed and allowing changes in files to be reflected in the container in real-time. Furthermore, Docker Compose allows the definition of environment variables in a .env file to facilitate the configuration of the application.

To use Docker Compose, we need to create a configuration file called docker-compose.yml. This file defines the services that will be run by Docker Compose, as well as the configurations for each service.

 version: "3.7"

services:
  dev:
    build:
      context: .
      dockerfile: Dockerfile.dev
    container_name: demo-docker-nextjs-dev
    environment:
      - WATCHPACK_POLLING=true
    volumes:
      - .:/app
      - /app/node_modules
      - /app/.next
    ports:
      - "3000:3000"
    env_file:
      - .env.local
  prod:
    build:
      context: .
      dockerfile: Dockerfile.prod
    container_name: demo-docker-nextjs
    ports:
      - "3000:3000"
    env_file:
      - .env.local

volumes:
  node_modules: 

Let’s analyze the commands in the docker-compose.yml file:

 # Define the version of Docker Compose.
version: "3.7"

# Define the services to be run by Docker Compose.
services:
  # Define the dev service.
  # This service will be used to run the application in the development environment.
  dev:
    # Define the build context as the current directory and the Dockerfile to be used as Dockerfile.dev.
    build:
      context: .
      dockerfile: Dockerfile.dev
    # Define the container name.
    container_name: demo-docker-nextjs-dev
    # Define the environment variable WATCHPACK_POLLING as true. This is necessary for hot reload to work correctly.
    environment:
      - WATCHPACK_POLLING=true
    # Define the volumes to be mounted in the container.
    # The volume .:/app maps the current working directory to the /app directory in the container.
    # The volume /app/node_modules is used to persist project dependencies between containers.
    # The volume /app/.next is used to persist files generated by Next.js between containers.
    volumes:
      - .:/app
      - /app/node_modules
      - /app/.next
    # Maps the ports to be exposed on the host.
    ports:
      - "3000:3000"
    # Define the environment file to be used.
    env_file:
      - .env.local
  # Define the prod service.
  # This service will be used to run the application in the production environment.
  prod:
    # Define the build context as the current directory and the Dockerfile to be used as Dockerfile.prod.
    build:
      context: .
      dockerfile: Dockerfile.prod
    # Define the container name.
    container_name: demo-docker-nextjs
    # Maps the ports to be exposed on the host.
    ports:
      - "3000:3000"
    # Define the environment file to be used.
    env_file:
      - .env.local

# Define the volumes to be used.
# The node_modules volume is used to persist project dependencies between containers.
volumes:
  node_modules: 

To run the application using Docker Compose, execute the command:

 docker-compose up prod 

Or, in development mode:

 docker-compose up dev 

Now, upon accessing http://localhost:3000, you should see the Next.js application running inside a Docker container with hot reload functioning in the development environment. Try making a change to any file and observe the changes being reflected in the container in real-time.

Publishing to Docker Hub

Finally, we can upload our application to Docker Hub so that other people can use the image we created. To do this, follow the steps below:

  1. Log in to Docker Hub using the command:
 docker login 
  1. Build the image using the command:
 docker build -t <DOCKERHUB_USERNAME>/<REPOSITORY>:<TAG> -f <DOCKERFILE> . 

In my case, the command would be:

 docker build -t renanleonel/demo-docker-nextjs-prod:v1 -f Dockerfile.prod . 
  1. Push the image to Docker Hub using the command:
 docker push <DOCKERHUB_USERNAME>/<REPOSITORY>:<TAG> 

We can view the image through the link on Docker Hub. To run the image in any environment that supports Docker, we should download the image using the command:

 docker push renanleonel/demo-docker-nextjs-prod:v1 

and then run the container:

 docker run -p 3000:3000 renanleonel/demo-docker-nextjs-prod:v1 

Now your Next.js application is containerized and ready to be used in any environment that supports Docker.

Docker Cheat Sheet

Here are some useful commands for managing Docker containers:

 docker ps 
 docker ps -a 
 docker stop <CONTAINER ID> 
 docker stop $(docker ps -a -q) 
 docker rm <CONTAINER ID> 
 docker rm $(docker ps -a -q) 
 docker container prune 
 docker image ls 
 docker image rm <REPOSITORY>:<TAG> 
 docker image rm -f <REPOSITORY>:<TAG> 
 docker rmi $(docker images -q)