7.9 KiB
Runtime Configuration Pattern
Overview
MotoVaultPro uses a K8s-aligned runtime configuration pattern where sensitive values (like API keys) are loaded at container startup from mounted secrets, not at build time.
This approach:
- Mirrors Kubernetes deployment patterns
- Allows configuration changes without rebuilding images
- Keeps secrets out of build artifacts and environment variables
- Enables easy secret rotation in production
Architecture
How It Works
- Build Time: Container is built WITHOUT secrets (no API keys in image)
- Container Startup:
/app/load-config.shreads/run/secrets/google-maps-api-key- Generates
/usr/share/nginx/html/config.jswith runtime values - Starts nginx
- App Load Time:
index.htmlloads<script src="/config.js"></script>window.CONFIGis available before React initializes- React app reads configuration via
getConfig()hook
File Structure
frontend/
├── scripts/
│ └── load-config.sh # Reads secrets, generates config.js
├── src/
│ └── core/config/
│ └── config.types.ts # TypeScript types and helpers
├── index.html # Loads config.js before app
└── Dockerfile # Runs load-config.sh before nginx
Usage in Components
Reading Configuration
import { getConfig, getGoogleMapsApiKey } from '@/core/config/config.types';
export function MyComponent() {
// Get full config object with error handling
const config = getConfig();
// Or get specific value with fallback
const apiKey = getGoogleMapsApiKey();
return <MapComponent apiKey={apiKey} />;
}
Conditional Features
import { isConfigLoaded } from '@/core/config/config.types';
export function FeatureGate() {
if (!isConfigLoaded()) {
return <LoadingFallback />;
}
return <AdvancedFeature />;
}
Adding New Runtime Configuration Values
1. Update load-config.sh
# In frontend/scripts/load-config.sh
if [ -f "$SECRETS_DIR/new-api-key" ]; then
NEW_API_KEY=$(cat "$SECRETS_DIR/new-api-key")
echo "[Config] Loaded New API Key"
else
NEW_API_KEY=""
fi
# In the config.js generation:
cat > "$CONFIG_FILE" <<EOF
window.CONFIG = {
googleMapsApiKey: '$GOOGLE_MAPS_API_KEY',
newApiKey: '$NEW_API_KEY'
};
EOF
2. Update config.types.ts
export interface AppConfig {
googleMapsApiKey: string;
newApiKey: string; // Add new field
}
export function getNewApiKey(): string {
try {
const config = getConfig();
return config.newApiKey || '';
} catch {
console.warn('New API Key not available.');
return '';
}
}
3. Update docker-compose.yml
mvp-frontend:
volumes:
- ./secrets/app/google-maps-api-key.txt:/run/secrets/google-maps-api-key:ro
- ./secrets/app/new-api-key.txt:/run/secrets/new-api-key:ro # Add new secret
4. Create Secret File
mkdir -p ./secrets/app
echo "your-api-key-value" > ./secrets/app/new-api-key.txt
Docker-Compose Configuration
The frontend service mounts secrets from the host filesystem:
mvp-frontend:
environment:
SECRETS_DIR: /run/secrets
volumes:
- ./secrets/app/google-maps-api-key.txt:/run/secrets/google-maps-api-key:ro
:roflag makes secrets read-only- Secrets are available at container startup
- Changes require container restart (no image rebuild)
Kubernetes Deployment
When deploying to Kubernetes, update the deployment manifest:
apiVersion: apps/v1
kind: Deployment
metadata:
name: motovaultpro-frontend
spec:
template:
spec:
containers:
- name: frontend
image: motovaultpro-frontend:latest
env:
- name: SECRETS_DIR
value: /run/secrets
volumeMounts:
- name: google-maps-key
mountPath: /run/secrets/google-maps-api-key
subPath: google-maps-api-key
readOnly: true
volumes:
- name: google-maps-key
secret:
secretName: google-maps-api-key
items:
- key: api-key
path: google-maps-api-key
Development Setup
Local Development
For npm run dev (Vite dev server):
# Copy secrets to secrets directory
mkdir -p ./secrets/app
echo "your-test-api-key" > ./secrets/app/google-maps-api-key.txt
# Set environment variable
export SECRETS_DIR=./secrets/app
# Start Vite dev server
npm run dev
To access config in your app:
// In development, config.js may not exist
// Use graceful fallback:
export function useConfig() {
const [config, setConfig] = useState<AppConfig | null>(null);
useEffect(() => {
if (window.CONFIG) {
setConfig(window.CONFIG);
} else {
// Fallback for dev without config.js
setConfig({
googleMapsApiKey: process.env.REACT_APP_GOOGLE_MAPS_KEY || '',
});
}
}, []);
return config;
}
Container-Based Testing
Recommended approach (per CLAUDE.md):
# Ensure secrets exist
mkdir -p ./secrets/app
echo "your-api-key" > ./secrets/app/google-maps-api-key.txt
# Rebuild and start containers
make rebuild
make logs
# Verify config.js was generated
docker compose exec mvp-frontend cat /usr/share/nginx/html/config.js
# Should output:
# window.CONFIG = {
# googleMapsApiKey: 'your-api-key'
# };
Debugging
Verify Secrets are Mounted
docker compose exec mvp-frontend cat /run/secrets/google-maps-api-key
Check Generated config.js
docker compose exec mvp-frontend cat /usr/share/nginx/html/config.js
View Container Logs
docker compose logs mvp-frontend
Look for lines like:
[Config] Loaded Google Maps API key from /run/secrets/google-maps-api-key
[Config] Generated /usr/share/nginx/html/config.js
Browser Console
Check browser console for any config loading errors:
console.log(window.CONFIG);
// Should show: { googleMapsApiKey: "your-key" }
Best Practices
-
Never Log Secrets: The load-config.sh script only logs that a key was loaded, never the actual value
-
Always Validate: Use
getConfig()which throws errors if config is missing:try { const config = getConfig(); } catch (error) { // Handle missing config gracefully } -
Use Fallbacks: For optional features, use graceful fallbacks:
const apiKey = getGoogleMapsApiKey(); // Returns empty string if not available -
Documentation: Update this file when adding new configuration values
-
Testing: Test with and without secrets in containers
Security Considerations
- Secrets are mounted as files, not environment variables
- Files are read-only (
:roflag) - config.js is generated at startup, not included in image
- Browser console can see config values (like any JavaScript)
- For highly sensitive values, consider additional encryption
Troubleshooting
config.js not generated
Symptom: Browser shows window.CONFIG is undefined
Solutions:
- Check secret file exists:
docker compose exec mvp-frontend cat /run/secrets/google-maps-api-key - Check load-config.sh runs:
docker compose logs mvp-frontend - Verify permissions:
docker compose exec mvp-frontend ls -la /run/secrets/
Container fails to start
Symptom: Container crashes during startup
Solution:
- Check logs:
docker compose logs mvp-frontend - Verify script has execute permissions (in Dockerfile)
- Test script locally:
sh frontend/scripts/load-config.sh
Secret changes not reflected
Symptom: Container still uses old secret after file change
Solution:
# Restart container to reload secrets
docker compose restart mvp-frontend
# Or fully rebuild
make rebuild