Journal indexDockup / field note
Note / env-vars-not-reaching-container

Environment Variables Not Reaching the Container

Environment variables not working in a container usually fail for one of five reasons: build time versus runtime, frontend bundling, quoting, restart timing, or the wrong scope. Check them in order.

You set the variable. The dashboard shows it. The application says it is undefined. Environment variables not working in a container is one of the most common configuration failures in application hosting, and it is nearly always one of five specific things.

They are listed here in the order that finds the problem fastest.

1. Build time and runtime are different worlds

This causes more of these than the other four combined, and it is the one people find least intuitive.

Variables set on your service exist when the container runs. Anything your Dockerfile does happens earlier, in a separate environment. A RUN step cannot see a runtime variable, because at that moment there is no runtime.

# This is empty during build. Always.
RUN echo $DATABASE_URL

# This is available at runtime, because it is the running process reading it
CMD ["node", "server.js"]

If you genuinely need a value during the build, it has to be passed as a build argument — which is a different mechanism with different security properties:

ARG BUILD_VERSION
RUN echo "Building $BUILD_VERSION"

Never pass a secret this way. Build arguments are recorded in the image's layer history. Anyone who can pull the image can read them.

2. Frontend variables are baked in, not read

If your frontend says undefined in production, this is almost certainly why.

A browser has no environment. When you write import.meta.env.VITE_API_URL or process.env.NEXT_PUBLIC_API_URL, the bundler substitutes the literal string at build time. There is no lookup happening in the browser — the value was compiled in.

Three consequences that catch people out:

  • Changing the variable does nothing until you rebuild. The old value is inside the JavaScript file.
  • The prefix is mandatory. Vite only exposes VITE_, Next.js only exposes NEXT_PUBLIC_. A variable without the prefix is deliberately withheld.
  • Anything exposed this way is public. It is in a file you serve to anyone. Never put a secret behind NEXT_PUBLIC_, whatever the naming suggests.

This is also why a prebuilt image cannot be configured this way after the fact. If the image was built elsewhere with the values compiled in, setting variables on the service changes nothing — the strings are already in the bundle.

3. Quoting

Values with special characters get mangled in ways that produce confusing errors rather than obvious ones.

# The shell eats everything after #
dockup env set DB_PASS=p@ss#word my-project/my-api

# Quote it
dockup env set 'DB_PASS=p@ss#word' my-project/my-api

Characters that cause this: # (comment), $ (expansion), spaces (argument splitting), ! (history expansion in interactive bash), and newlines — which appear in exactly one common case, private keys.

Multi-line values are the worst offender. A PEM key pasted into a single-line field arrives with its newlines removed and produces a parse error that says nothing about newlines. Base64-encode it and decode in the application:

dockup env set "PRIVATE_KEY_B64=$(base64 -i key.pem)" my-project/my-api

4. You did not restart

Environment variables are read by a process when it starts. Changing them affects the next process, not the one currently running.

Most platforms handle this by redeploying automatically when configuration changes, but not all do, and a partial change — set three variables, redeploy, set a fourth — leaves one behind.

dockup env list my-project/my-api --json   # what is configured
dockup restart my-project/my-api            # make the process re-read it

The check that settles it: read the variable from inside the running container, not from the dashboard.

dockup exec "printenv | sort" my-project/my-api

If it is in that output and your app still says undefined, the problem is in your code. If it is not in that output, the problem is in the configuration. That one command splits the search space in half.

5. Wrong scope

Variables are usually scoped — to a service, an environment, or a project. A variable set on production is not visible in a preview environment. One set on a different service in the same project is not visible either.

This is the usual cause when something works in one place and not another with identical code.

The diagnostic order

# 1. Is it actually in the container's environment?
dockup exec "printenv | sort" my-project/my-api

# 2. Is it configured on the service you think it is?
dockup env list my-project/my-api --json

# 3. Is the running process older than the change?
dockup status my-project/my-api --json

Step 1 first, every time. It converts an ambiguous problem into one of two unambiguous ones.

Secrets specifically

Two habits are worth adopting regardless of platform.

Mark secrets as secrets. On Dockup a variable marked secret is masked in listings and API responses — dockup env list shows ******** rather than the value. That matters more than it sounds like it does, because the most common way a credential leaks is not an attack; it is a screenshot, a support ticket, or a log line.

Keep them out of build arguments and out of frontend bundles. Both are readable by anyone who obtains the artefact. The rule of thumb: if it ends up in a file you distribute, it is not a secret any more.

Frequently asked questions

Why is my environment variable undefined at build time? Because build and runtime are separate environments. Runtime variables do not exist while the image is being built. Use a build argument if you truly need a value during the build — and never a secret.

Why does my frontend not see the variable? Bundlers substitute the value at build time and only expose prefixed names — VITE_, NEXT_PUBLIC_. Changing the variable requires a rebuild, and anything exposed this way is publicly readable.

Do I need to restart after changing a variable? Yes. A running process has already read its environment. Most platforms redeploy automatically on change; verify with printenv inside the container rather than trusting the dashboard.

How do I pass a multi-line value like a private key? Base64-encode it, set the encoded string, and decode in the application. Single-line environment fields strip newlines and produce parse errors that do not mention newlines.