Dockerizing Next.js
Published: 2024-04-20 20:35:27

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](#installing-necessary-tools)**: For this tutorial, we will need WSL, Docker, Docker Compose, and a Next.js project.
2. **[Dockerfile](#dockerfile)**: We will create two Dockerfiles, one for development environment and the other for production environment.
3. **[Docker Compose](#docker-compose)**: We will create a Docker Compose file to manage Docker containers.
4. **[Publishing to Docker Hub](#publishing-to-docker-hub)**: We will publish the image to Docker Hub to make it available for others to use.
5. **[Docker Cheat Sheet](#docker-cheat-sheet)**: A list of useful commands for managing Docker containers.

You can find the source code for this tutorial on [GitHub](https://github.com/renanleonel/demo-docker-nextjs)

### 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:

```bash
wsl --install
```

If you encounter any issues during installation, you can refer to the [official tutorial](https://docs.microsoft.com/en-us/windows/wsl/install) 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](https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-22-04) and [Docker Compose](https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-compose-on-ubuntu-22-04) 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](https://github.com/renanleonel/nextjs-template) 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:

```yaml
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:

```yaml
# 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.

```yaml
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:

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

To execute, use the command:

```bash
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:

```bash
docker ps
```

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

```bash
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.

```yaml
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:

```yaml
# 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:

```bash
docker-compose up prod
```

Or, in development mode:

```bash
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](https://hub.docker.com/) 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:

```bash
docker login
```

2. Build the image using the command:

```bash
docker build -t <DOCKERHUB_USERNAME>/<REPOSITORY>:<TAG> -f <DOCKERFILE> .
```

In my case, the command would be:

```bash
docker build -t renanleonel/demo-docker-nextjs-prod:v1 -f Dockerfile.prod .
```

3. Push the image to Docker Hub using the command:

```bash
docker push <DOCKERHUB_USERNAME>/<REPOSITORY>:<TAG>
```

We can view the image through the [link](https://hub.docker.com/r/renanleonel/demo-docker-nextjs-prod) on Docker Hub. To run the image in any environment that supports Docker, we should download the image using the command:

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

and then run the container:

```bash
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

```bash
docker ps
```

- View all containers, including stopped ones

```bash
docker ps -a
```

- To stop a container, execute the command

```bash
docker stop <CONTAINER ID>
```

- Stop all running Docker containers

```bash
docker stop $(docker ps -a -q)
```

- To remove a container, execute the command

```bash
docker rm <CONTAINER ID>
```

- Remove all Docker containers

```bash
docker rm $(docker ps -a -q)
```

- Remove all stopped Docker containers

```bash
docker container prune
```

- To view the images on your system, execute the command

```bash
docker image ls
```

- To remove an image, execute the command

```bash
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.

```bash
docker image rm -f <REPOSITORY>:<TAG>
```

- To remove all images, execute the command

```bash
docker rmi $(docker images -q)
```