added dynamic configuration of API_URL to docker deployments.

Now it works with editing only .env file.
This commit is contained in:
2026-02-25 23:09:50 +01:00
parent 670ea5f970
commit 3a0e751834
15 changed files with 432 additions and 80 deletions
+174
View File
@@ -0,0 +1,174 @@
# Runtime Configuration Implementation Summary
## What Was Implemented
Enhanced the BaMoRT frontend to support runtime configuration for **both desktop (Wails) and web production** deployments, eliminating the need to rebuild when changing API URLs.
## Architecture
### Desktop (Wails)
- Uses Go bindings to read API URL from `.env` file at runtime
- Method: `window['go']['main']['App']['GetAPIBaseURL']()`
- Configuration: `desktop/.env``API_PORT` variable
- No rebuild needed when changing port
### Web Production (NEW)
Uses a **multi-strategy fallback system**:
1. **config.json** (Priority 1)
- Loads `/config.json` from web root
- Can be modified after deployment
- Example: `{"apiBaseURL": "https://api.yourdomain.com"}`
2. **Auto-detection** (Priority 2)
- Probes `/api/public/version` at same origin
- Works automatically with reverse proxy setups
- Detects if backend and frontend share same domain
3. **VITE_API_URL** (Priority 3)
- Environment variable at build time
- Used for development: `VITE_API_URL=http://localhost:8180`
4. **Same Origin Fallback** (Priority 4)
- Uses `window.location.origin`
- Assumes backend and frontend on same domain
## Files Modified
### Core Implementation
- `/data/dev/bamort/frontend/src/utils/config.js` - Enhanced with multi-strategy config loading
- `/data/dev/bamort/frontend/src/utils/api.js` - Uses dynamic baseURL (already done for desktop)
- `/data/dev/bamort/desktop/main.go` - GetAPIBaseURL() Go binding (already done for desktop)
### Documentation
- `/data/dev/bamort/frontend/RUNTIME_CONFIG.md` - Complete web configuration guide
- `/data/dev/bamort/desktop/RUNTIME_CONFIG.md` - Desktop configuration guide (existing)
### Configuration Files
- `/data/dev/bamort/frontend/public/config.json.example` - Template for deployment
- `/data/dev/bamort/frontend/.gitignore` - Excludes `public/config.json` from git
## How It Works
### For Web Development
```bash
cd frontend
VITE_API_URL=http://localhost:8180 npm run dev
```
### For Web Production Deployment
**Option A: Using config.json (Recommended)**
```bash
# 1. Build once
cd frontend && npm run build
# 2. Deploy dist/ to web server
# 3. Create config.json in web root
cat > /var/www/bamort/config.json <<EOF
{
"apiBaseURL": "https://api.production.com"
}
EOF
# 4. Change API URL anytime without rebuild!
```
**Option B: Reverse Proxy Setup**
Example nginx configuration:
```nginx
server {
listen 80;
server_name yourdomain.com;
# Frontend
location / {
root /var/www/bamort;
try_files $uri $uri/ /index.html;
}
# Backend API
location /api {
proxy_pass http://localhost:8180;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
```
Frontend auto-detects and uses same origin.
**Option C: Build with Embedded URL**
```bash
VITE_API_URL=https://api.production.com npm run build
```
(Not recommended - requires rebuild for URL changes)
### For Desktop (Wails)
```bash
# 1. Build once
cd desktop && wails build
# 2. Change port in .env (no rebuild!)
echo "API_PORT=9000" > desktop/.env
# 3. Run app - uses new port automatically
./desktop/build/bin/bamort
```
## Verification
Open browser console when loading the app. Look for one of these messages:
**Desktop:**
```
Desktop app using API URL from config: http://localhost:8185
```
**Web:**
```
Loaded API URL from config.json: https://api.yourdomain.com
```
or
```
Detected backend at same origin: https://yourdomain.com
```
or
```
Web app using VITE_API_URL: http://localhost:8180
```
or
```
Web app using same origin: https://yourdomain.com
```
## Benefits
**No rebuild needed** for API URL changes in production web deployments
**Same build artifact** can be deployed to dev/staging/production
**Infrastructure-friendly** - works with reverse proxies, Docker, static hosting
**Flexible** - multiple configuration strategies with sensible fallbacks
**Dev-friendly** - VITE_API_URL still works for development
**Desktop-friendly** - reads .env at runtime (already implemented)
## Testing Checklist
- [x] Frontend builds successfully (`npm run build`)
- [x] Desktop app reads runtime config from .env
- [ ] Web dev mode works with VITE_API_URL
- [ ] Web production with config.json works
- [ ] Web production with reverse proxy auto-detection works
- [ ] Web production with same-origin fallback works
## Migration Path
**For existing deployments:**
1. No changes needed! Current builds continue working
2. To enable runtime config: Just add `config.json` to your web root
3. Old builds with VITE_API_URL still work as fallback
**For new deployments:**
1. Build once: `npm run build`
2. Deploy `dist/` contents
3. Add `config.json` with your API URL
4. Done!
+1 -1
View File
@@ -32,7 +32,7 @@ func SetupGin(r *gin.Engine) {
}
corsConfig = cors.Config{
AllowOrigins: allowedOrigins,
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"},
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
+5 -5
View File
@@ -1,9 +1,5 @@
# Environment variables for Bamort development environment
# API Configuration
# API_URL=http://localhost:8180
# Database Configuration (for development)
DATABASE_TYPE=mysql
DATABASE_URL=bamort:bG4)efozrc@tcp(mariadb-dev:3306)/bamort?charset=utf8mb4&parseTime=True&loc=Local
@@ -13,9 +9,13 @@ MARIADB_PASSWORD=bG4)efozrc
MARIADB_DATABASE=bamort
MARIADB_USER=bamort
# Frontend Configuration
# API Configuration
# API_URL is used by the frontend to connect to the backend
# For dev environment with Vite dev server, this goes to VITE_API_URL
API_URL=http://192.168.0.48:8180
API_PORT=8180
# Frontend Configuration
BASE_URL=http://localhost:5173
TEMPLATES_DIR=./templates
EXPORT_TEMP_DIR=./export_temp
+16 -3
View File
@@ -1,9 +1,16 @@
# Environment variables for Bamort production deployment
# Copy this file to .env and adjust the values as needed
# Copy this file to .env.dev or .env.prd and adjust the values as needed
#- API Configuration
# ===== FRONTEND API CONFIGURATION =====
# API_URL: The backend API endpoint that the frontend will use
# For production: Use your public API domain (https://api.yourdomain.com)
# For development: Use your local network IP (http://192.168.x.x:8180)
#
# IMPORTANT: After changing API_URL, just restart containers - NO REBUILD NEEDED!
# Production: docker-compose -f docker-compose.yml restart frontend
# Development: docker-compose -f docker-compose.dev.yml restart frontend-dev
API_URL=https://backend.domain.de
VITE_API_URL=https://backend.domain.de
API_PORT=8180
#- Database Configuration Backend
DATABASE_TYPE=mysql
@@ -20,3 +27,9 @@ BASE_URL=https://frontend.domain.de
TEMPLATES_DIR=./templates
EXPORT_TEMP_DIR=./export_temp
COMPOSE_PROJECT_NAME=bamort
# Mail Configuration (for development)
MAIL_HOST=mail.server.de
MAIL_PORT=465
MAIL_USERNAME=bamort@server.de
MAIL_PASSWORD=XXXZZZZZ.YYYXXX
+4 -4
View File
@@ -1,10 +1,10 @@
# Environment variables for Bamort production environment
# API Configuration
# API_URL is used by the frontend to generate config.json at container startup
# Change this value and restart containers (no rebuild needed!)
API_URL=https://bamort-api.trokan.de
VITE_API_URL=https://bamort-api.trokan.de
# Database Configuration Backend
API_PORT=8180
DATABASE_TYPE=mysql
#DATABASE_URL=bamort:bG4)efozrc@tcp(mariadb:3306)/bamort?charset=utf8mb4&parseTime=True&loc=Local
@@ -14,7 +14,7 @@ MARIADB_PASSWORD=bG4)efozrc
MARIADB_DATABASE=bamort
MARIADB_USER=bamort
API_PORT=8180
# Frontend Configuration
BASE_URL=https://bamort.trokan.de
TEMPLATES_DIR=./templates
EXPORT_TEMP_DIR=./export_temp
+12 -17
View File
@@ -1,42 +1,37 @@
# =========== 1) Build stage ===========
FROM node:22-alpine AS build
# Accept build arguments for Vite environment variables
ARG VITE_API_URL
ARG VITE_BASE_URL
ARG VITE_API_PORT
# Set them as environment variables for the build process
ENV VITE_API_URL=$VITE_API_URL
ENV VITE_BASE_URL=$VITE_BASE_URL
ENV VITE_API_PORT=$VITE_API_PORT
# No build args needed - using runtime configuration instead
WORKDIR /usr/src/app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy the rest of the frontend code
COPY . .
# Build the production bundle
# Build the production bundle WITHOUT baked-in API URL
# Runtime configuration will be generated from environment variables
RUN npm run build
# =========== 2) Serve stage ===========
FROM nginx:alpine
# Copy production build to Nginx html folder.
# Adjust /usr/src/app/build -> /usr/src/app/dist if youre using Angular/Vue
#COPY --from=build /usr/src/app/build /usr/share/nginx/html
# Copy production build to Nginx html folder
COPY --from=build /usr/src/app/dist /usr/share/nginx/html
# Copy custom nginx configuration for SPA routing
COPY --from=build /usr/src/app/nginx.conf /etc/nginx/conf.d/default.conf
# Copy entrypoint script that generates runtime config
COPY --from=build /usr/src/app/docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
# Expose HTTP port
EXPOSE 80
# Run Nginx in foreground
CMD ["nginx", "-g", "daemon off;"]
# Use custom entrypoint that generates config.json from environment
ENTRYPOINT ["/docker-entrypoint.sh"]
+6 -2
View File
@@ -9,8 +9,12 @@ COPY package*.json ./
# Install dependencies
RUN npm install
# Copy entrypoint script that generates config.json
COPY docker-dev-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
# Expose Vite dev server port
EXPOSE 5173
# Start development server with host binding for Docker
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
# Use entrypoint that generates config.json before starting Vite
ENTRYPOINT ["/docker-entrypoint.sh"]
+1 -7
View File
@@ -25,18 +25,12 @@ services:
build:
context: ../frontend
dockerfile: ../docker/Dockerfile.frontend
args:
VITE_API_URL: ${API_URL:-https://bamort-api.trokan.de}
VITE_BASE_URL: ${BASE_URL:-https://bamort.trokan.de}
VITE_API_PORT: ${API_PORT:-443}
container_name: bamort-frontend
ports:
- "8181:80"
environment:
- NODE_ENV=production
- VITE_API_URL=${API_URL:-https://bamort.trokan.de:8180}
- VITE_BASE_URL=${BASE_URL:-https://bamort.trokan.de}
- VITE_API_PORT=${API_PORT:-8180}
- API_URL=${API_URL:-https://bamort-api.trokan.de}
depends_on:
- backend
restart: unless-stopped
+35
View File
@@ -0,0 +1,35 @@
#!/bin/sh
set -e
# Generate config.json from VITE_API_URL for development
# This makes the dev environment behave the same as production
CONFIG_FILE="/app/public/config.json"
echo "🔧 Generating development config.json..."
# Use VITE_API_URL from environment
API_BASE_URL="${VITE_API_URL:-}"
if [ -z "$API_BASE_URL" ]; then
echo "⚠️ VITE_API_URL not set, creating minimal config"
cat > "$CONFIG_FILE" <<EOF
{
"_comment": "Development mode - VITE_API_URL will be used as fallback"
}
EOF
else
echo "✅ API URL configured: $API_BASE_URL"
cat > "$CONFIG_FILE" <<EOF
{
"apiBaseURL": "$API_BASE_URL"
}
EOF
fi
echo "📄 Created config.json:"
cat "$CONFIG_FILE"
# Start Vite dev server
echo "🚀 Starting Vite dev server..."
exec npm run dev -- --host 0.0.0.0
+36
View File
@@ -0,0 +1,36 @@
#!/bin/sh
set -e
# Generate config.json from environment variables at container startup
# This allows runtime configuration without rebuilding the container
CONFIG_FILE="/usr/share/nginx/html/config.json"
echo "🔧 Generating frontend runtime configuration..."
# Use API_URL from environment, or fallback to same origin
API_BASE_URL="${API_URL:-}"
if [ -z "$API_BASE_URL" ]; then
echo "⚠️ API_URL not set, frontend will auto-detect or use same origin"
# Create minimal config that triggers auto-detection
cat > "$CONFIG_FILE" <<EOF
{
"_comment": "API_URL not configured, using auto-detection"
}
EOF
else
echo "✅ API URL configured: $API_BASE_URL"
cat > "$CONFIG_FILE" <<EOF
{
"apiBaseURL": "$API_BASE_URL"
}
EOF
fi
echo "📄 Generated config.json:"
cat "$CONFIG_FILE"
# Start nginx
echo "🚀 Starting nginx..."
exec nginx -g "daemon off;"
+18 -1
View File
@@ -11,16 +11,33 @@ fi
# Gehe ins Docker-Verzeichnis
cd "$(dirname "$0")"
# Load development environment variables
if [ -f .env.dev ]; then
echo "📝 Loading configuration from .env.dev"
export $(grep -v '^#' .env.dev | xargs)
else
if [ -f .env ]; then
echo "📝 Loading configuration from .env"
export $(grep -v '^#' .env | xargs)
else
echo "⚠️ Warning: .env not found, using defaults"
fi
fi
# Get current git commit
export GIT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
echo "📝 Git Commit: $GIT_COMMIT"
echo "📦 Building and starting development containers..."
echo "🔧 Frontend will use API: ${API_URL:-http://localhost:8180}"
# Stoppe vorhandene Container
docker-compose -f docker-compose.dev.yml down
# Baue und starte die Container
docker-compose -f docker-compose.dev.yml up --build -d
docker-compose -f docker-compose.dev.yml --env-file .env.dev up --build -d
echo "✅ Development environment started."
echo "📱 Frontend: http://localhost:5173"
echo "🔌 Backend: http://localhost:8180"
echo "🗄️ phpMyAdmin: http://localhost:8081"
+23 -2
View File
@@ -11,14 +11,35 @@ fi
# Gehe ins Docker-Verzeichnis
cd "$(dirname "$0")"
# Load production environment variables
if [ -f .env.prd ]; then
echo "📝 Loading configuration from .env.prd"
export $(grep -v '^#' .env.prd | xargs)
else
if [ -f .env ]; then
echo "📝 Loading configuration from .env"
export $(grep -v '^#' .env | xargs)
else
echo "⚠️ Warning: .env not found, using defaults"
fi
fi
echo "📦 Building and starting production containers..."
echo "🔧 Frontend will use API: ${API_URL:-https://bamort-api.trokan.de}"
# Build before stopping existing containers
docker-compose -f docker-compose.yml build
docker-compose -f docker-compose.yml --env-file .env.prd build
# Stoppe vorhandene Container
docker-compose -f docker-compose.yml down
# Baue und starte die Container
docker-compose -f docker-compose.yml up --build -d
docker-compose -f docker-compose.yml --env-file .env.prd up -d
echo "✅ Production environment started."
echo "📱 Frontend: http://localhost:8181"
echo "🔌 Backend: http://localhost:8182"
echo ""
echo "💡 To change API URL: Edit .env.prd and run:"
echo " docker-compose -f docker-compose.yml restart frontend"
echo " (No rebuild needed!)"
+7
View File
@@ -13,6 +13,13 @@ dist
dist-ssr
*.local
# Runtime configuration (use config.json.example as template)
public/config.json
# Docker entrypoint scripts (copied from ../docker/ during build)
docker-entrypoint.sh
docker-dev-entrypoint.sh
# Editor directories and files
.vscode/*
!.vscode/extensions.json
+4
View File
@@ -0,0 +1,4 @@
{
"apiBaseURL": "http://localhost:8180",
"_comment": "This file can be modified at deployment without rebuilding. Copy to config.json and adjust apiBaseURL for your environment."
}
+83 -31
View File
@@ -4,38 +4,65 @@
* For desktop builds (Wails), reads the API base URL from the Go backend
* to allow runtime configuration via .env without rebuilding.
*
* For web builds, uses VITE_API_URL environment variable.
* For web builds, uses runtime config.json or auto-detection:
* 1. Try to load /config.json (can be modified at deployment without rebuild)
* 2. Try to detect backend at same origin (production reverse proxy setup)
* 3. Fall back to VITE_API_URL (development) or same origin
*/
let cachedAPIBaseURL = null
/**
* Check if we're running in a Wails desktop app
* Try to load configuration from /config.json
*/
function isWailsApp() {
return typeof window !== 'undefined' &&
window['go'] !== undefined &&
window['go']['main'] !== undefined &&
window['go']['main']['App'] !== undefined
async function loadConfigFile() {
try {
const response = await fetch('/config.json', {
cache: 'no-cache',
headers: { 'Accept': 'application/json' }
})
if (response.ok) {
// Check if response is actually JSON (not HTML from SPA fallback)
const contentType = response.headers.get('content-type')
if (contentType && contentType.includes('application/json')) {
const config = await response.json()
if (config.apiBaseURL) {
console.log('Loaded API URL from config.json:', config.apiBaseURL)
return config.apiBaseURL
}
}
}
} catch (error) {
// config.json doesn't exist or is invalid, that's okay
}
return null
}
/**
* Wait for Wails to initialize (max 3 seconds)
* Try to detect backend at same origin (production setup)
*/
async function waitForWails(maxAttempts = 30) {
for (let i = 0; i < maxAttempts; i++) {
if (isWailsApp()) {
return true
async function detectBackendAtOrigin() {
try {
const origin = window.location.origin
const response = await fetch(`${origin}/api/public/version`, {
method: 'GET',
cache: 'no-cache',
signal: AbortSignal.timeout(2000) // 2 second timeout
})
if (response.ok) {
console.log('Detected backend at same origin:', origin)
return origin
}
await new Promise(resolve => setTimeout(resolve, 100))
} catch (error) {
// Backend not at same origin, that's okay
}
return false
return null
}
/**
* Get the API base URL dynamically
* - In Wails desktop app: calls Go backend to get configured URL
* - In web app: uses VITE_API_URL or defaults to production API
* - In web app: tries config.json, auto-detection, or VITE_API_URL
*/
export async function getAPIBaseURL() {
// Return cached value if available
@@ -43,34 +70,58 @@ export async function getAPIBaseURL() {
return cachedAPIBaseURL
}
// Check if this looks like a desktop environment
const isDesktop = typeof window !== 'undefined' &&
(window.location.protocol === 'wails:' ||
window.location.hostname === 'wails.localhost')
// Wails desktop app - wait for Wails to be ready
if (isDesktop || typeof window !== 'undefined' && window['go']) {
const wailsReady = await waitForWails()
if (wailsReady) {
// Try Wails desktop app first (with timeout)
if (typeof window !== 'undefined' && window['go']) {
// Wait up to 3 seconds for Wails bindings to be ready
for (let i = 0; i < 30; i++) {
try {
// Access Wails Go bindings via window object
if (window['go']?.['main']?.['App']?.['GetAPIBaseURL']) {
const url = await window['go']['main']['App']['GetAPIBaseURL']()
cachedAPIBaseURL = url
console.log('Desktop app using API URL from config:', url)
return url
}
} catch (error) {
console.error('Failed to get API URL from Wails:', error)
// Fallback to localhost for desktop
break
}
await new Promise(resolve => setTimeout(resolve, 100))
}
// Wails detected but binding failed - use desktop fallback
cachedAPIBaseURL = 'http://localhost:8185'
console.log('Desktop app using fallback:', cachedAPIBaseURL)
return cachedAPIBaseURL
}
// Web app - try multiple strategies
// Strategy 1: Load from config.json (can be modified at deployment)
const configFileURL = await loadConfigFile()
if (configFileURL) {
cachedAPIBaseURL = configFileURL
return cachedAPIBaseURL
}
// Strategy 2: Check if backend is at same origin (production reverse proxy)
if (window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1') {
const sameOriginURL = await detectBackendAtOrigin()
if (sameOriginURL) {
cachedAPIBaseURL = sameOriginURL
return cachedAPIBaseURL
}
}
// Strategy 3: Use VITE_API_URL (development) or fallback to same origin
if (import.meta.env.VITE_API_URL) {
cachedAPIBaseURL = import.meta.env.VITE_API_URL
console.log('Web app using VITE_API_URL:', cachedAPIBaseURL)
} else {
// Final fallback: assume same origin for production
cachedAPIBaseURL = window.location.origin
console.log('Web app using same origin:', cachedAPIBaseURL)
}
// Web app - use environment variable or production default
cachedAPIBaseURL = import.meta.env.VITE_API_URL || 'https://bamort-api.trokan.de'
console.log('Web app using API URL:', cachedAPIBaseURL)
return cachedAPIBaseURL
}
@@ -79,4 +130,5 @@ export async function getAPIBaseURL() {
*/
export function isDesktopMode() {
return isWailsApp()
}
}typeof window !== 'undefined' &&
window['go']?.['main']?.['App'] !== undefined