Nx + NextJS + Docker: Creating the NextJS application
Tags: nextjs, nx, docker, tutorial
Published: 2023-06-28 04:58:28

## 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](#prerequisites)
- [Step 1: Set up your environment](#step-1-set-up-your-environment)
- [Step 2: Create your Nx workspace](#step-2-create-your-nx-workspace)
  - [Install the required Nx plugins](#install-the-required-nx-plugins)
- [Step 3: Generating our Next.js application](#step-3-generating-our-nextjs-application)
- [Step 4: Adding some workspace libraries](#step-4-adding-some-workspace-libraries)
- [Step 5: Fixing our `build-custom-server` target](#step-5-fixing-our-build-custom-server-target)
- [Conclusion](#conclusion)

## Prerequisites

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

- JS Tool Manager: [Volta](https://volta.sh/) v1.1.1
- JS Runtime: [Node](https://nodejs.org/en) v18
- Package manager: [pnpm](https://pnpm.io/) v8
- Monorepo build system: [Nx](https://nx.dev) v16.4.0
- App framework: [NextJS](https://nextjs.org/) v13
- Container engine: [Docker](https://www.docker.com/) v23.0.4 | [Buildx](https://github.com/docker/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](https://docs.volta.sh/guide/getting-started):

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

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

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

> **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:

```bash
volta install node@18
node -v
```

Now, install pnpm:

```bash
volta install pnpm@8
pnpm -v
```

## Step 2: Create your Nx workspace

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

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

> You can opt out of using [Nx Cloud](https://nx.dev/nx-cloud/intro/what-is-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:

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

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

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

### Install the required Nx plugins

We'll be using Nx in an [Integrated Monorepo](https://nx.dev/tutorials/integrated-repo-tutorial) setup, which relies on Nx plugins to handle most of the heavy lifting. Let's install the NextJs and ESBuild plugins:

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

## 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:

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

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`.

```js
//@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);
```

This plugin, among other things, is in charge of automatically passing the workspace libs your app is using to the NextJS [transpilePackages](https://nextjs.org/docs/app/api-reference/next-config-js/transpilePackages) config so you don't have to manually do it. Read more about it [here](https://nx.dev/packages/next/documents/next-config-setup).

**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:

```bash
➜ 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.
```

we will fix this later 🙂.

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

```bash
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
```

## Step 4: Adding some workspace libraries

Let's create a feature library for our application

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

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

`apps/my-app/app/page.tsx`:
```diff
+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>
```

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

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

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

`apps/my-app/server/main.ts`:
```diff
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();
  // ....
```

At this point, if you run the Nx graph:

```bash
pnpm exec nx graph
```

your workspace should look something similar to this:

![Nx dependency graph displaying the workspace projects and how they depend on each other](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3k6fxp4739hblpevxp50.png)

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](https://nx.dev/packages/node).

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:

```bash
pnpm exec nx g @nx/esbuild:init
```

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`:
```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"]
        }
     }
    },
    ...
  }
}
```

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:

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

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

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

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`:

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

To locally serve your application, run:

```bash
pnpm exec nx serve my-app
```

## 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:

{% embed https://dev.to/sebastiandg7/nx-nextjs-docker-the-nx-way-containerizing-our-application-1mi7 %}

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

{% github https://github.com/sebastiandg7/nx-nextjs-docker %}