Handbook
ADR: Fleet environment provisioning
Status: Accepted Date: 2026-08-28
Updated
Context
Forge Fleet already manages compose stacks (container services, app gateways, Market Studio rollout) but has no first-class environment model. Operators hand-maintain deploy/forge-market-studio and deploy/forge-market-studio-dev with duplicated .env files and separate rollout scripts.
Lenses Studio needs a generic wizard to create DEV/PROD (and other) environments from templates. Market Studio needs a persisted gateway URL selector so the workstation thin client can target the right remote stack.
Decision
Introduce a generic environment subsystem in Fleet:
- Environment records under
$FLEET_DATA_DIR/etc/environments/{app}--{env}.json. - Environment templates (
env_templates.py) describing how to render.envand compose identity for an app. - Provisioning API (
POST /v1/environments) with async progress log. - Seed modes:
clean(empty volumes + migrate) andreplicate(stop source, cold-copy volumes, migrate target). - Lifecycle: start, stop, delete (with optional volume purge).
- Adopt path for existing hand-made stacks without modifying them.
Provisioning runs on the Fleet host via the existing admin API (no SSH).
Environment record
See docs/schemas/environment.schema.json. Key fields:
| Field | Purpose |
|---|---|
id |
{app_id}--{env_id} |
app_id |
Logical app (e.g. forge-market-studio) |
env_id |
Short slug (prod, dev, staging) |
template_id |
Template used to render .env |
compose_root |
Absolute path to compose directory |
container_service_id |
Fleet managed service id |
gateway_slug |
App gateway service id |
ports |
Loopback host ports (app, postgres, …) |
volumes |
Docker volume names |
state |
Provisioning state machine value |
adopted |
true when materialised from an existing stack |
Template descriptor
Templates capture:
source_compose_root— directory to copy when provisioningcompose_overlay— overlay filename (e.g.compose.granite.yaml)app_service_name— primary service for health/migrateenv_keys— list of.envkeys with rewrite rules (literal,port,suffix,compose_project, …)port_ranges— suggested loopback ranges for auto-allocationvolume_map— volume env keys → suffix patternmigrate_command— compose run command for schema migrategateway— slug pattern, upstream port key, bearer env key
Provisioning state machine
pending → provisioning → seeding → migrating → registering → ready
↘ failed
- provisioning: allocate ports, copy compose root, render
.env, create volumes - seeding:
clean= skip;replicate= cold-copy volumes from source - migrating: postgres up, run migrate command
- registering: compose up, container-service + app-gateway registration, health smoke
Failed provisions run a cleanup path (remove generated compose dir, volumes, gateway) when provisioned_at is set and state is failed.
Volume replication
When seed: replicate, the source stack is stopped (compose down or stop services). Each mapped volume is copied with a throwaway helper container:
docker run --rm -v src:/from:ro -v dst:/to alpine sh -c 'cp -a /from/. /to/'
Source is restarted when restart_source: true (default). A per-app provisioning lock prevents concurrent replicate/provision operations.
Port allocation
Loopback-only ports matching validate_loopback_upstream in app_gateway.py. Allocator binds 127.0.0.1:port to confirm availability before writing .env.
API surface
| Method | Path | Purpose |
|---|---|---|
| GET | /v1/environments |
List (optional ?app=) |
| GET | /v1/environments/{id} |
Single record + deployment status |
| GET | /v1/environments/templates |
List templates |
| POST | /v1/environments |
Provision (seed: clean\|replicate) |
| POST | /v1/environments/{id}/replicate |
Re-seed from another environment |
| POST | /v1/environments/{id}/start |
compose up -d |
| POST | /v1/environments/{id}/stop |
compose stop |
| DELETE | /v1/environments/{id} |
Deregister gateway; optional ?purge_volumes=1 |
| GET | /v1/environments/provision-log |
Async progress log |
Open questions ledger
| # | Question | Resolution |
|---|---|---|
| 1 | Provisioning transport | Fleet host admin API only |
| 2 | Volume replication method | Alpine helper container cold-copy |
| 3 | Port scope | Loopback only |
| 4 | PROD replicate during live test | Defer if PROD unreachable; use clean seed |
| 5 | Bearer hydration on gateway PUT | Read from target .env via app_bearer_env |
Consequences
- Lenses proxies
/api/environments*behindLENSES_EXPERIMENTAL_ENVIRONMENTS. - Market Studio persists
gateway_urlin data-plane pref; launchers stop clobbering it. - Existing
market-studio/market-studio-devstacks are adopted, not duplicated.