Resolving nextjs PUBLIC_ENV undefined prod issue

Context
Backstory
The Issue
The solution
Closing notes

Context

Next.js allows us to define env vars that we want to expose to the browser (consequently making them public) with the prefix NEXT_PUBLIC_, meaning that they will be bundled into the code (at build time, something I wish I had known back then) and made available to the browser.These are usually necessary for integrating with public APIs, like the Google Maps API, reCAPTCHA APIs, CMS APIs, etc.

Backstory

I worked on a Next.js v12 app for an extended period of time and never encountered any env var issues whilst hosting the app on Vercel. All you had to do was redeploy the app every time you added a new env var (looking back, that should have been a tell of what was going on under the hood). Somewhere along the way, we switched to hosting the app on a Docker container on Azure's "Container Apps", which are specifically optimised for hosting Docker apps.

The initial setup didn't involve any CI when deploying to prod, which meant we built the image ourselves locally, pushed it directly to the container registry, and the container app would automatically grab the latest image and run it. This worked flawlessly for some time, UNTIL the moment when we eventually introduced CI/CD (Azure Pipelines in this case) and moved away from "Container Apps" to Azure's "App Services" (app services have more flexibility than container apps, whilst retaining most of the container apps' features).

The issue

All of a sudden, some of our 3rd-party API integrations were broken in production but worked perfectly locally, be it in dev mode or prod mode. Debugging the deployed app revealed that the NEXT_PUBLIC_ envs were "undefined", even though we had set them up in the "environment variables" section of the config settings on Azure.What was even more confusing at the time was that accessing the public envs from within Next.js' API routes a.k.a. the Next.js backend routes worked perfectly, both locally in dev and prod mode as well as on Azure App Services.

I faced this issue for at least 12 months, with no clear answer as to why this issue was happening and with only a couple of (wrong) assumptions to lean on. To be fair, finding a solution to the problem was never prioritized as there were bigger features to tackle, and the "band-aid solution" was to just set the env vars as a constant within the code (a risky move for sure, as one could easily include a secret key within the code without the proper labelling, a problem that NEXT_PUBLIC_ envs attempt to mitigate).

The solution

Working on a new feature that depended on a public env is what led me back to the issue (about 12 months later), prompting me to invest time into debugging the issue. It turns out that NEXT_PUBLIC_ env vars should be available "at build time", so that they can be bundled into the code and made available to the browser. That means that whichever environment you're using to build your app should have these public env vars configured. This is not usually an issue if you're building the application locally, but needs to be addressed if you're building your app in a Docker image on a CI/CD pipeline. My solution was to pass the public envs to the Dockerfile using build arguments, access the build args using ARG in the Dockerfile and then pass them to the layer environment variables using Docker's ENV command:


# Declare build arguments for Next.js public environment variables
ARG NEXT_PUBLIC_ENV_NAME

# Pass the build arguments to environment variables
# This makes them available during the build process and in the running container
ENV NEXT_PUBLIC_ENV_NAME=${NEXT_PUBLIC_ENV_NAME}

This also gives your Dockerfile the versatility to be used in both Azure Pipelines or GitHub Actions.This means that in your GitHub Actions file you can make these available through GitHub Secrets like this:


- name: Build and push Docker image
      uses: docker/build-push-action@v6
      with:
        context: .
        push: true
        tags: ${{ secrets.DOCKER_USERNAME }}/my-nextjs-app:latest
        build-args: |
          NEXT_PUBLIC_ENV_NAME=${{ secrets.NEXT_PUBLIC_ENV_NAME }}

And in Azure Pipelines, you could use Azure DevOps' "variable groups" in your Azure Pipelines YAML file to pass these variables like this:


variables:
  # Reference variable groups based on branch/environment
  - group: docker-registry-config # Common Docker settings

  - ${{ if eq(variables['Build.SourceBranch'], 'refs/heads/dev') }}:
      - group: nextjs-dev

# other steps...

          - task: Docker@2
            inputs:
              command: "build"
              dockerfile: "./path/to/Dockerfile"
              buildContext: "."
              repository: $(DOCKER_IMAGE_NAME_CLIENT)
              containerRegistry: "$(DOCKER_REGISTRY_SERVICE_CONNECTION)"
              tags: |
                $(DOCKER_IMAGE_TAG)
              arguments: |
                --build-arg NEXT_PUBLIC_ENV_NAME==$(NEXT_PUBLIC_ENV_NAME)

In this case it would be important to ensure that the variable group you reference has those environment variables defined.

Closing notes

Initially, I used to build the Docker image locally and push it to a container registry. Since the local repository also included a .env file, it meant that the env variables were available at build time, and thus I never experienced any issues with these.Hopefully, my experience can help save you hours of debugging this issue like I did and can also help someone who had previously solved the issue with a much jankier approach, to simplify their solution.