Deploying Next.js applications using the Nginx server keeping the Next Server intact(2/2).

Chenna Sreenu
7 min readAug 27, 2023

--

Part (1/2) explains the development setup using docker-compose.yml file for testing and debugging your application locally.

Part (2/2) explains the production setup using deployment.yml file for kubernettes clustor to test and debug your application in production.

We have already discussed the development setup for the NextJs applications using the Nginx Server keeping the Next Server intact using the docker-compose.yml file with the help of shared volumes to maintain the two-way binding between the Nginx Server file system and the Next Server File System.

In this article, we will discuss the same scenario in a Kubernetes World, Here in this world, volumes behave differently unlike docker volumes.

My use case contains two running containers(thus two servers, Next & Nginx) in a single Pod. Of course, you can replicate the same using different setups say, one Pod running the Next Server and another Pod running the Nginx Server in a single worker node. Or maybe two pods running in different worker nodes in the same main cluster. The concept is the same.

Q) Why docker volumes are different from Kubernetes volumes while establishing this Two-Way binding between both servers’ file systems?

Docker Volumes:

We all know the named volumes are for persisting the data when containers get deleted and restarted in the Docker world.

Case 1) Volume is not born yet, you are running the containers for the very first time after building the image.

  • At t=0, volume gets created and consumes (or eats in simple terms) the targetted folder. Assume the targeted folder contains a,b, and c when the image is built. When you start the container, the volume will also have a,b, and c. (You can assume that the targetted folder vomits its data into the volume though it is empty & wipes off the volume’s persisted data, it will make sense in Kubernetes world).
  • At t>0, while the container is running and altering the targetted folder(say it added p,q, and r), the volume keeps on consuming whatever the targetted folder is having. So volume now has a,b,c,p,q, and r.

You stopped the container and deleted the container.

Case 2) Volume is still alive(persisted with a,b,c,p,q,r), and you rebuild the image by adding extra things(x,y, and z) in the targetted folder either manually or with Docker RUN command(thus containing a,b,c,x,y and z). You start the container.

  • At t=0, the persisted volume with a,b,c,p,q,r will wipe off the targetted folder(containing a,b,c,x,y, and z) and vomits a,b,c,p,q,r into the targetted folder. So the targetted folder in the end contains a,b,c,p,q, and r. We lost x,y, and z.
  • At t>0, while the container is running and altering the targetted folder volume keeps on consuming as usual.

Kubernetes Volumes:

Docker World is fine for testing and debugging locally. The problem comes in the Kubernetes world. There is NO such Case 1 in the Kubernetes world. Kubernetes CAN’T recognize that you are deploying the Pod for the very first time.

In fact, this is how Kubernetes should work for security reasons, Case 1 should not execute. Because a cluster can have many Pods. Let's say there is a named volume(V1) with 10,000 user profiles that is binded to /users folder of a completely different Pod. And if you are deploying your new Pod and mistakenly targetted that Volume(V1), as I said(Case 1, t=0) our folder will vomit into it and wipe off all 10,000 user profiles.

Q) So, what is wrong if only Case 2 executes without Case 1?

As I said

  • At t=0, the named volume “nextjs-shared-volume” which is empty initially (because Case 1 did not execute) will (i)wipe off the targetted folder /app (containing build, node_modules, public, next.config.js, etc.) and (ii)vomit its content(which is empty initially) into the targetted folder. So the targetted folder /app ends up having nothing in either the Nginx Server File System or the Next Server File System.

