← Back home

2026.09 / configuration layers 055

Coolify environment variables not updating? Separate build time from runtime

You change an environment variable in Coolify, redeploy the application, and the old value remains on the page. The deployment is green. The replacement container is running. Another server-side setting may have changed correctly, which makes the stale one even more confusing.

The usual cause is not that environment variables are random. It is that the value crossed a boundary you have not identified. It may have been read while building the Docker image, injected when the container started, copied into a browser bundle, overridden by a file, or cached after a successful release. The fix is to classify the consumer first, then rebuild or restart the layer that actually owns the value.

A runtime restart cannot rewrite JavaScript or HTML that was generated at build time. A rebuild cannot help if the new container is still receiving an overridden runtime value.

The three places a value can live

LayerTypical consumersWhat a change requires
Build timeStatic-site generator, bundler, Dockerfile RUN, compiled assetsA new image build with the variable available to that build
RuntimeNode, Python, PHP or another server process reading its environmentA replacement or restarted container with the new variable
Browser/runtime configurationClient code fetching a config endpoint or generated config.jsA changed response or startup-generated file, plus cache-aware verification

A variable can be needed in more than one layer. For example, a server-rendered app may read a private API origin at runtime while its build system needs a public analytics hostname to create client assets. Coolify’s environment-variable settings therefore expose independent Build Variable and Runtime Variable controls. Its documentation says both are enabled by default for newly created variables, but imported, edited or older application configurations still deserve inspection rather than assumption.

1. Find where the application reads the variable

Start in source. Search for the exact variable name, including Dockerfiles, Compose files, framework configuration, startup scripts and generated-config templates:

git grep -n 'PUBLIC_API_ORIGIN'
git grep -nE 'ARG |ENV |environment:|env_file:|NEXT_PUBLIC_|VITE_|PUBLIC_' \
  -- Dockerfile* '*compose*.yml' '*compose*.yaml' '*.js' '*.ts' '*.mjs'

Do not search for or print a secret’s value. The variable name and the code path normally provide enough evidence. Classify each read:

Next.js documents the important frozen-value behaviour directly: NEXT_PUBLIC_ variables are inlined into the browser bundle during next build. Once that image exists, changing the container environment does not rewrite the bundle. Other frontend bundlers use different prefixes, but the same principle often applies: public client configuration becomes build output.

2. Check the Coolify flags, scope and exact name

Open the application’s Environment Variables view and inspect the variable without revealing its value. Confirm:

  1. The name matches source exactly, including case.
  2. The variable belongs to the intended application, environment and destination server.
  3. Build Variable is enabled if a build command reads it.
  4. Runtime Variable is enabled if the running process reads it.
  5. The value is not accidentally defined twice at different scopes.
  6. A preview or pull-request environment is not being confused with production.

Save the setting before deploying. If automation updates Coolify through its API, write the environment variable through Coolify’s environment-variable endpoint rather than modifying encrypted database fields or generated container definitions directly. Then fetch the application configuration again and compare the variable’s name and flags—not its secret value—to prove the change reached the control plane.

3. Understand Docker ARG and ENV

Docker’s build-variable documentation distinguishes build arguments from environment variables. An ARG is available to Dockerfile instructions in the build stage where it is declared. An ENV sets an environment variable that can persist in the image and its containers. Neither is a safe secret store.

# Public build configuration only—not a secret
ARG PUBLIC_API_ORIGIN
ENV PUBLIC_API_ORIGIN=$PUBLIC_API_ORIGIN
RUN npm run build

This pattern can make a public value available to the build, but do not copy API tokens, database passwords or private keys into an image layer or browser bundle. Docker explicitly warns that build arguments and environment variables are inappropriate for build secrets because they can persist in image metadata or layers. Use supported secret mounts for private build credentials, and keep browser-exposed variables genuinely public.

Multi-stage builds add another trap: an ARG declared in one stage is not automatically a normal runtime variable in the final stage. Declare and consume inputs deliberately in the stage that needs them. Then remember that a static frontend copied from the build stage contains the result of the build, not a live connection to future container variables.

4. Choose rebuild, restart, or both

