Environment Separation Plan: Development vs Production
Status: Implemented and operational across all Frao stacks
Approach: Dual VPN isolation (frao.dev/frao.prod) with same ports across both stacks
Scope: All Frao Technologies services
Executive Summary
The Problem
Frao Technologies operates a complex microservice architecture. Initially, all services ran on a single Docker Compose stack. When an AI agent or developer ran docker compose up -d --build <service>, it rebuilt and restarted the only instance of that service — often the production one. There was no guard preventing accidental production disruption.
The Solution
We introduced two fully independent stacks that coexist on the same host, sharing nothing but the Docker daemon and the source tree:
| Stack | Env File | Bind IP | Data Volumes |
|---|---|---|---|
| Production | .env |
10.128.0.5 (frao.prod) |
hedgefund_* (existing) |
| Development | .env.dev |
10.64.0.5 (frao.dev) |
hedgefund_dev_* (new) |
Key design properties:
- Zero container name collisions — different
COMPOSE_PROJECT_NAME - Zero port collisions — same ports on different VPN interfaces
- Zero data collisions — dev uses separate volumes, databases, Redis instances
- AI agents default to dev — only explicit commands touch production
The Sacred Commandment
A core principle emerged: Dev-First, Prod-by-Intent.
THOU SHALT NOT rebuild, restart, or deploy to the production stack unless the user explicitly says "production", "prod", "deploy", or otherwise names the production stack.
The default target is ALWAYS the development stack.
Production is NEVER the default.
∀ rebuild_request ∈ {agent, user}:
if ¬explicit_prod_intent(rebuild_request) → target = development
∀ deploy_action ∈ {agent}:
confirm(user, "This affects PRODUCTION. Continue?") → proceed ∨ abort
Architecture
┌─────────────────────────────────────────────────────┐
│ Docker Host │
│ │
│ ┌────────────────────────────┐ ┌────────────────────────────┐
│ │ PRODUCTION (frao.prod) │ │ DEVELOPMENT (frao.dev) │
│ │ hedgefund_* │ │ hedgefund_dev_* │
│ │ Binds to: 10.128.0.5 │ │ Binds to: 10.64.0.5 │
│ │ │ │ │
│ │ db:5432 │ │ db:5432 │
│ │ redis:6379 │ │ redis:6379 │
│ │ backend:8080 │ │ backend:8080 │
│ │ frontend:3010 │ │ frontend:3010 │
│ │ company-portal:80 │ │ company-portal:80 │
│ │ ... │ │ ... │
│ └────────────────────────────┘ └────────────────────────────┘ │
│ │
│ Both stacks share: Docker daemon, source code │
│ Both stacks have: separate volumes, networks, ports │
└─────────────────────────────────────────────────────┘
How Stack Selection Works
The stack is selected by which --env-file and which override compose file is passed:
# Production (explicit — requires human confirmation)
docker compose -f docker-compose.yml -f docker-compose.prod.yml --env-file .env up -d
# Development (default for AI agents)
docker compose -f docker-compose.yml -f docker-compose.dev.yml --env-file .env.dev up -d
A Makefile wraps these into simple commands — making the intent crystal clear.
Implementation Phases
The separation was implemented in 10 phases over several weeks:
.env.devFile — copied from.env, overridden with dev-specific ports and secretsdocker-compose.dev.yml— dev overrides for ports, env vars, hot-reload volumesdocker-compose.prod.yml— thin production overlay with explicit bindings- Base compose cleanup — removed environment-specific overrides from shared config
- Makefile creation —
make up-dev,make rebuild-dev <svc>, and all other commands - Build script updates — separate image tags for dev vs prod
- Deploy script updates — explicit
--envflag for deployment target - VPS setup/update scripts — always target prod stack explicitly
- Skill & agent instruction updates — all AI agents trained to default to dev
- Testing — verified isolation, port separation, data separation
Port Allocation
Ports are identical across both stacks. Isolation comes from WireGuard VPN interfaces binding to different IPs:
| Service | Port | Binding |
|---|---|---|
| trading-platform-backend | 8080 | Same port, different VPN IP |
| trading-platform-frontend | 3010 | Same port, different VPN IP |
| matrix-svc | 3020 | Same port, different VPN IP |
| trading-signal-service | 8085 | Same port, different VPN IP |
| cost-analyzer-svc | 3030 | Same port, different VPN IP |
| company-portal | 80 | Same port, different VPN IP |
Since 10.64.0.5 and 10.128.0.5 are different network interfaces, they can use the same port numbers without conflict. This eliminates the +1000 offset scheme used previously.
Lessons Learned: Hardcoded IPs
During implementation, we discovered 33 hardcoded networking references across 4 repositories. These fell into categories:
Application Source Code
Services that hardcoded 10.64.0.5 as their bind address — which fails inside containers where that IP is on the host, not in the container’s network namespace.
Fix: Every service now reads its bind address from an environment variable with fallback to 0.0.0.0.
Frontend URL Construction
SvelteKit components constructing URLs with hardcoded WireGuard IPs instead of relative paths — breaking on the other stack.
Fix: All frontend code uses relative paths or window.location.host. Never construct URLs with hardcoded IPs.
Docker Compose Bindings
Port mappings hardcoded to 10.64.0.5 instead of using ${BIND_IP} variable syntax.
Fix: All compose files use ${BIND_IP:-<default>} set per-stack in .env / .env.dev.
AI Agent Integration
The most critical change was training all AI agents to default to the development stack. Every agent that works on Frao services now follows this flow:
1. Agent receives a change request
2. Agent determines: user didn't say "production" → targets DEVELOPMENT
3. Agent edits code on the `dev` branch
4. Agent runs: make rebuild-dev <service>
5. ✅ Development service rebuilds. Production untouched.
The Makefile provides unambiguous command pairs:
make up/make up-dev— start stacksmake rebuild <svc>/make rebuild-dev <svc>— rebuild servicesmake logs <svc>/make logs-dev <svc>— view logsmake down/make down-dev— stop stacks
Verification & Testing
Our isolation verification suite ensures both stacks work correctly:
| Test | Expected |
|---|---|
make up then docker ps |
Only hedgefund_* containers |
make up-dev alongside |
Both hedgefund_* AND hedgefund_dev_* running |
make down-dev then docker ps |
Only hedgefund_* remaining |
| Volume listing | Both hedgefund_* and hedgefund_dev_* volumes exist |
| Simulated dev rebuild | Prod container uptime unchanged |
Conclusion
The dual-stack architecture has been running successfully since implementation. Key takeaways:
- Simple design works best — same ports on different VPN interfaces
- Makefiles are the interface — agents never call docker compose directly
- Training agents is as important as configuring infrastructure — explicit guardrails in skill definitions
- Discover hardcoded IPs aggressively —
rg -n '10\.(64|128)\.0\.\d+'is now standard procedure for new services
This pattern has become the canonical approach for all new Frao Technologies services: separate env files, separate compose overrides, Makefile wrappers, and AI agent training to default to development.
Originally documented in Frao Doc: "Environment Separation Plan: Development vs Production" (shortid: 040718ed)