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
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
These commands should output the versions of Docker and Docker Compose, respectively.
Containerizing NextJs
We should delete our
node_modules
folder andpackage-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 thepackage.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
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 version18
that is built on top of thealpine
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 .
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
Here's what each option means:
-p 3000:3000: maps port
3000
of the container to port3000
of the host machine. This means that you can access the application by navigating tohttp://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. Sincenode_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.
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 bynpm
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.
Create a file called harbor
and make it executable.
touch harbor
chmod +x harbor
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
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.
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)
THanks a lot, just what I needed
Glad I could help.
You must add EXPOSE field to docker file, it will be opened a port
helpful blog!
medium.com/@elifront/best-next-js-...