12-Factor Runtime Configuration
In traditional Vite SPA deployments, environment variables defined in .env are baked into the bundled JavaScript during npm run build. This makes it impossible to build a single Docker image once and deploy it across multiple staging and production environments without rebuilding.
BonardaHR solves this by implementing dynamic runtime configuration using env.template.js, window.__APP_CONFIG__, and docker-entrypoint.sh.
How It Works​
[ Docker Container Startup ]
│
â–¼
[ docker-entrypoint.sh ] ──(envsubst)──► Writes /usr/share/nginx/html/env.js
│
â–¼
[ Browser index.html ] ─── Loads <script src="/env.js">
│
â–¼
[ Window Global ] ──────── Sets window.__APP_CONFIG__ = { ... }
│
â–¼
[ runtimeConfig.ts ] ───── Exports typed appConfig object with Vite fallback
File Breakdown​
1. env.template.js​
Located in the frontend root, this file defines the template evaluated by envsubst:
window.__APP_CONFIG__ = {
VITE_API_BASE_URL: '$VITE_API_BASE_URL',
VITE_AZURE_CLIENT_ID: '$VITE_AZURE_CLIENT_ID',
VITE_AZURE_TENANT_ID: '$VITE_AZURE_TENANT_ID',
VITE_AZURE_REDIRECT_URI: '$VITE_AZURE_REDIRECT_URI',
VITE_APP_NAME: '$VITE_APP_NAME',
VITE_APP_VERSION: '$VITE_APP_VERSION',
VITE_DEV_LOGIN_ENABLED: '$VITE_DEV_LOGIN_ENABLED',
};
2. docker-entrypoint.sh​
When the Docker container starts, docker-entrypoint.sh performs variable substitution before starting Nginx:
#!/bin/sh
set -e
envsubst '$VITE_API_BASE_URL $VITE_AZURE_CLIENT_ID $VITE_AZURE_TENANT_ID $VITE_AZURE_REDIRECT_URI $VITE_APP_NAME $VITE_APP_VERSION $VITE_DEV_LOGIN_ENABLED' \
< /usr/share/nginx/html/env.template.js \
> /usr/share/nginx/html/env.js
exec nginx -g 'daemon off;'
3. src/config/runtimeConfig.ts​
The application accesses configuration values via the centralized appConfig export. If window.__APP_CONFIG__ is not present (such as during local npm run dev), it falls back gracefully to import.meta.env:
type RuntimeConfig = {
VITE_API_BASE_URL?: string;
VITE_AZURE_CLIENT_ID?: string;
VITE_AZURE_TENANT_ID?: string;
VITE_AZURE_REDIRECT_URI?: string;
VITE_APP_NAME?: string;
VITE_APP_VERSION?: string;
VITE_DEV_LOGIN_ENABLED?: string;
};
declare global {
interface Window {
__APP_CONFIG__?: RuntimeConfig;
}
}
const runtimeConfig: RuntimeConfig = window.__APP_CONFIG__ ?? {};
function getConfigValue<K extends keyof RuntimeConfig>(key: K, fallback: string): string {
return runtimeConfig[key] ?? fallback;
}
export const appConfig = {
apiBaseUrl: getConfigValue(
'VITE_API_BASE_URL',
import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:8081/api/v1'
),
azureClientId: getConfigValue('VITE_AZURE_CLIENT_ID', import.meta.env.VITE_AZURE_CLIENT_ID ?? ''),
azureTenantId: getConfigValue('VITE_AZURE_TENANT_ID', import.meta.env.VITE_AZURE_TENANT_ID ?? ''),
azureRedirectUri: getConfigValue('VITE_AZURE_REDIRECT_URI', import.meta.env.VITE_AZURE_REDIRECT_URI ?? ''),
appName: getConfigValue('VITE_APP_NAME', import.meta.env.VITE_APP_NAME ?? 'Bonarda HR'),
appVersion: getConfigValue('VITE_APP_VERSION', import.meta.env.VITE_APP_VERSION ?? '0.0.1'),
devLoginEnabled:
getConfigValue('VITE_DEV_LOGIN_ENABLED', import.meta.env.VITE_DEV_LOGIN_ENABLED ?? 'false') === 'true',
};
Cache Invalidation for env.js​
To guarantee that browsers never cache outdated runtime configuration after container updates, nginx.conf sets strict no-cache headers for /env.js:
location = /env.js {
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
try_files $uri =404;
}