EvidenceCorrect action
Only server-side runtime code reads the variableReplace/restart the container; a source rebuild may be unnecessary
Build command or frontend bundler reads itRun a new image build with Build Variable enabled
Both build and server code read itEnable both flags and deploy a newly built replacement container
Startup script generates public configReplace the container and verify the generated config response or file
Value comes from Compose or an env fileRepair that source of truth, commit when appropriate, then redeploy

For a build-time value, use a forced rebuild once if an ordinary deploy can legitimately reuse a previously built image or cached build result. This is diagnostic, not a reason to disable caching forever. The deployment log should show the expected build command running from the intended Git commit. Follow the specific deployment identifier until it reaches a terminal successful state.

If the deployment fails, read that deployment’s log. Do not keep clicking redeploy: a missing build variable may appear as an explicit validation error, while some tools quietly replace it with an empty string. Either outcome is more useful than a queue full of identical attempts. For deployment-state troubleshooting, use the Coolify queued or in-progress deployment runbook.

5. Check for higher-precedence configuration

If Coolify shows the expected flags and a rebuild still produces the old behaviour, identify every competing source:

Do not commit a real production .env file just to win a precedence fight. Remove an obsolete public default or align the deployment configuration instead. Repository history is permanent enough that deleting a secret in the next commit is not an adequate repair; rotate anything accidentally committed.

6. Verify presence without leaking the value

For a non-sensitive public value, you can inspect the rendered output directly. For anything private, verify only whether the variable exists or compare a one-way digest locally. Avoid pasting complete docker inspect output into tickets because container environments commonly hold credentials.

# Presence only; does not print the value

docker exec APP_CONTAINER sh -lc \
  'test -n "$PRIVATE_SERVICE_TOKEN" && echo present || echo missing'

# Names only from the configured container environment

docker inspect APP_CONTAINER \
  --format '{{range .Config.Env}}{{println .}}{{end}}' | \
  cut -d= -f1 | sort

Even a digest should remain private when the input has low entropy. The safest production acceptance check is usually behavioural: call a narrow endpoint that depends on the setting and confirm the expected non-secret result. Do not add a debug route that dumps the environment.

7. Prove which release the public route serves

A correct container can exist while the domain still reaches an older replica, old route or cached asset. Verify the source commit or harmless release marker in the running application, then fetch unique public content with cache awareness:

curl --fail --silent --show-error \
  'https://app.example.com/settings-check?v=RELEASE_ID' | \
  grep -F 'EXPECTED PUBLIC LABEL'

curl --silent --show-error --head \
  'https://app.example.com/assets/app.RELEASE_ID.js'

Use a real fingerprinted asset name from the generated manifest rather than inventing one. If HTML points to a new bundle but a browser displays old behaviour, test the bundle URL directly and inspect cache headers. If the HTML itself is old, return to deployment, routing and CDN checks. The broader Coolify deploy-not-updating checklist traces that complete path.

A safer pattern for one image across environments

Inlining public configuration at build time means the image is tied to that environment. Promoting the exact same image from staging to production will also promote the values baked into its client bundle. If one immutable image must run in several environments, move environment-specific public configuration to runtime deliberately—for example, a server-rendered response, a small startup-generated config file, or an authenticated server endpoint.

That design has trade-offs. A public config file must not contain secrets, needs an explicit cache policy, and must load before client code that depends on it. Server-only values should remain server-only. The important part is not which pattern you choose; it is that the source, lifecycle and exposure of each value are documented.

Compact acceptance checklist

  1. The exact variable name and every source-code read have been identified.
  2. Each read is classified as build-time, runtime or browser/runtime configuration.
  3. The variable is attached to the intended Coolify application and environment.
  4. Build Variable and Runtime Variable flags match the actual consumers.
  5. No Compose, env-file, Dockerfile or framework default unexpectedly overrides it.
  6. No private value is copied into a Docker layer, log, client bundle or public report.
  7. A new build runs when generated assets depend on the value.
  8. The replacement container receives required runtime variables.
  9. The intended commit/release is the one behind the public domain.
  10. Unique public behaviour or content proves the corrected configuration is live.

Authoritative references: Coolify’s environment variables documentation describes independent build-time and runtime flags. Docker’s build variables guide explains ARG, ENV, scope and why neither should carry build secrets. Next.js’s environment-variable guide documents that NEXT_PUBLIC_ values are inlined during the build and frozen afterward.