This document describes how client-side configuration values (LaunchDarkly, DataDog RUM, etc.) are injected at runtime instead of build time, allowing a single Docker image to be deployed across all environments.
Runtime configuration values are:
- Injected at container startup via environment variables
- Transferred from server to browser using Angular's TransferState
- Not baked into the Docker image during build
- Environment-specific without requiring separate builds
Examples: LaunchDarkly client ID, DataDog RUM client token
Container Start (ENV vars: LD_CLIENT_ID, DD_RUM_CLIENT_ID, etc.)
β
βΌ
Express Server reads process.env
β
βΌ
SSR Request β server.ts builds RuntimeConfig object
β
βΌ
Angular SSR renders β REQUEST_CONTEXT contains runtimeConfig
β
βΌ
provideRuntimeConfig() stores config in TransferState
β
βΌ
HTML sent to browser with TransferState serialized
β
βΌ
Browser hydrates β TransferState contains config
β
βΌ
provideFeatureFlags() reads config from TransferState
β
βΌ
LaunchDarkly/DataDog initialize with runtime values
The runtime configuration system uses two providers:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β app.config.ts β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β providers: [ β
β ... β
β provideRuntimeConfig(), // Must be first β
β provideFeatureFlags(), // Uses runtime config β
β // provideDataDog(), // Future: will use config β
β ] β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
runtime-config.provider.ts: Sets up TransferState with config from serverfeature-flag.provider.ts: Reads config and initializes LaunchDarkly- Future providers: Can use the same pattern for DataDog, analytics, etc.
File: packages/shared/src/interfaces/runtime-config.interface.ts
export interface RuntimeConfig {
launchDarklyClientId: string;
dataDogRumClientId: string;
dataDogRumApplicationId: string;
}File: apps/lfx-one/src/server/server.ts
The Express server reads environment variables and passes them to Angular:
// Build runtime config from environment variables
const runtimeConfig: RuntimeConfig = {
launchDarklyClientId: process.env['LD_CLIENT_ID'] || '',
dataDogRumClientId: process.env['DD_RUM_CLIENT_ID'] || '',
dataDogRumApplicationId: process.env['DD_RUM_APPLICATION_ID'] || '',
};
angularApp.handle(req, {
auth,
runtimeConfig, // Passed via REQUEST_CONTEXT
providers: [
{ provide: APP_BASE_HREF, useValue: process.env['PCC_BASE_URL'] },
{ provide: REQUEST, useValue: req },
],
});File: apps/lfx-one/src/app/shared/providers/runtime-config.provider.ts
async function initializeRuntimeConfig(): Promise<void> {
const transferState = inject(TransferState);
const reqContext = inject(REQUEST_CONTEXT, { optional: true });
// Server-side: Store config to TransferState for browser hydration
if (reqContext?.runtimeConfig) {
transferState.set(RUNTIME_CONFIG_KEY, reqContext.runtimeConfig);
}
}
export const provideRuntimeConfig = (): EnvironmentProviders => provideAppInitializer(initializeRuntimeConfig);File: apps/lfx-one/src/app/shared/providers/feature-flag.provider.ts
async function initializeOpenFeature(): Promise<void> {
// Skip on server - LaunchDarkly is browser-only
if (typeof window === 'undefined') {
return;
}
const transferState = inject(TransferState);
const runtimeConfig = getRuntimeConfig(transferState);
const clientId = runtimeConfig.launchDarklyClientId;
if (!clientId) {
console.warn('LaunchDarkly client ID not configured');
return;
}
// Initialize LaunchDarkly with runtime client ID
const provider = new LaunchDarklyClientProvider(clientId, { ... });
await OpenFeature.setProviderAndWait(provider);
}| Variable | Description | Example |
|---|---|---|
LD_CLIENT_ID |
LaunchDarkly client-side ID | 691b727361cbf309e9d74468 |
DD_RUM_CLIENT_ID |
DataDog RUM client token (future) | pub123456789 |
DD_RUM_APPLICATION_ID |
DataDog RUM application ID (future) | app-uuid-here |
Add to your .env file (already gitignored):
# Runtime Client IDs for local development
LD_CLIENT_ID=your-launchdarkly-dev-client-id
DD_RUM_CLIENT_ID=your-datadog-rum-client-token
DD_RUM_APPLICATION_ID=your-datadog-rum-app-idThe server reads these via dotenv in development mode:
if (process.env['NODE_ENV'] !== 'production') {
dotenv.config();
}Pass environment variables at container runtime:
docker run \
-e LD_CLIENT_ID=prod-client-id \
-e DD_RUM_CLIENT_ID=prod-rum-token \
-e DD_RUM_APPLICATION_ID=prod-rum-app-id \
ghcr.io/linuxfoundation/lfx-v2-ui:latestConfigure in your Kubernetes manifests or Helm values:
env:
- name: LD_CLIENT_ID
valueFrom:
secretKeyRef:
name: lfx-one-secrets
key: launchdarkly-client-id
- name: DD_RUM_CLIENT_ID
valueFrom:
secretKeyRef:
name: lfx-one-secrets
key: datadog-rum-client-idBuild once, deploy anywhere:
# Same image for all environments
docker build -t lfx-one .
# Development
docker run -e LD_CLIENT_ID=dev-id lfx-one
# Staging
docker run -e LD_CLIENT_ID=staging-id lfx-one
# Production
docker run -e LD_CLIENT_ID=prod-id lfx-one- Client IDs never committed to repository
- No secrets baked into Docker layers
- Configuration managed via environment
Add new runtime configurations easily:
- Add property to
RuntimeConfiginterface - Read from
process.envinserver.ts - Access via
getRuntimeConfig(transferState)in your provider
// packages/shared/src/interfaces/runtime-config.interface.ts
export interface RuntimeConfig {
launchDarklyClientId: string;
dataDogRumClientId: string;
dataDogRumApplicationId: string;
newServiceClientId: string; // Add new property
}// apps/lfx-one/src/server/server.ts
const runtimeConfig: RuntimeConfig = {
launchDarklyClientId: process.env['LD_CLIENT_ID'] || '',
dataDogRumClientId: process.env['DD_RUM_CLIENT_ID'] || '',
dataDogRumApplicationId: process.env['DD_RUM_APPLICATION_ID'] || '',
newServiceClientId: process.env['NEW_SERVICE_CLIENT_ID'] || '', // Add
};// apps/lfx-one/src/app/shared/providers/runtime-config.provider.ts
export const DEFAULT_RUNTIME_CONFIG: RuntimeConfig = {
launchDarklyClientId: '',
dataDogRumClientId: '',
dataDogRumApplicationId: '',
newServiceClientId: '', // Add
};// apps/lfx-one/src/app/shared/providers/new-service.provider.ts
async function initializeNewService(): Promise<void> {
if (typeof window === 'undefined') return;
const transferState = inject(TransferState);
const config = getRuntimeConfig(transferState);
if (!config.newServiceClientId) return;
// Initialize your service
}
export const provideNewService = (): EnvironmentProviders => provideAppInitializer(initializeNewService);// apps/lfx-one/src/app/app.config.ts
providers: [
provideRuntimeConfig(),
provideFeatureFlags(),
provideNewService(), // Add after provideRuntimeConfig
];# apps/lfx-one/.env.example
NEW_SERVICE_CLIENT_ID=your-new-service-client-idSymptom: getRuntimeConfig() returns default values
Possible Causes:
provideRuntimeConfig()not included inapp.config.tsprovideRuntimeConfig()not before other providers that use it- Environment variable not set on server
Solution:
- Ensure provider order:
provideRuntimeConfig()beforeprovideFeatureFlags() - Verify environment variable is set:
echo $LD_CLIENT_ID - Check server logs for config being passed
Symptom: Config works on server but not browser
Possible Causes:
- SSR not enabled
- TransferState not serialized in HTML
- Browser hydration disabled
Solution:
- Verify
provideClientHydration()is inapp.config.ts - Check HTML source for
<script id="serverApp-state">tag - Ensure
withIncrementalHydration()is configured
Symptom: Warning "LaunchDarkly client ID not configured"
Solution:
- Local development: Add to
.envfile - Docker: Pass with
-e LD_CLIENT_ID=xxx - Kubernetes: Configure in deployment manifest
If migrating from the previous build-time approach:
- Remove
LAUNCHDARKLY_CLIENT_IDfromangular.jsondefine blocks - Remove
launchDarklyClientIdfrom environment files - Remove
declare const LAUNCHDARKLY_CLIENT_IDfrom environment files - Update Dockerfile to remove
--mount=type=secretfor client IDs - Update GitHub workflows to remove AWS Secrets Manager steps for client IDs
- Add environment variables to deployment configuration
- Feature Flags - LaunchDarkly integration details
- Deployment Guide - Deployment processes and environments
- SSR Server - Server-side rendering architecture