DEV Community

Cover image for Nx + NextJS + Docker: Creating the NextJS application
Sebastián Duque G
Sebastián Duque G

Posted on

Nx + NextJS + Docker: Creating the NextJS application

Introduction

Welcome to this blog post, where we will guide you through the process of creating a NextJS application using Nx and Docker. In this tutorial, we will cover the following steps:

  1. Creating a monorepo workspace using modern tooling.
  2. Adding a NextJS application with a custom server implementation to the workspace.
  3. Building and shipping the app in an optimized Docker container with minimal dependencies.

⚠️ Warning: If you prefer not to use a custom server implementation for your NextJS project, you can skip the steps related to the server. We will provide alternate configurations for the built-in NextJS server.

Table of Contents

Prerequisites

To complete this tutorial, we will be using the following tools:

  • JS Tool Manager: Volta v1.1.1
  • JS Runtime: Node v18
  • Package manager: pnpm v8
  • Monorepo build system: Nx v16.4.0
  • App framework: NextJS v13
  • Container engine: Docker v23.0.4 | Buildx v0.10.4

You will need to have Docker installed. We will guide you installing the remaining tools.

Let's get started!

Step 1: Set up your environment

First, let's install Volta by following the instructions here:



curl https://get.volta.sh | bash


Enter fullscreen mode Exit fullscreen mode

Once Volta is installed, enable Volta's PNPM feature:



echo "export VOLTA_FEATURE_PNPM=1 >> ~/.zshrc"


Enter fullscreen mode Exit fullscreen mode

Warning: If you're not using zsh as your shell, update the respective shell configuration file for your system.

Next, let's install Node v18:



volta install node@18
node -v


Enter fullscreen mode Exit fullscreen mode

Now, install pnpm:



volta install pnpm@8
pnpm -v


Enter fullscreen mode Exit fullscreen mode

Step 2: Create your Nx workspace

With our tooling set up, it's time to create our Nx workspace. Run the following command:



pnpm dlx create-nx-workspace@16.4.0 myorg --pm pnpm --nxCloud --preset empty


Enter fullscreen mode Exit fullscreen mode

You can opt out of using Nx Cloud by passing --nxCloud false, but we recommend giving it a shot!

Navigate into the workspace directory and pin the Node and pnpm versions we're using for this project:



cd myorg
volta pin node@18
volta pin pnpm@8


Enter fullscreen mode Exit fullscreen mode

Make sure your package.json file reflects the pinned versions:



{
  ...
  "volta": {
    "node": "18.16.1",
    "pnpm": "8.6.5"
  }
}


Enter fullscreen mode Exit fullscreen mode

Install the required Nx plugins

We'll be using Nx in an Integrated Monorepo setup, which relies on Nx plugins to handle most of the heavy lifting. Let's install the NextJs and ESBuild plugins:



pnpm add -D @nx/next@16.4.0 @nx/esbuild@16.4.0


Enter fullscreen mode Exit fullscreen mode

Step 3: Generating our Next.js application

Now that we have the @nx/next plugin installed we can run generators to scaffold NextJS code in our workspace.

In your terminal, run the following command to generate the NextJS application:



pnpm exec nx g @nx/next:app my-app --customServer -s css --appDir


Enter fullscreen mode Exit fullscreen mode

Replace my-app with the desired name for your application.

This command will generate a new directory named my-app inside the apps folder of your Nx workspace, containing the initial NextJS application structure.

Tip: to know more about the included generators, run pnpm nx list @nx/next in your terminal.

You will notice in the apps/my-app/next.config.js the usage of the withNx next plugin from @nx/next.



//@ts-check

// eslint-disable-next-line @typescript-eslint/no-var-requires
const { composePlugins, withNx } = require('@nx/next');

/**
 * @type {import('@nx/next/plugins/with-nx').WithNxOptions}
 **/
const nextConfig = {
  nx: {
    // Set this to true if you would like to use SVGR
    // See: https://github.com/gregberge/svgr
    svgr: false,
  },
};

const plugins = [
  // Add more Next.js plugins to this list if needed.
  withNx,
];

module.exports = composePlugins(...plugins)(nextConfig);


Enter fullscreen mode Exit fullscreen mode

This plugin, among other things, is in charge of automatically passing the workspace libs your app is using to the NextJS transpilePackages config so you don't have to manually do it. Read more about it here.

Note: As of the moment of writing this post, @nx/next v16.4.0 generates a custom server build target that fails if you try to serve your application:



➜ pnpm exec nx serve my-app

> nx run my-app:serve:development

/.../nx-nextjs-docker/node_modules/.pnpm/@nx+js@16.4.0_nx@16.4.0_typescript@5.1.5/node_modules/@nx/js/src/executors/node/node.impl.js:235
                throw new Error(`Could not find ${fileToRun}. Make sure your build succeeded.`);
                      ^
Error: Could not find /.../nx-nextjs-docker/dist/apps/my-app/main.js. Make sure your build succeeded.


Enter fullscreen mode Exit fullscreen mode

we will fix this later 🙂.

In the meanwhile you can build and run your NextJS app this way:



pnpm exec nx build my-app
cd ./dist/apps/my-app
# If using a custom server
node server/main.js
# If not using a custom server
npm start


Enter fullscreen mode Exit fullscreen mode

Step 4: Adding some workspace libraries

