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:
- Installing Necessary Tools: For this tutorial, we will need WSL, Docker, Docker Compose, and a Next.js project.
- Dockerfile: We will create two Dockerfiles, one for development environment and the other for production environment.
- Docker Compose: We will create a Docker Compose file to manage Docker containers.
- Publishing to Docker Hub: We will publish the image to Docker Hub to make it available for others to use.
- 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:
-t
: This flag is used to tag an image.-f
: This flag specifies the Dockerfile to use for building an image.-p
: This flag maps ports between the Docker container and the host system.
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:
- Log in to Docker Hub using the command:
docker login
- 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 .
- 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:
- View all running containers
docker ps
- View all containers, including stopped ones
docker ps -a
- To stop a container, execute the command
docker stop <CONTAINER ID>
- Stop all running Docker containers
docker stop $(docker ps -a -q)
- To remove a container, execute the command
docker rm <CONTAINER ID>
- Remove all Docker containers
docker rm $(docker ps -a -q)
- Remove all stopped Docker containers
docker container prune
- To view the images on your system, execute the command
docker image ls
- To remove an image, execute the command
docker image rm <REPOSITORY>:<TAG>
- If the image is being used by a container, you must stop and remove the container before removing the image. Another way is to add the -f flag to force removal.
docker image rm -f <REPOSITORY>:<TAG>
- To remove all images, execute the command
docker rmi $(docker images -q)