Get Environment Variable Kubernetes on Next.js App

Viewed 5843

I've been reading other questions about getting K8s environment variables to work in a Next.js app, but no accepted answer till now.

My app works fine using .env.local but it's getting an error (undefined) when deployed to K8s.

This is my next.config.js

module.exports = {
  env: {
    NEXT_PUBLIC_API_BASE_URL: process.env.NEXT_PUBLIC_API_BASE_URL,
  },
};

K8s environment:

k8s environment

Can anyone help me to get that environment var works on my next.js app?

Right now I do a simple trick, that is added ARG and ENV on dockerfile, then inject it when I build the docker image

Dockerfile:

ARG NEXT_PUBLIC_API_BASE_URL
ENV NEXT_PUBLIC_API_BASE_URL=${NEXT_PUBLIC_API_BASE_URL}
6 Answers

You should add the ENV_VARS in a .env.local file. in form of a configMap. (https://nextjs.org/docs/basic-features/environment-variables)

In Kubernetes you create a configMap like so:

apiVersion: v1
name: env-local
data:
  .env: |-
    NEXT_PUBLIC_API_URL=http:/your.domain.com/api
    API_URL=http://another.endpoint.com/serverSide
kind: ConfigMap

Then you mount that configMap as FILE into your deployment, it then is available at app/.env.local:


apiVersion: apps/v1
kind: Deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: your-app
  template:
    metadata:
      labels:
        app: your-app
    spec:
      containers:
      - image: your/image:latest
        imagePullPolicy: Always
        name: your-app
        ports:
        volumeMounts:
        - mountPath: /app/.env.local
          name: env-local
          readOnly: true
          subPath: .env.local
      volumes:
      - configMap:
          defaultMode: 420
          items:
          - key: .env
            path: .env.local
          name: env-local
        name: env-local


What also worked - for me at least - for server side vars was simply adding them as regular env vars in my deployment: https://kubernetes.io/docs/tasks/inject-data-application/define-environment-variable-container/#define-an-environment-variable-for-a-container

apiVersion: v1
kind: Pod
metadata:
  name: your-app
  labels:
    purpose: demonstrate-envars
spec:
  containers:
  - name: your-app-container
    image: gcr.io/google-samples/node-hello:1.0
    env:
    - name: DEMO_GREETING
      value: "Hello from the environment"
    - name: DEMO_FAREWELL
      value: "Such a sweet sorrow"

    const withSvgr = require('next-svgr');
    
    module.exports = {   
        // Will only be available on the server side
        serverRuntimeConfig: {
            API_URL: process.env.API_URL,
        },
        // Will be available on both server and client
        publicRuntimeConfig: {
            NEXT_PUBLIC_API_URL: process.env.API_URL,
        },
    };

I spent whole day experimenting with the ways to throw my vars into next js app not exposing them in a repo. None of above mentioned clues did the job, same as official docs. I use GitLab CI/CD for building stage, and K8S deployments. Finally made it work like so:

  1. Create Project variables in GitLab
  2. in .gitlab-ci.yml reconstructed .env.local (since it's the only point you get vars from)
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - touch .env.local
    - echo "NEXT_PUBLIC_API_KEY='$NEXT_PUBLIC_API_KEY'" | cat >> .env.local
    ...
    - echo "NEXT_PUBLIC_MEASUREMENT_ID='$NEXT_PUBLIC_MEASUREMENT_ID'" | cat >> .env.local
    - docker build -t $IMAGE_TAG .
    - docker push $IMAGE_TAG

Try to remove the definition of the environment variable from your dockerfile. Then add the definition of the environment variable to your deployment(or pod or replicaset), for exemple:

...
spec:
  containers:
    - name: test-container
      image: gcr.io/kuar-demo/kuard-amd64:blue
      imagePullPolicy: Always
env:
  - name: NEXT_PUBLIC_API_BASE_URL
    value: ANY_VALUE
...

I ran into this issue and got it working with Docker while still respecting the 12 Factor App Rules, the TL;DR is you need to modify your next.js.config and your _app.js files with the following:

next.js.config

/** @type {import('next').NextConfig} */
const nextConfig = {
  publicRuntimeConfig: {
    // remove private variables from processEnv
    processEnv: Object.fromEntries(
      Object.entries(process.env).filter(([key]) =>
        key.includes('NEXT_PUBLIC_')
      )
    ),
  },
}

module.exports = nextConfig

_app.js

import App from 'next/app'

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />
}

// Only uncomment this method if you have blocking data requirements for
// every single page in your application. This disables the ability to
// perform automatic static optimization, causing every page in your app to
// be server-side rendered.

MyApp.getInitialProps = async (appContext) => {
  // calls page's `getInitialProps` and fills `appProps.pageProps`
  const appProps = await App.getInitialProps(appContext);
  return { ...appProps }
}

export default MyApp

To access the environment variables in any page or component simply add this:

import getConfig from 'next/config';

const {
  publicRuntimeConfig: { processEnv },
} = getConfig();

Here’s an example of what component would look like:

import getConfig from 'next/config';

const {
  publicRuntimeConfig: { processEnv },
} = getConfig();

const Header = () => {

  const {NEXT_PUBLIC_MESSAGE} = processEnv;
  
  return (
    <div>
      Hello, {NEXT_PUBLIC_MESSAGE}
    </div>
  )
}

export default Header;

The real issue is the way the Dockerfile starts the app in order to load env vars we need to start it with npm start.

I wrote an article with my findings if you want to get the full details of why and how it works: https://benmarte.com/blog/nextjs-in-docker/

I also made a sample repo which can be used as a template: https://github.com/benmarte/nextjs-docker

I will make a PR this week to the with-docker repo.

Kubernetes set the environment variables in runtime. But the NEXT_PUBLIC_API_BASE_URL is created in BUILD TIME, not in RUN TIME. That means that env var should be in the .env file when you run the command npm run build. It's not possible to set up that env var in run time according to the documentation:

This inlining occurs at build time, so your various NEXT_PUBLIC_ envs need to be set when the project is built.

enter image description here

https://nextjs.org/docs/basic-features/environment-variables#exposing-environment-variables-to-the-browser

What you can do is implement the getServerSideProps and return the value in the props.

Or you have another option more complex to achieve the configuration in runtime there is a workaround like this https://dev.to/akdevcraft/react-runtime-variables-49dc

TL;DR: next build && next start from within the container

I'm also using Next with Kubernetes. Howver, in recent Next versions (I'm running version 11) environment variables are resolved at build time. I guess this is an static optimization feature.

In order to leverage the ConfigMap to update values of the app (almost) on the fly, I ended up adding a next build just before the next start from within the container.

My package.json contains the following:

{
  "scripts": {
    "start:docker": "next build && next start"
  }
}

The drawback is the application container will take much longer to be up, but honestly until Next come with a built-in fix for this workflow, I'll stick to this.

Heads up since you container takes longer to be up, don't forget to configure your k8s readinessProbe.

Edit: I realized two more big drawbacks to this approach, hence I would not recommend.

  1. Resource usage will increase significantly during the build. On k8s exceeding the memory limits will get your pod killed.
  2. The pods will have different JS bundle outputs e.g. /_next/static/chunks/pages/_app-.js which would lead to 404 if page is loaded in one pod and the static in another pod.
Related