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:
- Creating a monorepo workspace using modern tooling.
- Adding a NextJS application with a custom server implementation to the workspace.
- 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
- Step 1: Set up your environment
- Step 2: Create your Nx workspace
- Step 3: Generating our Next.js application
- Step 4: Adding some workspace libraries
- Step 5: Fixing our
build-custom-server
target - Conclusion
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
Once Volta is installed, enable Volta's PNPM feature:
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:
volta install node@18
node -v
Now, install pnpm:
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:
pnpm dlx create-nx-workspace@16.4.0 myorg --pm pnpm --nxCloud --preset empty
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
Make sure your package.json file reflects the pinned versions:
{
...
"volta": {
"node": "18.16.1",
"pnpm": "8.6.5"
}
}
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
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
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);
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.
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
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
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>
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
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();
// ....
At this point, if you run the Nx graph:
pnpm exec nx graph
your workspace should look something similar to this:
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
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"]
}
}
},
...
}
}
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'.
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"]
}
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"]
},
...
},
}
To locally serve your application, run:
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:
You can find all related code in the following Github repo:
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:
- Nx + NextJS + Docker - The Nx way: Creating the NextJS application.
- Nx + NextJS + Docker - The Nx way: Containerizing our application
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:
-
Clone the…
Top comments (3)
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 😅
Getting the following error when trying to pin pnpm
Error: Only node and yarn can be pinned in a project
Use
npm install
oryarn add
to select a version of pnpm for this project.Awesome, this helped me a lot today! Thanks!