Which configuration type is right, and where the security risks lie
runtimeConfig is built for values that can still change after the build, for example because the same Docker container runs in several environments with different API endpoints. The difference between public and private values directly determines whether a value ends up accidentally exposed as a secret in the client.
Table of Contents
- 1. Build Time and Runtime: Two Different Points in Time
- 2. The Concept Behind runtimeConfig
- 3. Public vs. Private Runtime Config Values
- 4. Practical Case: Docker Containers Across Different Environments
- 5. Naming Convention for Environment Variables
- 6. When Build-Time Variables Are Still Necessary
- 7. Security Implications of Exposed Secrets
- 8. Best Practices for Working with runtimeConfig
- 9. Conclusion: Deliberately Separate Configuration by Timing
- 10. Summary
- 11. FAQ
1. Build Time and Runtime: Two Different Points in Time
In a classic frontend application, environment variables are usually baked directly into the JavaScript code during the build process. A value like process.env.API_URL gets replaced by the build tool with the actual string, and that string is then permanently fixed inside the shipped bundle, regardless of which environment the bundle actually ends up running in later.
This works fine as long as a separate build gets produced for every environment, such as staging and production. But as soon as the same, already-built Docker container is supposed to run in several environments with different configuration values, this approach falls short, since the value was already baked immutably into the code at build time and can no longer change at runtime.
2. The Concept Behind runtimeConfig
Nuxt solves this problem with runtimeConfig, a configuration structure whose values aren't baked in at build time but are instead read from the actual environment variables of the running process when the server starts. The same built container can therefore be launched in different environments with different values, without needing a new build.
runtimeConfig gets defined centrally in nuxt.config.ts, where the values stored there simultaneously serve as defaults and as a type definition. At runtime, Nuxt automatically overrides every value as soon as an appropriately named environment variable is set, which cleanly separates configuration from code.
3. Public vs. Private Runtime Config Values
Within runtimeConfig, Nuxt strictly distinguishes between top-level values, which are available only on the server, and values nested under the public key, which are accessible both on the server and in the client. This separation isn't just a convention, it's technically enforced by Nuxt, since only the public portion actually gets embedded in the JavaScript bundle shipped to the client.
In the example below, the database access key is available exclusively on the server, while the public API base URL is deliberately made available to the client too, via public, for instance for client-side $fetch calls made directly from the browser.
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
// server-side only
databaseApiKey: '',
public: {
// available on both server and client
apiBase: 'https://api.example.com',
},
},
});
// server/api/orders.get.ts
export default defineEventHandler((event) => {
const config = useRuntimeConfig(event);
// databaseApiKey is available here, but NOT in the client
return $fetch(`${config.public.apiBase}/orders`, {
headers: { Authorization: `Bearer ${config.databaseApiKey}` },
});
});
4. Practical Case: Docker Containers Across Different Environments
In a typical Docker-based deployment, an image gets built exactly once and then rolled out unchanged across several environments such as staging, pre-production, and production. If API endpoints or feature flags were treated as build-time variables, a separate image would have to be built for every environment, which breaks the principle of building once and deploying an identical artifact everywhere.
With runtimeConfig, the image stays identical across all environments, and only the environment variables set when the container starts differ between environments. This matches the established principle of strictly separating configuration from code, and it also reduces the risk that different code gets tested and shipped across environments.
5. Naming Convention for Environment Variables
Nuxt automatically derives the name of the corresponding environment variable from the nested key path in runtimeConfig, using uppercase letters and underscores. A value at runtimeConfig.public.apiBase, for example, can be set through the variable NUXT_PUBLIC_API_BASE, while a private value like runtimeConfig.databaseApiKey corresponds to NUXT_DATABASE_API_KEY.
This automatic derivation significantly reduces boilerplate, since no manual mapping between environment variables and the configuration structure needs to be maintained. In more complex projects with many nested configuration values, it's still worth documenting the full list of resulting variable names, to avoid typos when setting the variables in the deployment pipeline.
6. When Build-Time Variables Are Still Necessary
Not every piece of configuration is a good fit for runtimeConfig. Values that influence the build output itself, such as which modules get enabled, which CSS file gets loaded, or whether certain code should be stripped from the bundle entirely, necessarily have to be known at build time, since they change the result of the build process itself rather than becoming relevant only after the server starts.
For such cases, classic build-time environment variables, read directly inside nuxt.config.ts via process.env, remain the right approach. The deciding factor is whether a value controls the behavior of the already-built application at runtime, in which case runtimeConfig applies, or whether it influences the build process itself, in which case a classic build-time variable is the right tool.
7. Security Implications of Exposed Secrets
The most common and most dangerous mistake when working with runtimeConfig is accidentally placing a sensitive value, such as an API key, a database password, or a private certificate, inside the public key. Since everything under public ends up unencrypted in the JavaScript bundle shipped to the browser, any visitor to the site can simply inspect that value through the browser's developer tools.
What makes this especially tricky is that such a mistake usually goes unnoticed during local development, since everything appears to work as expected, while the secret is already visible to every site visitor in the background. Regularly checking the actually shipped bundle, for example through the network panel of the browser's developer tools, along with a deliberate code review rule for every change to runtimeConfig, helps minimize this risk.
8. Best Practices for Working with runtimeConfig
As a basic rule: every value starts out as a private, server-side value at the top level of runtimeConfig, and only values that are demonstrably needed in the client get moved deliberately and explicitly into the public key. This defensive default stance prevents a value from getting marked public out of convenience when it never actually needed to be.
It's also worth automatically checking, as part of the deployment pipeline, whether all expected environment variables are actually set when the container starts, to prevent the application from running in production with empty or missing values from runtimeConfig. A missing but expected value should ideally cause the deployment process to fail early and visibly.
9. Conclusion: Deliberately Separate Configuration by Timing
The choice between runtimeConfig and classic build-time environment variables should always be based on whether a value controls the behavior of the finished build at runtime or influences the build process itself. For Docker-based deployments with an image built once and rolled out across several environments, runtimeConfig is generally the more fitting and flexible choice.
At the same time, runtimeConfig demands discipline in separating public from private values, since a single misplaced value is enough to permanently expose a secret to every site visitor. Anyone who consistently maintains this separation gains the flexibility of runtime configuration without taking on the associated security risks.
| Aspect | Build-Time Env Variable | runtimeConfig |
|---|---|---|
| Resolved at | During the build process | When the server starts, at runtime |
| Change without rebuild | Not possible | Possible, via newly set env variables |
| Typical use | Enabled modules, CSS selection | API endpoints, feature flags, secrets |
| Docker fit | One image per environment needed | One image for all environments |
| Security risk | Value lives in source code/build | Risky only with wrong public placement |
Mironsoft
Vue architecture, Composition API, and Nuxt performance
Vue applications that don't get more complicated with every feature?
We review existing Vue and Nuxt projects for unstructured composables, unnecessary reactivity, and bloated bundles, then build an architecture that absorbs new features without making the codebase harder to follow.
Architecture Review
Checking composables, state management, and component structure for maintainability.
Performance Audit
Systematically optimizing reactivity overhead, bundle size, and Nuxt rendering strategy.
Nuxt Integration
Building robust, type-safe SSR/SSG setup and API integration.
10. Summary
Runtime Config vs. Build-Time Env: The Essentials at a Glance
runtimeConfig
Values that can still change after the build, read from the environment
public key
The only values that actually reach the client bundle
Docker advantage
One image for all environments, configuration via env variables
Biggest risk
A secret accidentally placed in public instead of staying private