This guide explains how to set up a multi-environment development system that allows multiple isolated environments to run simultaneously using:
- Port Offsets: Each environment uses unique ports to avoid conflicts
- Shared Infrastructure: Single database instances with logical isolation
- Vite Hot Reloading: Development server with instant UI updates
- Git Worktrees: Optional multi-branch development support
Based on the Chronicle codebase architecture.
Layer 1: Shared Infrastructure (Same Ports)
- MongoDB: Single instance on port 27017
- Redis: Single instance on port 6379
- Qdrant/Vector Store: Single instance on port 6333
- Why? Resource efficient, simpler to manage, no duplicate data services
Layer 2: Application Services (Offset Ports)
- Backend API: 8000 + PORT_OFFSET
- Frontend/WebUI: 3000 + PORT_OFFSET
- Why? Each environment needs its own application, but they can share databases
Layer 3: Logical Isolation (Database Names)
- MongoDB databases:
projectname,projectname_env1,projectname_env2 - Redis databases: 0, 1, 2, 3... (Redis supports 0-15)
- Why? Data isolation without duplicate database services
project/
├── .env.default # Committed template with defaults
├── backends/advanced/
│ ├── .env # Generated, gitignored overrides
│ ├── docker-compose.yml # Main compose file
│ ├── compose/
│ │ ├── backend.yml # Backend service definition
│ │ ├── frontend.yml # Frontend service definition
│ │ └── overrides/
│ │ ├── dev-webui.yml # Vite dev server override
│ │ └── prod.yml # Production build override
│ └── webui/
│ ├── Dockerfile.dev # Dev container with npm
│ ├── vite.config.ts # Vite configuration
│ └── src/ # Source files (volume mounted)
├── compose/
│ └── infrastructure-shared.yml # Shared databases
└── setup/
└── setuputils.py # Port/DB validation utilities
Create setup/setuputils.py for validation:
#!/usr/bin/env python3
"""
Setup utilities for multi-environment configuration.
Provides port checking and Redis database validation.
"""
import sys
import socket
import subprocess
import json
from typing import List, Tuple, Optional
def check_port_in_use(port: int) -> bool:
"""Check if a TCP port is already in use."""
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.settimeout(0.5)
result = sock.connect_ex(('127.0.0.1', port))
return result == 0
except Exception:
return False
def find_available_redis_db(
preferred_db: int = 0,
env_name: Optional[str] = None,
container_name: str = "redis"
) -> int:
"""
Find available Redis database (0-15) for the environment.
First checks if environment already has a marked database (reuse it).
If not, tries preferred database, then finds empty one.
This prevents database exhaustion in multi-worktree setups.
"""
# Check if this environment already has a marked database
if env_name:
for db in range(16):
marker = get_redis_db_env_marker(db, container_name)
if marker == env_name:
return db
# Try preferred database if empty
if not check_redis_db_has_data(preferred_db, container_name):
return preferred_db
# Find any empty database
for db in range(16):
if not check_redis_db_has_data(db, container_name):
return db
# All full - return preferred
return preferred_db
def set_redis_db_env_marker(
db_num: int,
env_name: str,
container_name: str = "redis"
) -> bool:
"""
Set environment marker in Redis database.
Marker: chronicle:env:name = environment_name
"""
try:
result = subprocess.run(
["docker", "exec", container_name, "redis-cli", "-n", str(db_num),
"SET", "chronicle:env:name", env_name],
capture_output=True,
text=True,
timeout=5
)
return result.returncode == 0
except Exception:
return False
def validate_ports(ports: List[int]) -> Tuple[bool, List[int]]:
"""Validate that ports are available."""
conflicts = [port for port in ports if check_port_in_use(port)]
return (len(conflicts) == 0, conflicts)Key Features:
- Port conflict detection
- Redis database finder with environment markers
- Prevents database exhaustion (important for 16 DB limit)
- JSON output for shell script consumption
# Project Default Configuration
# Committed to repository - provides base defaults
# ==========================================
# DOCKER COMPOSE PROJECT NAME
# ==========================================
COMPOSE_PROJECT_NAME=projectname
# ==========================================
# DATABASE CONFIGURATION (Shared Infrastructure)
# ==========================================
MONGODB_URI=mongodb://mongo:27017
MONGODB_DATABASE=projectname
REDIS_URL=redis://redis:6379/0
REDIS_DATABASE=0
QDRANT_BASE_URL=qdrant
# ==========================================
# NETWORK CONFIGURATION (Application Ports)
# ==========================================
PORT_OFFSET=0
BACKEND_PORT=8000
WEBUI_PORT=3000
HOST_IP=localhost
# CORS with variable substitution support
CORS_ORIGINS=http://localhost:${WEBUI_PORT:-3000},http://localhost:${BACKEND_PORT:-8000}
VITE_BACKEND_URL=http://localhost:${BACKEND_PORT:-8000}
# ==========================================
# TEST ENVIRONMENT PORTS
# ==========================================
TEST_BACKEND_PORT=8001
TEST_WEBUI_PORT=3001
# ==========================================
# AUTHENTICATION & API KEYS
# ==========================================
AUTH_SECRET_KEY=
# Add other secrets...Why this structure?
- ✅ Committed template everyone can use
- ✅ Variable substitution for dynamic ports
- ✅ Clear separation of concerns
- ✅ Test ports automatically offset by 1
Create quick-start.sh:
#!/bin/bash
set -e
ENV_FILE="backends/advanced/.env"
SETUP_UTILS="setup/setuputils.py"
# Colors for output
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'
echo "🚀 Multi-Environment Setup"
echo ""
# Prompt for environment name
read -p "Environment name [projectname]: " INPUT_ENV_NAME
ENV_NAME="${INPUT_ENV_NAME:-projectname}"
ENV_NAME=$(echo "$ENV_NAME" | tr '[:upper:]' '[:lower:]' | tr -cs '[:alnum:]' '-')
# Find available ports with validation
PORTS_AVAILABLE=false
while [ "$PORTS_AVAILABLE" = false ]; do
read -p "Port offset [0]: " INPUT_PORT_OFFSET
PORT_OFFSET="${INPUT_PORT_OFFSET:-0}"
BACKEND_PORT=$((8000 + PORT_OFFSET))
WEBUI_PORT=$((3000 + PORT_OFFSET))
# Validate ports using Python utility
PORT_CHECK=$(python3 "$SETUP_UTILS" validate-ports "$BACKEND_PORT" "$WEBUI_PORT")
if [ $? -eq 0 ]; then
PORTS_AVAILABLE=true
else
CONFLICTS=$(echo "$PORT_CHECK" | python3 -c "import sys, json; print('\n'.join([f'Port {p}' for p in json.load(sys.stdin)['conflicts']]))")
echo -e "${RED}⚠️ Port conflicts: ${CONFLICTS}${NC}"
echo "Please choose different offset"
fi
done
# Find available Redis database
PREFERRED_REDIS_DB=$(( (PORT_OFFSET / 10) % 16 ))
REDIS_RESULT=$(python3 "$SETUP_UTILS" find-redis-db "$PREFERRED_REDIS_DB" "$ENV_NAME")
REDIS_DATABASE=$(echo "$REDIS_RESULT" | python3 -c "import sys, json; print(json.load(sys.stdin)['db_num'])")
# Set environment marker
python3 "$SETUP_UTILS" set-redis-marker "$REDIS_DATABASE" "$ENV_NAME"
# Calculate test ports
TEST_BACKEND_PORT=$((8001 + PORT_OFFSET))
TEST_WEBUI_PORT=$((3001 + PORT_OFFSET))
# Set database names
if [[ "$ENV_NAME" == "projectname" ]]; then
MONGODB_DATABASE="projectname"
COMPOSE_PROJECT_NAME="projectname"
else
MONGODB_DATABASE="projectname_${ENV_NAME}"
COMPOSE_PROJECT_NAME="projectname-${ENV_NAME}"
fi
echo ""
echo -e "${GREEN}✅ Configuration:${NC}"
echo " Environment: $ENV_NAME"
echo " Backend: $BACKEND_PORT"
echo " WebUI: $WEBUI_PORT"
echo " Redis DB: $REDIS_DATABASE"
echo ""
# Generate .env file
cat > "$ENV_FILE" <<EOF
# Environment Overrides - Generated $(date -u +"%Y-%m-%dT%H:%M:%SZ")
# DO NOT COMMIT - Contains environment-specific configuration
ENV_NAME=${ENV_NAME}
COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME}
MONGODB_DATABASE=${MONGODB_DATABASE}
REDIS_DATABASE=${REDIS_DATABASE}
PORT_OFFSET=${PORT_OFFSET}
BACKEND_PORT=${BACKEND_PORT}
WEBUI_PORT=${WEBUI_PORT}
TEST_BACKEND_PORT=${TEST_BACKEND_PORT}
TEST_WEBUI_PORT=${TEST_WEBUI_PORT}
CORS_ORIGINS=http://localhost:${WEBUI_PORT},http://localhost:${BACKEND_PORT}
VITE_BACKEND_URL=http://localhost:${BACKEND_PORT}
# Add your API keys here:
# OPENAI_API_KEY=
# DEEPGRAM_API_KEY=
EOF
chmod 600 "$ENV_FILE"
# Ask about dev server
echo ""
read -p "Enable Vite dev server? (Y/n): " use_dev_server
if [[ "$use_dev_server" == "n" ]] || [[ "$use_dev_server" == "N" ]]; then
COMPOSE_OVERRIDE="-f compose/overrides/prod.yml"
else
COMPOSE_OVERRIDE="-f compose/overrides/dev-webui.yml"
fi
# Start infrastructure
echo ""
echo "🏗️ Starting shared infrastructure..."
docker compose -f compose/infrastructure-shared.yml up -d
# Start application
echo ""
echo "🚀 Starting application..."
cd backends/advanced
docker compose -f docker-compose.yml $COMPOSE_OVERRIDE up -d --build
echo ""
echo -e "${GREEN}✅ Ready! Access at: http://localhost:${WEBUI_PORT}${NC}"Script Features:
- ✅ Interactive setup with validation
- ✅ Port conflict detection
- ✅ Redis database finder with reuse
- ✅ Automatic test port calculation
- ✅ Dev server option prompt
# Shared infrastructure for all environments
# Start once: docker compose -f compose/infrastructure-shared.yml up -d
services:
mongo:
image: mongo:latest
container_name: mongo
ports:
- "27017:27017"
volumes:
- mongo_data:/data/db
networks:
- projectname-network
restart: unless-stopped
redis:
image: redis:alpine
container_name: redis
ports:
- "6379:6379"
volumes:
- redis_data:/data
networks:
- projectname-network
restart: unless-stopped
qdrant:
image: qdrant/qdrant:latest
container_name: qdrant
ports:
- "6333:6333"
- "6334:6334"
volumes:
- qdrant_data:/qdrant/storage
networks:
- projectname-network
restart: unless-stopped
networks:
projectname-network:
name: projectname-network
volumes:
mongo_data:
redis_data:
qdrant_data:# Main application compose file
# Usage: docker compose -f docker-compose.yml -f compose/overrides/dev-webui.yml up
include:
- compose/backend.yml
- compose/frontend.yml
# Note: Infrastructure is SHARED across all environments
# Start once with: docker compose -f ../../compose/infrastructure-shared.yml up -d
networks:
projectname-network:
name: projectname-network
external: true# Backend service definition
services:
backend:
build:
context: ..
dockerfile: Dockerfile
env_file:
- ../../../.env.default # Base defaults
- ../.env # Environment overrides
ports:
- "${BACKEND_PORT:-8000}:8000"
volumes:
- ../src:/app/src
- ../data:/app/data
environment:
- REDIS_URL=redis://redis:6379/${REDIS_DATABASE:-0}
- MONGODB_URI=${MONGODB_URI:-mongodb://mongo:27017}
restart: unless-stopped
networks:
- projectname-network
networks:
projectname-network:
name: projectname-network
external: trueCritical Details:
- ✅
env_filereads BOTH default and override - ✅ Port uses variable with fallback:
${BACKEND_PORT:-8000} - ✅ Redis URL includes database number:
/${REDIS_DATABASE:-0} - ✅ External network connects to shared infrastructure
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
base: process.env.VITE_BASE_PATH || '/',
server: {
port: 5173,
host: '0.0.0.0', // Allow external connections
allowedHosts: process.env.VITE_ALLOWED_HOSTS
? process.env.VITE_ALLOWED_HOSTS.split(' ')
: ['localhost', '127.0.0.1', '.nip.io'],
hmr: {
port: 5173,
// HMR connects to external port (not internal)
clientPort: process.env.VITE_HMR_PORT
? parseInt(process.env.VITE_HMR_PORT)
: undefined,
},
},
build: {
outDir: 'dist',
sourcemap: true,
},
})Key Configuration:
- ✅
host: '0.0.0.0'- Allows Docker port mapping - ✅
clientPort- HMR uses external port (WEBUI_PORT) - ✅
allowedHosts- Prevents Host header attacks
# Development override for hot-reload
# Usage: docker compose -f docker-compose.yml -f compose/overrides/dev-webui.yml up
services:
webui:
build:
context: ./webui
dockerfile: Dockerfile.dev
args:
- VITE_BASE_PATH=${VITE_BASE_PATH:-/}
- VITE_BACKEND_URL=${VITE_BACKEND_URL:-http://localhost:8000}
command: npm run dev
volumes:
# Volume mount source files for hot-reload
- ./webui/src:/app/src
- ./webui/public:/app/public
- ./webui/index.html:/app/index.html
- ./webui/vite.config.ts:/app/vite.config.ts
ports:
- "${WEBUI_PORT:-3000}:5173" # External:Internal
environment:
- VITE_BACKEND_URL=${VITE_BACKEND_URL:-http://localhost:8000}
- VITE_HMR_PORT=${WEBUI_PORT:-3000} # HMR client connects to external portCritical for Hot Reloading:
- ✅ Volume mounts for
src/,public/,vite.config.ts - ✅
VITE_HMR_PORTset to external port (WEBUI_PORT) - ✅ Port mapping:
${WEBUI_PORT}:5173(external:internal)
FROM node:18-alpine
WORKDIR /app
# Install dependencies
COPY package.json package-lock.json ./
RUN npm ci
# Copy config files
COPY . .
EXPOSE 5173
# Start dev server (npm run dev = vite)
CMD ["npm", "run", "dev"]Why this works:
- ✅ Dependencies installed at build time
- ✅ Source files mounted as volumes (not copied)
- ✅ Changes to
src/trigger instant HMR - ✅ No rebuild needed for code changes
Symptoms:
- Changes to files don't appear
- Page requires manual refresh
Causes:
- Source files not volume mounted
- HMR client port mismatch
- WebSocket connection blocked
Solution:
# dev-webui.yml MUST include:
volumes:
- ./webui/src:/app/src # Critical!
- ./webui/vite.config.ts:/app/vite.config.ts
environment:
- VITE_HMR_PORT=${WEBUI_PORT:-3000} # Must match external portSymptoms:
- "port already in use" error
- Services fail to start
Solution:
# Use setuputils.py validation
python3 setup/setuputils.py validate-ports 8000 3000
# If conflicts, choose different offset
PORT_OFFSET=10 # backend=8010, webui=3010Symptoms:
- Seeing data from other environment
- Database shows wrong information
Causes:
- Forgot to set MONGODB_DATABASE
- REDIS_DATABASE not configured
- Using wrong .env file
Solution:
# Check .env file:
cat backends/advanced/.env
# Should show:
MONGODB_DATABASE=projectname_gold # NOT projectname
REDIS_DATABASE=1 # NOT 0
# Restart to apply:
docker compose down && docker compose up -dSymptoms:
- Must run
docker compose buildfor changes - Using wrong override file
Cause:
Using production override (prod.yml) instead of dev override
Solution:
# Wrong (production - no hot reload):
docker compose -f docker-compose.yml -f compose/overrides/prod.yml up
# Right (development - hot reload):
docker compose -f docker-compose.yml -f compose/overrides/dev-webui.yml upSymptoms:
- Running out of Redis databases
- Multiple environments using same DB
Cause: Redis only supports 16 databases (0-15), restarting environments claims new DBs
Solution: Implement environment markers:
# setuputils.py
def set_redis_db_env_marker(db_num: int, env_name: str):
"""Mark database with environment name for reuse"""
subprocess.run([
"docker", "exec", "redis", "redis-cli",
"-n", str(db_num), "SET", "chronicle:env:name", env_name
])
def find_available_redis_db(preferred_db: int, env_name: str):
"""Check for existing marker before claiming new DB"""
for db in range(16):
marker = get_redis_db_env_marker(db)
if marker == env_name:
return db # Reuse marked database
# ... continue with normal logicDefault environment: 0 (backend=8000, webui=3000)
Environment 1: 10 (backend=8010, webui=3010)
Environment 2: 20 (backend=8020, webui=3020)
Environment 3: 30 (backend=8030, webui=3030)
Why? Easy mental math, room for expansion (test ports +1)
✅ Good: gold, green, staging, dev, feature-auth
❌ Bad: env1, test123, johns-branch
Why? Descriptive names aid debugging and identification
Default: projectname
Named: projectname_envname
Test: projectname_envname_test
Why? Clear ownership, no collisions, test isolation
# Never commit environment-specific files
backends/advanced/.env
.env.quick-start
# Always commit templates
!.env.default
!.env.templateFor Hot Reload (Development):
volumes:
- ./webui/src:/app/src # Source code
- ./webui/public:/app/public # Static assets
- ./webui/vite.config.ts:/app/vite.config.ts
# NOT package.json or node_modules!For Production:
# No volumes - code baked into image
# Rebuild required for changes# .env with variable substitution
CORS_ORIGINS=http://localhost:${WEBUI_PORT},http://localhost:${BACKEND_PORT}
# Expands to (offset=10):
# http://localhost:3010,http://localhost:8010Why? Dynamic CORS based on actual ports
# Development ports:
BACKEND_PORT=8000 + offset
WEBUI_PORT=3000 + offset
# Test ports (always +1):
TEST_BACKEND_PORT=8001 + offset
TEST_WEBUI_PORT=3001 + offset
# Test database:
MONGODB_DATABASE=${MONGODB_DATABASE}_testBenefit: Parallel testing across worktrees without conflicts
# Each environment has its own test ports
cd worktree-gold/backends/advanced
./run-test.sh # Uses 8011, 3011 (offset=10 + 1)
# Simultaneously in another worktree:
cd worktree-green/backends/advanced
./run-test.sh # Uses 8021, 3021 (offset=20 + 1)Current setup:
# Single .env file with hardcoded ports
BACKEND_PORT=8000
WEBUI_PORT=3000Migration steps:
- Backup existing config:
cp backends/advanced/.env backends/advanced/.env.backup- Create .env.default template:
# Extract common config to .env.default
# Use variable substitution: ${BACKEND_PORT:-8000}- Run quick-start.sh:
./quick-start.sh
# Choose environment name: default
# Choose port offset: 0 (maintains existing ports)- Copy API keys:
# Copy keys from backup to new .env
grep API_KEY backends/advanced/.env.backup >> backends/advanced/.env- Test and verify:
docker compose down
docker compose -f compose/infrastructure-shared.yml up -d
cd backends/advanced
docker compose up -d- Create
setuputils.pywith port and Redis DB validation - Create
.env.defaultwith variable substitution - Create
quick-start.shfor interactive setup - Set up infrastructure-shared.yml (MongoDB, Redis, Qdrant)
- Configure backend.yml with env_file layers
- Create dev-webui.yml override for hot reload
- Configure vite.config.ts with HMR settings
- Create Dockerfile.dev with volume mounts
- Add .env to .gitignore
- Test hot reload with source file change
- Vite server on host: '0.0.0.0'
- VITE_HMR_PORT set to external port
- src/ folder volume mounted
- vite.config.ts volume mounted
- Port mapping: ${WEBUI_PORT}:5173
- Using dev-webui.yml override
- npm run dev starts Vite dev server
- Each environment has unique PORT_OFFSET
- Each environment has unique MONGODB_DATABASE
- Each environment has unique REDIS_DATABASE (0-15)
- Each environment has unique COMPOSE_PROJECT_NAME
- Port validation runs before starting
- Redis DB markers prevent exhaustion
- Test ports automatically calculated (+1 offset)
- Infrastructure started only once
- .env files gitignored
# Check which ports are in use
python3 setup/setuputils.py validate-ports 8000 3000 8010 3010
# Find available Redis database for environment
python3 setup/setuputils.py find-redis-db 2 "gold"
# List running containers by environment
docker ps --filter "name=projectname-gold"
# View all environment containers
docker ps --format "table {{.Names}}\t{{.Ports}}" | grep projectname
# Stop specific environment
docker compose -p projectname-gold down
# View logs for specific environment
docker logs projectname-gold-backend-1 -f# Check if WebSocket connection works
# Open browser dev console → Network → WS tab
# Should see: ws://localhost:3010/ (connected)
# Verify volume mounts
docker inspect projectname-webui-1 | grep -A 20 Mounts
# Check HMR environment variables
docker exec projectname-webui-1 env | grep VITE_HMR_PORTThis multi-environment setup provides:
✅ Parallel Development: Multiple environments run simultaneously ✅ Resource Efficiency: Single database infrastructure shared by all ✅ Fast Iteration: Vite hot reload for instant UI updates ✅ Data Isolation: Separate databases prevent cross-contamination ✅ Parallel Testing: Test suites run concurrently without conflicts ✅ Simple Management: Clear container naming, easy identification ✅ Scalability: Supports up to 16 environments (Redis DB limit)
By following this guide, other agents can implement the same robust multi-environment architecture used in the Chronicle project.