Jason McFarlane
Jason McFarlane

Reputation: 2175

Cant access NEXTjs env in kubernetes/terraform

This question has been asked before, ive been trying plenty of examples over the past two days to try and configure with no luck so I am posting my environment for any help.

Problem
Nextjs environment variables are all undefined after deploying to kubernetes using Terraform

Expected Result

staging: NEXT_PUBLIC_APIROOT=https://apis-staging.mywebsite.com
production: NEXT_PUBLIC_APIROOT=https://apis.mywebsite.com

The secrets are stored in github actions. I have a terraform setup that deploys my application to my staging and production klusters, a snippet below:

env:
  ENV: staging
  PROJECT_ID: ${{ secrets.GKE_PROJECT_STAG }}
  GOOGLE_CREDENTIALS: ${{ secrets.GOOGLE_CREDENTIALS_STAG }}
  GKE_SA_KEY: ${{ secrets.GKE_SA_KEY_STAG }}
  NEXT_PUBLIC_APIROOT: ${{ secrets.NEXT_PUBLIC_APIROOT_STAGING }}

I have an additional step to manually create a .env file as well

    - name: env-file
      run: |
        touch .env.local
        echo NEXT_PUBLIC_APIROOT: ${{ secrets.NEXT_PUBLIC_APIROOT_STAGING }} >> .env.local

Dockerfile

FROM node:16-alpine AS deps
RUN apk add --no-cache libc6-compat

WORKDIR /app
COPY package.json package-lock.json .npmrc ./
RUN npm ci

FROM node:16-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM node:16-alpine AS runner
WORKDIR /app

RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001

# You only need to copy next.config.js if you are NOT using the default configuration
COPY --from=builder /app/next.config.js ./
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json

# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT 3000

CMD ["node", "server.js"]

next.config.js

module.exports = withBundleAnalyzer({
    publicRuntimeConfig: {
        NEXT_PUBLIC_APIROOT: process.env.NEXT_PUBLIC_APIROOT,
    },
    output: 'standalone',
    webpack: (config, { dev, isServer }) => {
        if (dev && isServer) {
            const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
            config.plugins.push(
                new ForkTsCheckerWebpackPlugin({
                    eslint: {
                        files: './src/**/*.{ts,tsx,js,jsx}',
                    },
                })
            )
        }
        return config
    },
})

Anybody have experience with this issue?

Upvotes: 6

Views: 2424

Answers (1)

vladzam
vladzam

Reputation: 5898

I wanna start by saying that I am by no means an expert in NextJS. Therefore, I tried approaching your problem under the following assumptions:

  • the issue is not necessarily related to NextJS, as it appears to be related to Kubernetes resource deployment and management
  • leverage the default next.config.js mechanism that automatically loads environment variables from .env.local
  • use a GKE cluster to use the same deployment target (region: us-central1-c)

My first step was to create a dummy NextJS application with a single API endpoint that simply prints one of the environment variables that I am trying to set when deploying the workload to Kubernetes. When it comes to the Dockerfile, I used the exact same image that you provided. Please find below the relevant files from my dummy app:

pages/api/test.js

export default function handler(req, res) {
    res.status(200).json(process.env.NEXT_PUBLIC_APIROOT)
}

next.config.js

const withBundleAnalyzer = require('@next/bundle-analyzer')({
    enabled: true,
});

module.exports = withBundleAnalyzer({
    publicRuntimeConfig: {
        NEXT_PUBLIC_APIROOT: process.env.NEXT_PUBLIC_APIROOT,
    },
    output: 'standalone'
})

Dockerfile

FROM node:16-alpine AS deps
RUN apk add --no-cache libc6-compat

WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

FROM node:16-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM node:16-alpine AS runner
WORKDIR /app

RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001

# You only need to copy next.config.js if you are NOT using the default configuration
COPY --from=builder /app/next.config.js ./
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json

# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT 3000

CMD ["npm", "start"]

There is a single change that I've done in the Dockerfile and that is updating the CMD entry so that the application starts via the npm start command.

As per the official documentation, NextJS will try to look for .env.local in the app root folder and load those environment variables in process.env.

Therefore, I created a YAML file with Kubernetes resources that will be used to create the deployment setup.

nextjs-app-setup.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: nextjs-app-config
data:
  .env.local: |-
    NEXT_PUBLIC_APIROOT=hello_i_am_an_env_variable
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nextjs-app
  labels:
    app: nextjs-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nextjs-app
  template:
    metadata:
      labels:
        app: nextjs-app
    spec:
      containers:
      - name: nextjs-app
        image: public.ecr.aws/u4x8r8g3/nextjs-app:latest
        ports:
        - containerPort: 3000
        volumeMounts:
        - name: nextjs-app-config
          mountPath: "/app/.env.local"
          subPath: ".env.local"
          readOnly: true
      volumes:
      - name: nextjs-app-config
        configMap:
          name: nextjs-app-config
---
apiVersion: v1
kind: Service
metadata:
  name: nextjs-service
spec:
  selector:
    app: nextjs-app
  ports:
    - protocol: TCP
      port: 3000
      targetPort: 3000

There are multiple things happening in the above configuration:

  • Define a ConfigMap resource that will hold all of the required environment variables that the NextJS application will require. There is a single entry for .env.local that will hold all of the environment variables and will be mounted as a file in the application pod
  • Define a Deployment resource for the NextJS application. The most important section here is the volumes and volumeMounts blocks. Here, I am mounting the .env.local entry from the ConfigMap that was defined on the /app/.env.local path
  • Define a Service resource to be able to interact with the NextJS application

After connecting to the GKE cluster via kubectl, I applied the configuration via kubectl apply -f nextjs-app-setup.yaml.

To connect to the service from my local workstation, I executed kubectl port-forward service/nextjs-service 3000:3000. Then I navigated in my browser to localhost:3000/api/test and can see the value that I set in the ConfigMap as the output.

Disclaimer: I understand that your setup might involve some additional components especially when it comes to CI/CD and Infrastructure-as-Code, but my answer here should at least provide you with an approach to accessing environment variables in your containerized NextJS workloads. If you still get undefined values, my assumption is that it would most likely be related to how you are configuring them in your CI/CD pipeline, but that would be a different issue that is not related to NextJS or Kubernetes.

Upvotes: 2

Related Questions