Let's create a feature library for our application



pnpm exec nx g @nx/next:lib feature-main --directory my-app --buildable


Enter fullscreen mode Exit fullscreen mode

Next, let's use this library in our root page:

apps/my-app/app/page.tsx:



+import { MyAppFeatureMain } from '@myorg/my-app/feature-main';
import styles from './page.module.css';

export default async function Index() {
  /*
   * Replace the elements below with your own.
   *
   * Note: The corresponding styles are in the ./index.css file.
   */
  return (
    <div className={styles.page}>
      <div className="wrapper">
        <div className="container">
+         <MyAppFeatureMain />
          <div id="welcome">
            <h1>
              <span> Hello there, </span>
              Welcome my-app 👋
            </h1>
          </div>


Enter fullscreen mode Exit fullscreen mode

Now, let's add a utility library with an example function that will be used by our custom server implementation:



pnpm exec nx g @nx/js:lib util-nextjs-server --directory shared


Enter fullscreen mode Exit fullscreen mode

Next, let's use this utility library in the server/main.ts file:

apps/my-app/server/main.ts:



import { createServer } from 'http';
import { parse } from 'url';
import * as path from 'path';
import next from 'next';
+import { sharedUtilNextjsServer } from '@myorg/shared/util-nextjs-server';

// .... 

async function main() {
+ console.log(sharedUtilNextjsServer());

  const nextApp = next({ dev, dir });
  const handle = nextApp.getRequestHandler();
  // ....


Enter fullscreen mode Exit fullscreen mode

At this point, if you run the Nx graph:



pnpm exec nx graph


Enter fullscreen mode Exit fullscreen mode

your workspace should look something similar to this:

Nx dependency graph displaying the workspace projects and how they depend on each other

Great! 🎉

Step 5: Fixing our build-custom-server target

Now it's time to fix all the build errors we are having with our custom server build. If you are not using a custom server, everything should be working at this point.

To fix our build-custom-server target we will configure this target using the @nx/esbuild nx plugin, similar to what @nx/node generated apps do for their build target. Read more about it here.

In the previous steps, we installed the @nx/esbuild plugin in our workspace. Let's now call its init generator to install all the required dependencies this plugin needs:



pnpm exec nx g @nx/esbuild:init


Enter fullscreen mode Exit fullscreen mode

Next, let's update our NextJS app's project.json and configure the build-custom-server target as shown below:

apps/my-app/project.json:



{
  ...
  "targets": {
    ...
    "build-custom-server": {
      "executor": "@nx/esbuild:esbuild",
      "defaultConfiguration": "production",
      "options": {
        "outputPath": "dist/apps/my-app",
        "main": "apps/my-app/server/main.ts",
        "tsConfig": "apps/my-app/tsconfig.server.json",
        "clean": false,
        "assets": [],
        "platform": "node",
        "outputFileName": "server/main.js",
        "format": ["cjs"],
        "bundle": true,
        "thirdParty": false,
        "esbuildOptions": {
          "sourcemap": true,
          "outExtension": {
            ".js": ".js"
          }
        }
     },
     "configurations": {
        "development": {
          "sourcemap": true
        },
        "production": {
          "sourcemap": false,
          "assets": [".npmrc"]
        }
     }
    },
    ...
  }
}


Enter fullscreen mode Exit fullscreen mode

This new build configuration for our custom server will bundle all the imported libraries into a single file, which is perfect for deploying our application without needing the rest of our monorepo dependencies (and results in smaller containers 👀).

If you encounter the following error while building the my-app project:



error TS5069: Option 'tsBuildInfoFile' cannot be specified without specifying option 'incremental' or option 'composite'.


Enter fullscreen mode Exit fullscreen mode

remove the "tsBuildInfoFile" line from the tsconfig.server.json file:

apps/my-app/tsconfig.server.json:



{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "module": "commonjs",
    "noEmit": false,
    "incremental": true,
-   "tsBuildInfoFile": "../../tmp/buildcache/apps/my-app/server",
    "types": ["node"]
  },
  "include": ["server/**/*.ts"]
}


Enter fullscreen mode Exit fullscreen mode

Now that our build-custom-server target is working, let's tell Nx to first build the project dependencies before building the custom server. Add the following to your nx.json:



{
  "targetDefaults": {
    ...
    "build-custom-server": {
      "dependsOn": ["^build"]
    },
    ...
  },
}


Enter fullscreen mode Exit fullscreen mode

To locally serve your application, run:



pnpm exec nx serve my-app


Enter fullscreen mode Exit fullscreen mode

Conclusion

Your NextJS app is now ready to work with a custom server and use workspace libraries in the server.ts or in any other place of your application 🥳

In the next post of this series, we will address building our application for production and packing it into a container in the most efficient way possible. You can find it here:

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 (3)

Collapse
 
fyodorio profile image
Fyodor

It’s Next.js I believe, not NextJS. Sorry, I have the painful history of multiple migrations from Angular.js to Angular, I feel in my bones how important letters can be 😅

Image description

Collapse
 
mrsrv7 profile image
MrSrv7

Getting the following error when trying to pin pnpm

Error: Only node and yarn can be pinned in a project

Use npm install or yarn add to select a version of pnpm for this project.

Collapse
 
ejeker profile image
Eric Jeker

Awesome, this helped me a lot today! Thanks!