Starting with Next.js and Kubernetes

Next.js is a framework for building high performance web applications. This article looks at how a Next.js application can be deployed to a Kubernetes cluster. I do not intend to explain how to develop a Next.js application but I do start from the beginning.

Martin Hodges
8 min readMay 12, 2024
Next.js structure

You can find the code for this article here.

Next.js

If you have ever created a web application, you will know that there is a lot to think about, including user interactions, fetching data, interfacing to APIs, authentication, authorisation and much more.

Frameworks like Next.js simplify the development by providing a lot of the code required to manage these common tasks.

Advantages

As well as providing the standard set of utilities, the Next.js framework provides a number of other advantages when creating a web application:

  • Server components
  • Client components
  • Site caching
  • Data caching
  • Data orchestration

Next.js can produce static content that can be distributed across a Content Distribution Network (CDN) but its real power is in the split between the server-side and the client-side components. If you are not already aware, these components are React components.

By providing server-based solution, Next.js can render components on the server and then send the results to the client. This can improve performance, reduce bandwidth requirements and enhance security.

These server-side components can securely access the backend APIs and insert the content directly into the components, which are then served to the client (called hydration). The server-side component can also be hydrated with data from multiple APIs, making the response very efficient (called orchestration).

Server-side rendering also allows bots to crawl and index your content, increasing your SEO scores.

Disadvantages

There are a few disadvantages to frameworks like Next.

By rendering on the server, you will incur additional server costs as you will need to run a Node.js server to do the rendering, rather than relying on the user’s own computer.

When you are building an application with highly volatile data, caching may need to be disabled to avoid stale data, removing the benefits of the cache.

Session management including user authentication and identity is still required and must be performed and held by the client.

Finally, components that are highly interactive require to be rendered on the client side, bypassing the caching and orchestration provided by server-side rendering.

Creating a Next.js application

Before you can create a Next.js application, you need version 18+ of Node.js.

Installing Node.js

On a Mac, this can be achieved with the following (assuming you have homebrew installed):

brew install node

You can check your version with:

node -v

Creating your first Next.js application

Once node.js is installed, you can use the npx utility to create your first Next.js application:

npx create-next-app@latest my-nextjs-app --use-npm

This starts a simple wizard. You will be asked a set of questions to set up your application (you can use the defaults as shown below, or select your own options):

  • Use Typescript? Yes
  • Use ESLint? Yes
  • Use Tailwind CSS? Yes
  • Use src directory? No
  • Use App router? Yes
  • Customise import aliases? No

The required packages will then be installed and a template folder structure created:

app - your application goes here
node_modules - installed npm packages
public - static files that will be accessible to everyone

On top of this there is a set of configuration files. The README.md file tells you about the commands you have available to you.

Running your application

There are three commands involved with running your application:

  • npm run dev … runs the application in development mode with hot-swap for any code changes you make
  • npm run build … builds a production version of your application
  • npm start … runs the production build that you created with the previous command

You can try it now without making any changes to the code that was generated.

cd my-nextjs-app
npm run dev

You will find the result on http://localhost:3000. You should be presented with a nice looking Next.js introduction page.

From here you can now start creating your own application. You can use Tailwind CSS modules automatically along with Next.js page routing and more.

Any changes you make should be hot-loaded into the browser. If you find this does not work, try stopping the development server, deleting your .next folder and running the development server again.

Deploying a Next.js application

Whilst Next.js allows fully static sites to be created, I am going to assume we are deploying a dynamic, interactive application that includes a server.

As we saw earlier, the server is a Node.js server and this can be deployed as part of a Docker image. This works well with Kubernetes.

There are three steps to deploying Next.js to a Kubernetes cluster:

  1. Build the application
  2. Create a Docker image
  3. Create a Kubernetes deployment
  4. Deploy your application

1. Build the application

Perhaps the easiest step! We have already seen the command for this. From within your project file:

npm run build

You can test this with:

npm start

Now check the results in your browser as before.

If you see the Step 1 complete!

2. Create a Docker image

Like any Dockerised application, Next.js requires a Dockerfile to tell Docker how to build your image. Create the following file:

Dockerfile

FROM node:18-alpine AS runtime
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY .next ./.next
COPY public ./public
EXPOSE 3000
USER node
CMD ["npm","start"]

A few things to note:

  1. node:18-alpine is the official base image for Node.js version 18
  2. We copy over the dependencies and then only load the ones required for production
  3. We copy over our built files from .next and our public files from public
  4. Production still uses port 3000
  5. We change from the root user to the node user run the image

