NextJS build vs runtime environment variables

:wave: I’m fairly new to using NextJS and trying to understand how build and runtime variables are loaded and how they interact with fly.toml sections [build.args] and [env].

I’ve looked at the NextJS docs about environment variable precedence, but I’m not sure how that interacts with fly.toml.

For context, I’m trying to set an environment variable with one value during the build phase and another during the server-side runtime phase. No matter how I configure it both the build phase and the runtime phase have the same value. I’ve tried setting them in the Dockerfile, in .env.production files and in fly.toml. Even doing

RUN BUILD_ENV=fly npm run build
CMD BUILD_ENV=none nump run start

Didn’t work.

I access the value via process.env['BUILD_ENV'] to avoid inlining but it doesn’t seem to be working.

Halp!


Here’s some more context with full files:

Dockerfile

ARG RUBY_VERSION=3.2.2
ARG BUNDLER_VERSION=2.2.33
ARG YARN_VERSION=1.22.19
ARG NODE_VERSION=18

FROM node:${NODE_VERSION} as www

RUN apt-get update &&\
     apt-get install --yes build-essential git pkg-config redis libpq-dev

RUN mkdir trove-www
WORKDIR /trove-www
COPY . .

RUN npm ci

ENV NEXT_TELEMETRY_DISABLED 1
ARG NEXT_PUBLIC_TROVE_API_ENDPOINT
ARG BUILD_ENV
RUN npm run build
CMD npm run start

fly.toml

[build.args]
  BUILD_ENV = "fly"

[env]
  BUILD_ENV = "none"

hello.js

const env = process.env
const key = "BUILD_ENV"

async function getData() {
    console.log(process.env)

    if (env[key] == "fly") {
      return {name: "Fetch skipped"}
    }

    //const endpoint = process.env.NEXT_PUBLIC_TROVE_API_ENDPOINT + "/info/name"
    const endpoint = "http://127.0.0.1/info/name"
    const res = await fetch(endpoint, { cache: 'no-store' })
   
    if (!res.ok) {
      return {name: "Error"}
    }
   
    return res.json()
}
 
export default async function Hello() {
  const data = await getData();
 
  return (
    <div>
      <h1>Your name is: {data.name}</h1>
      <h2>Env is: {process.env.NODE_ENV}</h2>
      <h2>BUILD_ENV is: {env[key]}</h2>
      <h3>Foo bar is: {process.env.FOO_BAR}</h3>
    </div>
  )
}

.env.production

FOO_BAR="oh its a foo bar from the local file"
BUILD_ENV="none"

Hi,

I’m not entirely sure. Perhaps I’m not clear on the why, as you say you are accessing the value to avoid inlining … but isn’t that the reason you would want to extract a variable at build-time :thinking:. So … that it could be bundled into the JS?

Since there is a bit that runs on the server (that uses run-time variables). And then that results in some HTML, JS etc being delivered to the client. That client-code can’t have “process.env.X” in … because that wouldn’t make sense. Hence the need to inline environment variables.

You seem to have commented out the line in hello.js where you have prefixed a variable with the crucial NEXT_PUBLIC_. My understanding is that that is precisely what you need to do if you want the value to be built in, and so delivered in the JS that runs on the client. This being the key paragraph:

Fly will make the contents of [env] or fly secrets available at run-time (when the VM starts) and put them in process.env. So if you have any EXAMPLE value already in there, they would be overwritten. Next will use whatever value it sees when the code is run on the server. As above, unless you explicitly tell it that what you want it to do is to build the value of that variable, inline, instead.

You could take Fly out of the equation by trying it locally. Build the image locally. That’s often a good way to debug as if it works locally, then it should work on Fly.

Thank you for the detailed response!

To answer the “why” I’ll take a step back: my main goal here is to detect whether or not my code is being run as part of next build or next start.

The reason I want to detect the build vs runtime environment is the call to fetch in the server-side component. Because I’m not caching the result of that call, I want to just skip it entirely during the build phase and only make the call when we’re in a runtime environment.

My idea was to have an environment variable with the value "fly" in the build context and "none" in the runtime context. But it seems like I’ve got the wrong mental model about process.env.

For the above reason, I’m actually trying to avoid extracting this variable at build-time :smile:

1 Like

There are a number of environment variables, mostly starting with the letters FLY_ that can be used.

This list may be useful: The Fly Runtime Environment · Fly Docs

Also try:

fly ssh console -C printenv | grep FLY

Ah … Yes, that explains it.

In that case, see the answer from @rubys

@rubys @greg I’m really starting to wonder if I have the wrong mental model for environment variables in NextJS and next build vs next start. You can see on my little test page that process.env.FLY_APP_NAME is empty.

Rendered with:

export default async function Hello() {
  const data = await getData();
 
  return (
    <div>
      <h1>Your name is: {data.name}</h1>
      <h2>Env is: {process.env.NODE_ENV}</h2>
      <h2>BUILD_ENV is: {env[key]}</h2>
      <h3>Foo bar is: {process.env.FOO_BAR}</h3>
      <h3>Fly app name: {process.env.FLY_APP_NAME}</h3>
    </div>
  )
}

What’s likely happening is that npm run build is producing static files which is served by something like express. What that means is that at runtime your test page is not running under node.js at all, but rather in the browser.

A package like browser-or-node - npm may help.

This will also work:

if (typeof window === 'undefined')
// this is node

Yep, it may be the static/dynamic causing confusion. Further to that:

In Next.js, a route can be statically or dynamically rendered.

  • In a static route, components are rendered on the server at build time. The result of the work is cached and reused on subsequent requests.
  • In a dynamic route, components are rendered on the server at request time.

Also: I found a random stackblitz showing process.env in action. May be of some help to play with to see which variables are on the server vs. client. Again removing Fly from the equation while debugging:

I think this is partially true, but not entirely. My understanding is that next build does some building of static files, but things are entirely pre-rendered. For example, if the results of my fetch call change, that change is shown when I reload the page.

This works to detect the client, but not to detect the build environment. In my mental model there are three modes for a NextJS app:

  1. Client mode where code runs in the browser
  2. Server mode where code runs in node on Fly
  3. Build mode where code runs inside Docker

I’ve been trying to find a way to distinguish between 2 and 3

Yes! Thank you for pointing this out. I read those docs but didn’t update my mental model after. This is definitely part of what’s causing confusion. It doesn’t help with detecting the build environment, but I can sidestep the entire problem.

I think I’ve discovered a bug (or NextJS docs are wrong). The dogs say that using a fetch without a cache should switch this component to dynamic rendering, which is not happening. But if I use the cookies() function, it appears to switch to dynamic rendering.

This appears to be working as expected:

import { cookies } from 'next/headers'

async function getData() {
    console.log(process.env)
    const cookieStore = cookies()

    const endpoint = process.env.TROVE_API_ENDPOINT + "/info/name"
    const res = await fetch(endpoint, { cache: 'no-store' })
   
    if (!res.ok) {
      return {name: "Error"}
    }
   
    return res.json()
}
 
export default async function Hello() {
  const data = await getData();
 
  return (
    <div>
      <h1>Your name is: {data.name}</h1>
      <h2>Env is: {process.env.NODE_ENV}</h2>
      <h2>API Endpoint is: {process.env.TROVE_API_ENDPOINT}</h2>
    </div>
  )
}
1 Like

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.