Search code examples
node.jstypescriptkubernetesnext.jswebpack

NextJS symlinks not updating and crashing the application in Kubernetes deployment


Intro

I have Kubernetes deployment of a NextJS app, I want to run the app in development mode so that updating a file causes the application to hot-reload immediately, in particular I want to update just some JSON files in the "config" folder. I created a docker container that copies the necessary files except the "config files", the config files instead are attached to the container using Kubernetes ConfigMaps, mounted as volumes.

I'm running on minikube on Windows WSL2.

Problem

When I first run the container it works fine, but if I navigate to some pages or I change values in the configmaps, I get this error (or an error with a different file like PlanModel.config.json):

> @ dev-base /usr/src/app
> next dev

▲ Next.js 14.2.3

- Local: http://localhost:3000

✓ Starting...
Attention: Next.js now collects completely anonymous telemetry regarding usage.
This information is used to shape Next.js' roadmap and prioritize features.
You can learn more, including how to opt-out
if you'd not like to participate in this anonymous program, by visiting the following URL:https://nextjs.org/telemetry

✓ Ready in 3s
○ Compiling /middleware ...
✓ Compiled /middleware in 2.3s (216 modules) ○ Compiling / ...
Browserslist: caniuse-lite is outdated. Please run:
npx update-browserslist-db@latest
Why you should do it regularly: https://github.com/browserslist/update-db#readme  
 ✓ Compiled / in 18.6s (4401 modules)
cookie is: undefined
GET / 200 in 19523ms
✓ Compiled in 1339ms (1847 modules)
cookie is: undefined
GET / 200 in 68ms
cookie is: undefined
GET / 200 in 46ms
cookie is: undefined
GET / 200 in 37ms
⨯ ./config/ai/..2024_09_30_15_42_23.830652425/actions.config.json
Module build failed: Error: ENOENT: no such file or directory, open '/usr/src/app/config/ai/..2024_09_30_15_42_23.830652425/actions.config.json'

Import trace for requested module:
./config/ai/..2024_09_30_15_42_23.830652425/actions.config.json
./lib/chat/actions.config.tsx
./lib/chat/actions.tsx
./app/(chat)/page.tsx

Sometimes it happened that instead of getting this error immediately the app continued working but without hot reloading.

In both cases, I opened a shell in the container and I manually verified that the files are there and are correct.

Possible issue

I noticed that Kubernetes created symlinks for files in configmaps. In particular, the symlinks go like this:

actions.config.json -> ..data/actions.config.json -> ..2024_09_30_15_42_23.830652425/actions.config.json

I found out that NextJS has problems with tracking symlinks in development mode, because of webpack (https://github.com/vercel/next.js/issues/53175, https://github.com/webpack/watchpack/pull/232). I don't know how to solve this issue. I think I could either user a different Kubernetes approach that doesn't create symlinks (but I still need that I can update easily the config files) or I fix this NextJS problem.

More context

I want to build a platform that allows user to customize a NextJS application by changing some "config files", that are actually JSON files. These config files are imported (statically) by the NextJS application and determine the colors, the styles and the behavior of the application. So my users can access an editor that shows:

  • The final (production) version of their customized application: this will be deployed to the world
  • The preview (development) version of their customized application: this will be seen by the user as a preview of the actual application and it refreshes automatically while the user is editing the application

Instead of creating a new slightly different NextJS application from the one I already created, and in order to allow live changes, I simply decided to run it in development mode.

In any case, both the "final version" and the "preview version" must be run at a scale and I decided to use kubernetes. I created an API that dynamically generates kubernetes manifests for both versions.

What I'm having problem with is the preview version, I have this shortened example file:

apiVersion: v1
kind: Namespace
metadata:
  labels:
    app_id: myId
    app_subdomain: testapp
  name: myId
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-environment
  namespace: myId
data:
  # App configuration
  APP_ID: myId

  # Postgres database, MongoDB database, etc env variables
---
apiVersion: v1
kind: ConfigMap
metadata:
  labels:
    app_id: myId
    app_subdomain: testapp
  name: app-config
  namespace: myId
data:
  theme.config.json: |
    CONTENT
---
apiVersion: v1
kind: ConfigMap
metadata:
  labels:
    app_id: myId
    app_subdomain: testapp
  name: app-config-ai
  namespace: myId
data:
  actions.config.json: |
    CONTENT
---
apiVersion: v1
kind: ConfigMap
metadata:
  labels:
    app_id: myId
    app_subdomain: testapp
  name: app-config-components
  namespace: myId
data:
  button-scroll-to-bottom.config.json: |
    CONTENT
  chat-history.config.json: |
    CONTENT
  chat-message-actions.config.json: |
    CONTENT
---
apiVersion: v1
kind: ConfigMap
metadata:
  labels:
    app_id: myId
    app_subdomain: testapp
  name: app-config-components-ui
  namespace: myId
data:
  sheet.config.json: |
    CONTENT
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app_id: myId
    app_subdomain: testapp
  name: app
  namespace: myId
spec:
  replicas: 1
  selector:
    matchLabels:
      app_id: myId
  template:
    metadata:
      labels:
        app_id: myId
        app_subdomain: testapp
    spec:
      containers:
        - name: nginx
          image: my-nginx:latest
          imagePullPolicy: Always
          ports:
            - containerPort: 80
        - name: app
          image: my-app-configurable:latest
          imagePullPolicy: Always
          envFrom:
            - configMapRef:
                name: app-environment
          ports:
            - containerPort: 3000
          volumeMounts: # /usr/src/app/ is the root directory of the app in the container
            - name: app-config
              mountPath: /usr/src/app/config
            - name: app-config-ai
              mountPath: /usr/src/app/config/ai
            - name: app-config-components
              mountPath: /usr/src/app/config/components
            - name: app-config-components-ui
              mountPath: /usr/src/app/config/components/ui
      volumes:
        - name: app-config
          configMap:
            name: app-config
        - name: app-config-ai
          configMap:
            name: app-config-ai
        - name: app-config-components
          configMap:
            name: app-config-components
        - name: app-config-components-ui
          configMap:
            name: app-config-components-ui
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app_id: myId
    app_subdomain: testapp
  name: nginx-service
  namespace: myId
spec:
  ports:
    - port: 80
      protocol: TCP
      targetPort: 80
  selector:
    app_id: myId
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app_id: myId
    app_subdomain: testapp
  name: app
  namespace: myId
spec:
  ports:
    - port: 3000
      protocol: TCP
      targetPort: 3000
  selector:
    app_id: myId
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  labels:
    app_id: myId
    app_subdomain: testapp
  name: nginx
  namespace: myId
spec:
  rules:
    - host: testapp.zshape.ai
      http:
        paths:
          - backend:
              service:
                name: nginx-service
                port:
                  number: 80
            path: /
            pathType: Prefix

This is an example import of a JSON file in my NextJS app:

import rawConfig from '@/config/components/empty-screen.config.json'

interface ComponentConfig {
  [key: string]: any
  title?: string
  description?: string
  linkURL?: string
  linkLabel?: string
}

const { title, description, linkURL, linkLabel }: ComponentConfig = rawConfig

export const emptyScreenConfig = {
  title,
  description,
  linkURL,
  linkLabel
}

Then the exported emptyScreenConfig is imported in other parts of the app.


Solution

  • I solved this by using next dev --turbo that uses turbopack instead of webpack. In my case, I uses the canary version of next and the beta version of next-auth, because some features are not supported in the current stable version.