DEV Community

Cover image for Nx + NextJS + Docker: Containerizing our application
Sebastián Duque G
Sebastián Duque G

Posted on • Updated on

Nx + NextJS + Docker: Containerizing our application

Introduction

In the previous blog post, we learned how to create a Next.js application using Nx and set up our development environment. Now, it's time to take our application to the next level by containerizing it with Docker. Containerization allows us to package our application along with its dependencies, ensuring consistent and portable deployments. In this follow-up blog post, we will explore the process of containerizing our Nx + Next.js application and deploying it using Docker.

Table of Contents

Prerequisites

Before proceeding, make sure you have the following prerequisites in place:

  1. Completed the previous post steps.

  2. Basic understanding of Docker and containerization concepts.

  3. Docker installed on your machine.

Step 1: Preparing the Next.js Application

Let's prepare our Next.js application for containerization.

Good news! There is not much to do here. The @nx/next plugin already takes care of most of the work for us. When we build our application using:

pnpm exec nx build my-app
Enter fullscreen mode Exit fullscreen mode

You will notice in the dist/apps/my-app directory that a package.json file was created. This file will represent a subset of the workspace package.json containing only and just only the packages needed by our app (and it's workspace dependencies).

dist/apps/my-app/package.json:

{
  "name": "my-app",
  "version": "0.0.1",
  "dependencies": {
    "next": "13.4.1",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "typescript": "5.1.5"
  },
  "scripts": {
    "start": "next start"
  }
}
Enter fullscreen mode Exit fullscreen mode

This generated package.json will help us to only install the minimum required dependencies needed by the application in our container.

If you are not using static dependency versions in your root package.json you may want to also generate a lock file to ensure dependency versions in production match with your development environment. To achieve this add the following option to your application build target:

{
    ...
    "build": {
      "executor": "@nx/next:build",
      "outputs": ["{options.outputPath}"],
      "defaultConfiguration": "production",
      "options": {
        "outputPath": "dist/apps/my-app"
      },
      "configurations": {
        "development": {
          "outputPath": "apps/my-app"
        },
        "production": {
+         "generateLockfile": true
        }
      },
      "dependsOn": ["build-custom-server"]
    },
    ...
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Adding a container target to the project

We want to run the container build process the same way as we lint, build and test our app: with a project target. To achieve this, we will make use the awesome @nx-tools/nx-container Nx plugin.

The @nx-tools/nx-container Nx plugin provides first class support for Container builds in your Nx workspace. It supports Docker, Podman and Kaniko engines. Leave a star in its repo and take a look there for advanced configuration.

Start by installing the plugin, run:

pnpm add -D @nx-tools/nx-container
Enter fullscreen mode Exit fullscreen mode

Tip: You can optionally follow the docs about using the @nx-tools/container-metadata package to enable automatic image tagging with OCI Image Format Specification labels.

Next, let's setup our project to be containerized:

pnpm exec nx g @nx-tools/nx-container:init my-app --template next --engine docker
Enter fullscreen mode Exit fullscreen mode

You will see a new container target added to the application's project.json. Let's configure the target as shown below:

apps/my-app/project.json:

{
  ...
  "targets": {
    ...
    "container": {
      "executor": "@nx-tools/nx-container:build",
      "dependsOn": ["build"],
      "defaultConfiguration": "local",
      "options": {
        "engine": "docker",
        "context": "dist/apps/my-app",
        "file": "apps/my-app/Dockerfile"
      },
      "configurations": {
        "local": {
          "tags": ["my-app:latest"],
          "push": false
        },
        "production": {
          "tags": ["my.image-registry.com/my-app:latest"],
          "push": true
        }
      }
    }
  },
  ...
}
Enter fullscreen mode Exit fullscreen mode

You can replace the production configuration with what better suits your needs. You are also free to add all the necessary configurations.

Understanding our container target config

We have configured the container target to make use of Docker as the container engine with some additional options:

  • context: we are telling docker to use our app's output directory as the context passed to the image build process. This way we don't waste memory passing the whole monorepo when we only need some specific files.

  • push: For the local configuration this option is turned off as we don't want to push the built image to the registry by default.

  • file: Here we specify where to find the Dockerfile used for the container image build, this path is relative to the workspace root.

Step 2: Creating a Dockerfile

A Dockerfile is a text file that contains instructions for building a Docker image. In this step, we will create a Dockerfile for our Next.js application. We'll define the base image, copy the application code, and specify the required dependencies.

# Install dependencies only when needed
FROM docker.io/node:lts-alpine as dependencies

RUN apk add --no-cache libc6-compat
WORKDIR /usr/src/app
COPY .npmrc package.json ./
RUN npm install --only=production

# Production image, copy all the files and run next
FROM docker.io/node:lts-alpine as runner
RUN apk add --no-cache dumb-init

ENV NODE_ENV production
ENV PORT 3000
ENV HOST 0.0.0.0
ENV NEXT_TELEMETRY_DISABLED 1

WORKDIR /usr/src/app

# Copy installed dependencies from dependencies stage
COPY --from=dependencies /usr/src/app/node_modules ./node_modules

# Copy built application files
COPY ./ ./

# Run the application under "node" user by default
RUN chown -R node:node .
USER node
EXPOSE 3000

# If you are using the custom server implementation:
CMD ["dumb-init", "node", "server/main.js"]

# If you are using the NextJS built-int server:
# CMD ["dumb-init", "npm", "start"]
Enter fullscreen mode Exit fullscreen mode

Important: If you are also using pnpm and enabled the generateLockfile option for build target, you may want to first install pnpm in the dependencies stage to make use of the generated pnpm-lock.yaml file.

You will also need to copy the pnpm-lock.yaml before running the installation command:

+ RUN npm install -g pnpm
- COPY .npmrc package.json ./
+ COPY .npmrc package.json pnpm-lock.yaml ./
- RUN npm install --only=production
+ RUN pnpm install --frozen-lockfile --prod

To improve the efficiency of our Docker builds and reduce the image size, we are leveraging the concept of multi-stage builds. Using a multi-stage build allow us to separate the dependencies installation environment from the runtime environment. This way, as an example, we can remove sensitive data like private registries authentication tokens from our app runtime container.

Step 3: Building the Docker Image

With the Dockerfile in place and our container target configured, we'll proceed to build the Docker image. We'll use the container target to execute the build process, which involves pulling the base image, installing dependencies, and creating the final image.

To build our image, run:

pnpm exec nx container my-app
Enter fullscreen mode Exit fullscreen mode

This will first build our application and it's dependencies prior to run the docker container build. To visualize this tasks dependencies you can run:

pnpm exec nx container my-app --graph
Enter fullscreen mode Exit fullscreen mode

You will find the following task dependency structure.

Task dependency graph of the container target for the my-app project

Step 4: Running the Docker Container

Once the Docker image is built, we'll run it as a container to verify that our application is working correctly within the containerized environment.

To start our container, run:

docker run -p 3000:3000 -t my-app:latest
Enter fullscreen mode Exit fullscreen mode

You will get and output like:

➜ docker run -p 3000:3000 -t my-app:latest
shared-util-nextjs-server
[ ready ] on http://0.0.0.0:3000
Enter fullscreen mode Exit fullscreen mode

You can now visit http://localhost:3000 to access your NextJS application.

nextjs application running inside a docker container

You can even send an HTTP request to your exposed API endpoints:

➜ curl http://localhost:3000/api/hello
Hello, from API!
Enter fullscreen mode Exit fullscreen mode

Great! 🎉

Conclusion

Containerization provides numerous benefits, including improved portability, scalability, and reproducibility of our applications. In this follow-up blog post, we've learned how to containerize our Nx + Next.js application using Docker. By leveraging Docker, we can simplify the deployment process and ensure consistent behavior across different environments.

Stay tuned for more exciting topics as we continue our journey with Nx, Next.js, and Docker!

You can find all related code in the following Github repo:

GitHub logo sebastiandg7 / nx-nextjs-docker

An Nx workspace containing a NextJS app ready to be deployed as a Docker container.

Nx + Next.js + Docker

This repository contains the code implementation of the steps described in the blog posts titled:

Overview

The blog post provides a detailed guide on setting up a Next.js application using Nx and Docker, following best practices and leveraging the capabilities of the Nx workspace.

The repository contains all the necessary code and configuration files to follow along with the steps outlined in the blog post.

Prerequisites

To successfully run the Next.js application and Dockerize it, ensure that you have the following dependencies installed on your system:

  • Docker (version 23)
  • Node.js (version 18)
  • pnpm (version 8)

You can alternatively use Volta to setup the right tooling for this project.

Getting Started

To get started, follow the steps below:

  1. Clone the…

Top comments (11)

Collapse
 
thebrogramm3r profile image
MichaelAngelo Rivera

you are AWESOME!!!!!!!!!!!!!!!!!!!!

Collapse
 
sebastiandg7 profile image
Sebastián Duque G

I'm glad it was helpful! 😀

Collapse
 
beldenschroeder profile image
Belden Schroeder

I got this to work by building my image on my Docker Desktop and deploying it to Docker Hub. Now, I want to do this using Skaffold. How would any of this code need to change for that?

Since Skaffold gives a random name for every container I build, would this not work, given that you have to specify the container name beforehand in the Nx project.json file in containers > configuration > production?

If so, how do you get around this?

Collapse
 
fkrautwald profile image
Frederik Krautwald

Try to build the Dockerfile using just docker and it fails.

Collapse
 
sebastiandg7 profile image
Sebastián Duque G

Hey. Can you share what command did you use?

Collapse
 
fkrautwald profile image
Frederik Krautwald

docker build

It works when using the nx-container command, but docker files should stand on their own.

Thread Thread
 
sebastiandg7 profile image
Sebastián Duque G • Edited

Aboslutely! The equivalent build command to the container target would be:

From the workspace root:
docker buildx build --file apps/my-app/Dockerfile --tag my-app:latest dist/apps/my-app

You first need to build the app as Nx does prior to building the docker image.

Collapse
 
naviat profile image
naviat

really good!!
I have a question: how to config in project.json when I have one app but want to build multi docker image from this app with different environment?

Collapse
 
sebastiandg7 profile image
Sebastián Duque G

You could add different configurations to the container target with different Dockerfiles, tags, etc.

Collapse
 
xwlee profile image
Lee Xiang Wei

Great article. Keep more coming.

Collapse
 
kaualandi profile image
kaualandi

I'm need help to configure Docker Dev Containers for programming with Nx Angular