DEV Community

Scalia Abel
Scalia Abel

Posted on • Updated on

Containerizing A NextJs Application For Development

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:

npx create-next-app docker-next
Enter fullscreen mode Exit fullscreen mode

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. If you are on linux, you may want to follow the instructions listed here.

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

docker -v
docker-compose -v
Enter fullscreen mode Exit fullscreen mode

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:

touch Dockerfile
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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:

docker build -t docker-next .
Enter fullscreen mode Exit fullscreen mode

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:

docker run docker-next -p 3000:3000 -v /app/node_modules -v .:/app
Enter fullscreen mode Exit fullscreen mode

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

Having to type this command:

docker run docker-next -p 3000:3000 -v /app/node_modules -v .:/app
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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.

Create a file called harbor and make it executable.

touch harbor
chmod +x harbor
Enter fullscreen mode Exit fullscreen mode

Paste the following into the harbor file:

#!/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
Enter fullscreen mode Exit fullscreen mode

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

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.

Note: Link to GitHub Repository

Top comments (5)

Collapse
 
akuoko_konadu profile image
Konadu Akwasi Akuoko

THanks a lot, just what I needed

Collapse
 
scaabel profile image
Scalia Abel

Glad I could help.

Collapse
 
vitya_obolonsky profile image
viktor_k

You must add EXPOSE field to docker file, it will be opened a port

Collapse
 
dreamwork999 profile image
dreamwork999

helpful blog!

Collapse
 
elifront profile image
Eli Front