Ok, so now we are ready to build our image:

docker build -t my-nextjs-app-img .

You can now run your image with:

docker run -p 3000:3000 my-nextjs-app-img

You should, again, be able to access your application on http:localhost:3000.

Combining build and image creation

If you were observant, you will have noticed we referred to our base image in the Docker file As runtime. This is because we can add a build stage before this to build the application before creating the final image.

To do this, add this to the start of the Dockerfile:

FROM node:18-alpine AS build

WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

Now change these two COPY commands in the original part, to reference the image created by the build stage:

COPY --from=build /app/.next ./.next
COPY --from=build /app/public ./public

Now there is no need to do the build before creating the Docker image. Creating the Docker image will carry out the build and then create the image in the one command.

Normally, at this stage, you would push your image to an external Docker image repository, such as Docker Hub. First tag your image so that you can push it to your repository, then push it. The example here is pushing the image to my own Docker Hub repository so change my name to your own Docker Hub username!

docker tag my-nextjs-app-img martinhodges/my-nextjs-app-img
docker push martinhodges/my-nextjs-app-img

You can, of course, add a version tag as well.

3. Create a Kubernetes deployment

Once you have your Docker image, you can deploy it into your Kubernetes cluster like any other Docker image.

First, create a deployment manifest file (remember to change my name for yours — unless you want to use my image!).

k8s/deployment.yml

apiVersion: apps/v1
kind: Deployment
metadata:
name: my-nextjs-app
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: my-nextjs-app
template:
metadata:
labels:
app: my-nextjs-app
spec:
containers:
- name: my-nextjs-app
image: martinhodges/my-nextjs-app-img:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 3000

Note that I am using the default namespace.

Your application will not be accessible outside of your cluster and so you will need a Kubernetes Service. The way you deploy this Service will depend on your Kubernetes cluster architecture.

As I am using my Kind cluster, which already has port 30000 exposed, I will use a NodePort Service to expose my application’s container port 3000 on port 30000 of the node.

Create the following file

k8s/service.yml

apiVersion: v1
kind: Service
metadata:
name: my-nextjs-app
namespace: default
spec:
selector:
app: my-nextjs-app
type: NodePort
ports:
- port: 3000
targetPort: 3000
nodePort: 30000

Now deploy it with:

kubectl apply -f k8s/service.yml

4. Deploying your application

I am assuming you have a Kubernetes cluster to hand. If not, you can create a local one based on Kind as described in my previous article.

You can now deploy your application with:

kubectl apply -f k8s/deployment.yml

You can check on progress with:

kubectl get pods

Hopefully it fetches the image and starts up!

You should now be able to see your application in your browser at http:localhost:30000 (note the extra 0!).

Deploying to Kind

If, like me, you have a Kind cluster deployed locally, you can avoid the upload (and subsequent download) of your docker file and load it into your cluster directly with:

kind load docker-image martinhodges/my-nextjs-app-img

You can do this with each change you make to your image in order to test it.

A note on logging!

When writing your web application, you will probably use the console.log() feature to write out your logs.

You can access these logs under Kubernetes in the same way as any other pod:

kubectl logs <pod name> -n <namespace> -f

Of course, the namespace defaults to default and can be omitted if this is where your pod resides. The -f means follow and will give you a live update to your logs until you press control-C.

Now, you may be wondering where your logs have gone and you may be thinking you have done something wrong as your logs do not appear.

This is due to the caching of the Next.js framework. If it can server previous content, it will. This will bypass your code and your logging line. This is why you may not see the logs you expect!

To remove the caching, you can add this to the top of your page or route:

export const dynamic = "force-dynamic";

Of course, this switches off caching so you may want to only do this whilst you are debugging something.

Summary

In this article we have taken a quick look at the Next.js web application framework. We then created a sample application and created a Docker image from it. This then allowed us to deploy it to Kubernetes using deployment and service manifest files. Finally we looked at deploying it to a local Kind Kubernetes cluster.

This can form the basis of developing sophisticated and highly performant web applications. There are a lot of articles and videos that can help with how to build applications under Next.js, such as this video series.

I encourage you to try out Next.js and have some fun creating your ‘next’ web application.

I hope you enjoyed this article and that you have extended your skills by learning something new, even if is is only one small thing.

If you found this article of interest, please give me a clap as that helps me identify what people find useful and what future articles I should write. If you have any suggestions, please add them as notes or responses.

--

--