Containerizing A NextJs Application For Development
Tags: docker, nextjs
Published: 2023-04-14 16:21:04

## Introduction
Containerizing your NextJs application can be an efficient way of developing it, as it provides a more isolated and reproducible environment. In this article, we will go through the steps required to containerize a NextJs application and explain each step in detail.

## Prerequisites
Before we begin, you should have a basic knowledge of NextJs and familiarity with Docker CLI and Docker Compose commands.

> **Note**: The docker image we'll be building should not be used in a production environment.

## Creating A NextJs Application
The first step is to create a NextJs application. To do this, you can run the following command:
```bash
npx create-next-app docker-next
```
This will create a NextJs application called `docker-next` in your current working directory. 

You will be prompt to decide whether to include `TypeScript`, `TailwindCSS` and a few other things. Feel free to choose whichever options as we won't be going through NextJs in depth.

## Setting Up Docker
If you are on Windows or Mac, you can directly install [docker desktop](https://www.docker.com/products/docker-desktop/). If you are on linux, you may want to follow the instructions listed [here](https://docs.docker.com/engine/install/ubuntu/).

Once you have Docker installed, verify that the Docker CLI and Docker Compose are installed correctly by running the following commands:

```bash
docker -v
docker-compose -v
```
These commands should output the versions of Docker and Docker Compose, respectively.

## Containerizing NextJs
> We should delete our `node_modules` folder and `package-lock.json` file because it helps to reduce the size of the image and ensure that the dependencies in the image are consistent with the ones declared in the `package.json` file.

Now that we have the necessary tools to run docker, we can start containerizing our NextJs application.

### Creating A Dockerfile
The first step is to create a Dockerfile for our application. You can do this by running the following command:
```bash
touch Dockerfile
```

This command will create a new empty file named Dockerfile in your current directory.

The contents of your Dockerfile should look like this.
```
FROM node:18-alpine

WORKDIR /app

COPY package.json ./

RUN npm install

COPY . .

CMD ["npm", "run", "dev"]
```
Let's go through each line of this Dockerfile:

- **FROM node:18-alpine**: This line specifies the base image to use for our container. We're using the `node` image with version `18` that is built on top of the `alpine` Linux distribution.

- **WORKDIR /app**: This line sets the working directory for our container to `/app`. This is important to avoid clashing of folder names between our application and the container's.

- **COPY package.json ./**: This line copies the `package.json` file from our local machine to the `/app` directory inside the container.

- **RUN npm install**: This line installs the `dependencies` required by our application inside the container.

- **COPY . .**: This line copies the entire contents of our application from our local machine to the `/app` directory inside the container.

- **CMD ["npm", "run", "dev"]**: This line specifies the command to run when the container starts. In this case, we're running the `npm run dev` command, which will start our NextJs development server.

> **Note**: Replace `npm` with your choice of package manager.

### Bulding The Docker Image
Now that we have our Dockerfile, we can build the Docker image by running the following command:
```bash
docker build -t docker-next .
```
This command will build a new Docker image named `docker-next` from the Dockerfile in our current directory. The `-t` flag specifies the name of the image, and the `.` specifies the build context.
 
### Running The Container
To run the container, we can use the following command:
```bash
docker run docker-next -p 3000:3000 -v /app/node_modules -v .:/app
```
Here's what each option means:

- **-p 3000:3000**: maps port `3000` of the container to port `3000` of the host machine. This means that you can access the application by navigating to `http://localhost:3000` in your web browser.

- **-v /app/node_modules**: mounts the `/app/node_modules` directory in the container as a volume. This is useful because it allows you to take advantage of Docker's caching mechanism. Since `node_modules` is typically a large directory that doesn't change very often, mounting it as a volume means that it won't have to be rebuilt every time you make changes to your application's code.

- **-v .:/app**: mounts the current directory (i.e., the directory where you run the command) as a volume at the `/app` directory inside the container. This is where your Next.js application code lives.

So in summary, this command runs a Docker container from the `docker-next` image, maps port `3000` of the container to port `3000` of the host machine, mounts the `/app/node_modules` directory in the container as a volume, and mounts the current directory as a volume at `/app` inside the container

Once the container has run, visit `localhost:3000` in your browser and you should be greeted with the NextJs default page.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qnig9v757l39u4oiwic8.png)

Having to type this command:
```
docker run docker-next -p 3000:3000 -v /app/node_modules -v .:/app
```
every single time, will slowly chip away your sanity. That's where docker compose comes in.

### Enter Docker Compose
Docker Compose is a tool for defining and running multi-container Docker applications. It allows you to describe the services that make up your application in a YAML file, and then start and stop those services using a single command. 

With Docker Compose, you can also scale your application's services, set up networking between containers, and more. Essentially, Docker Compose makes it easier to manage and deploy multi-container Docker applications.

#### Creating The Docker Compose File
We can create the docker compose file by running:
```
touch docker-compose.yml
```

Paste this into the `docker-compose.yml` file:
```
version: '3.5'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: docker-next
    ports:
      - '3000:3000'
    volumes:
      - .:/app
      - /app/node_modules
```
First we define a service called `app`, that will be build using the Dockerfile located in our root directory and run in a container. The service exposes the container's port `3000` to the host's port `3000` and mounts two volumes:

- **.:app**: This will mount the directory where the Docker Compose file is located as a volume in the container at the `/app` directory. This is useful for development as it allows for live reloading of code changes made on the host machine to be reflected in the container.

- **/app/node_modules**: This mounts the `node_modules` directory in the container as a separate volume. This is done to avoid overriding the dependencies installed by `npm` during the image build process with the dependencies installed on the host machine.

To start our application, just run the following command:
```
docker-compose up
```

Congratulations, you just containerized a NextJs application.

## Bonus

There is one caveat developing with a containerized application, which is running `cli` commands. Specifically, in our case `npm` commands.

If you need to add new `dependencies`, you will need to run:
```
docker-compose run --rm app npm install <package name>
```

The `docker-compose run` command allows you to run a one-time command against a service defined in your `docker-compose.yml` file. The `--rm` option specifies that the container created from the service should be removed after the command is executed. This option ensures that the container is cleaned up and does not consume resources on your system after it's no longer needed.

In this case, `app` is the name of the service defined in `docker-compose.yml` that the command is being run against. 

To keep our sanity in check, we'll be building a script to help us run `docker-compose`, `npm` and `node` commands.

### Creating A Script To Run Docker Compose, NPM And Node Commands
We will be building a script based on [laravel/sail](https://laravel.com/docs/10.x/sail#introduction).

Create a file called `harbor` and make it executable.
```bash
touch harbor
chmod +x harbor
```
Paste the following into the `harbor` file:
```bash
#!/usr/bin/env bash
function display_help {
  echo "Harbor"
  echo
  echo "Usage:" >&2
  echo "  harbor COMMAND [options] [arguments]"
  echo
  echo "Unknown commands are passed to the docker-compose binary."
  echo
  echo "docker-compose Commands:"
  echo "  harbor up        Start the application"
  echo "  harbor up -d     Start the application in the background"
  echo "  harbor stop      Stop the application"
  echo "  harbor down      Stop the application and remove related resources"
  echo "  harbor restart   Restart the application"
  echo "  harbor ps        Display the status of all containers"
  echo
  echo "Node Commands:"
  echo "  harbor node ...         Run a Node command"
  echo "  harbor node --version"
  echo
  echo "Npm Commands:"
  echo "  harbor npm ...        Run a Npm command"
  echo "  harbor npm test"
  echo
  echo "Customization:"
  echo "  harbor build --no-cache       Rebuild all of the harbor containers"

  exit 1
}

if [ $# -gt 0 ]; then
  if [ "$1" == "npm" ]; then
    shift 1
    docker-compose run --rm app npm "$@"
  elif [ "$1" == "node" ]; then
    shift 1
    docker-compose run --rm app node "$@"
  elif [ "$1" == "help" ] || [ "$1" == "--help" ] || [ "$1" == "-h" ]; then
    display_help
  else
    docker-compose -f docker/dev/docker-compose.yml "$@"
  fi
else
  display_help
fi
```
The `display_help` function is defined first, which prints out the available commands, options, and arguments to the user.

The script then checks if there is at least one command-line argument `($# -gt 0)`. If there is, it checks if the first argument is `npm` or `node`. If the argument is `npm` or `node`, it runs the corresponding command using `docker-compose run --rm app`. If the argument is `help`, `--help`, or `-h`, it prints out the help information using the `display_help` function. Otherwise, it passes the command and any arguments to `docker-compose` to execute.

If there are no command-line arguments, the script also prints out the help information using the `display_help` function.

The `help` output will look like this.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f10beb1kfphr9xuiezio.png)

Now we are able to run `npm` and `node` commands by using the script that we just created. You can take it up a notch and include conditions for `yarn` and `pnpm` or put everything in an `npm` package and publish it, since it's basically framework agnostic.

## Conclusion
Containerizing a Next.js application with Docker provides a more isolated and reproducible environment, making it an efficient way of developing and onboarding new developers. 

Hopefully, this article has helped provide some clarity and deepen your understanding regarding docker. If you have any feedback or comments, feel free to leave them in the comments section.

If you enjoyed this article, consider [tipping me](https://ko-fi.com/scaliaabel).

> **Note**: Link to [GitHub Repository](https://github.com/scaabel/docker-next)