Because /app has nothing Next Server will not even start(npm start with empty /app folder), but the Nginx Server will start though /app has nothing in it(Nginx Server doesn't rely on contents of /app for starting the server).

apiVersion: apps/v1
kind: Deployment
metadata: #<name of the deployment>
name: nextjs-app-deployment
spec: # this spec corresponds to cofig of deployment object
replicas: 1 # no.of pod instances for load balacing purpose
selector:
# this is for targetting Pods with the specified labels(defined below in template of the pod)
# so that this deployment(when pushed) can only alter/update those pods with mentioned key and value in K8.
# In other words, to target already existing older deployment pods inorder to replace them with latest rollouts
matchLabels:
app: nextjs-app
template: # here we define configurations of single pod
metadata:
labels: # here we define labels of single pod to get targetted by multiple object resources like Deployments, Services, etc
app: nextjs-app
spec:
containers:
- name: medium-nextjs-app
# image: #<Image name from any regsitry like DockerHub>:<tag of the image>
image: csrinu236/medium-nextjs-app:latest
imagePullPolicy: Always
resources:
limits:
cpu: 300m
memory: 384Mi
requests:
cpu: 200m
memory: 256Mi
command: ['npm', 'run', 'start']
volumeMounts:
- name: nextjs-shared-volume
mountPath: /app
- name: medium-nginx-app
image: csrinu236/medium-nginx-app:latest
imagePullPolicy: Always
env:
- name: NEXTJS_CONTAINER_IP
value: localhost
resources:
limits:
cpu: 300m
memory: 384Mi
requests:
cpu: 200m
memory: 256Mi
command: ['sh', '-c', '/etc/nginx/convert-nginx.sh']
volumeMounts:
- name: nextjs-shared-volume
mountPath: /app
volumes:
- name: nextjs-shared-volume
emptyDir: {}

Note this sequence: (i) wiping off happens first and then (ii) command [‘npm’, ‘run’, ‘start’] executes.

Next Server is not starting because /app has nothing & npm start fails
The Next Server is starting because it doesn’t require anything from /app to run.

Q) What can be done then?

If we somehow pre-populate the Volume with /app folders and files so that, initially it will have the /app folder. And even though initially the Volume wipes off the /app of Next Server, later the same Volume vomits the same prepopulated /app into the /app of Next Server.

Q) How can we pre-populate the Volume?

Using init containers. Init containers run before the actual containers and their file system dies after completing their task.

spec:
initContainers:
- name: init-container-nextjs
image: csrinu236/medium-nextjs-app:latest
command: ['sh', '-c', 'cp -r /app/* /temp']
volumeMounts:
- name: nextjs-shared-volume
mountPath: /temp

Here, we are copying all the nested folders and files of /app into /temp and the nextjs-shared-volume is binded to the /temp folder. So the volume is prepopulated with /app and persists for the actual containers.

According to the sequence as noted above, (i) Volume will wipe off /temp first, and then (ii) the command executes which copies /app/* into /temp which is bound to the Volume. So the Volume is prepopulated with /app and persists for the actual containers.

apiVersion: apps/v1
kind: Deployment
metadata: #<name of the deployment>
#kubectl create deployment <name of deployment>
name: nextjs-app-deployment
spec: # this spec corresponds to cofig of deployment object
replicas: 1 # no.of pod instances for load balacing purpose
selector:
matchLabels:
app: nextjs-app
template: # here we define configurations of single pod
metadata:
labels: # here we define labels of single pod to get targetted by multiple object resources like Deployments, Services, etc
app: nextjs-app
spec:
initContainers:
- name: init-container-nextjs
image: csrinu236/medium-nextjs-app:latest
command: ['sh', '-c', 'cp -r /app/* /temp']
volumeMounts:
- name: nextjs-shared-volume
mountPath: /temp

containers:
- name: medium-nextjs-app
image: csrinu236/medium-nextjs-app:latest
imagePullPolicy: Always
resources:
limits:
cpu: 300m
memory: 384Mi
requests:
cpu: 200m
memory: 256Mi
command: ['npm', 'run', 'start']
volumeMounts:
- name: nextjs-shared-volume
mountPath: /app
readOnly: true
- name: medium-nginx-app
image: csrinu236/medium-nginx-app:latest
imagePullPolicy: Always
env:
- name: NEXTJS_CONTAINER_IP
value: localhost
resources:
limits:
cpu: 300m
memory: 384Mi
requests:
cpu: 200m
memory: 256Mi
command: ['sh', '-c', '/etc/nginx/convert-nginx.sh']
volumeMounts:
- name: nextjs-shared-volume
mountPath: /app
volumes:
- name: nextjs-shared-volume
emptyDir: {}
/app of the Next Server File System
/app of the Nginx Server File System.

Refer to the k8-conf branch of this repo for checking the output in your local Minikube cluster. Check deploymeny.yml and service.yml files.

kubectl apply -f=deployment.yml

kubectl apply -f=service.yml

Done, two way binding between Next Server File System and Nginx Server File System is established in Production as well.

--

--

Chenna Sreenu

Hyderabad, India | Jio Platforms Limited | IIT Kharagapur