Unified Docker development
The root Compose deployment runs one isolated MailGen instance per checkout. A shared Nginx Proxy Manager (NPM) has a separate lifecycle and persistent state. This is single-host Docker Compose, not Docker Swarm.
From first setup to daily work
Run commands in the motherrepo root. Begin with first developer setup and environment configuration. Tmux displays existing container terminals. It does not start another host application.
Placement
| Root selection | Local services | Use |
|---|---|---|
Missing selectors or fulllocal / false | Applications and all selected infrastructure | Full-local startup |
partialRemote / true | Genuine local infrastructure and local values | Exercise the remote configuration path |
partialRemote / false | Applications, PostgreSQL, SeaweedFS/S3, OpenSearch, Dragonfly, proxy, and keys | Prepared remote Kafka, ClamAV, Collabora, Tika, and Qdrant |
Simulation is not a mock or a memory-saving remote deployment. fulllocal with simulation true is invalid. Inactive remote settings do not activate remote services. The runtime derives active consumer settings in memory.
Placement does not choose development or production execution. It does not move Kafka offsets or Qdrant points. Complete the remote contract before changing existing targets.
First setup
Complete first installation, including repository permissions, Docker, root configuration, provider callbacks, and secret files.
Before cold bootstrap, inspect load:
free -h
uptime
docker ps
tmux list-sessions
bash memmonitor.sh
free -h applies to Linux/WSL. On macOS, inspect Docker Desktop resources and Activity Monitor. No tmux session does not mean the application is broken. Exit the monitor with Ctrl+C.
Choose one full active instance before cold bootstrap or reset. Stop only the selected instance from its own checkout, preserving its volumes. Keep unrelated services running. Separate networks do not supply separate physical RAM.
bash 0daysetup.sh
bash devstack.sh status
Setup adds missing derived values, checks configuration/ownership/memory, prepares shared proxy and client routing, then starts production execution. FormGen and docs-mcp are selected explicitly. Google selection depends on provider flags. Correct reported prerequisites if setup stops. Do not delete data.
Daily lifecycle
| Goal | Command | Result |
|---|---|---|
| Check configuration | bash devstack.sh validate | Reject invalid input without displaying secrets |
| Build images | bash devstack.sh build | Build images without proving application readiness |
| New full development start | bash startDevServers.sh | Dependency preparation, initialization, and development processes |
| Stop this instance | bash stopDevServer.sh | Stop managed applications/local infrastructure, preserve volumes and keys |
| Apply configuration without rebuilding production | bash restartDevServers.sh | Recreate services with saved modes; compiled artifacts remain unchanged |
| Select compiled production | bash startDevServers.sh --prod or bash restartDevServers.sh --prod | Build compiled output and select production execution |
| Resume unchanged containers | bash quickStarter.sh | Start compatible existing stopped containers without build/recreation/Prisma/bootstrap |
A normal restart does not run initialization jobs or schema migrations. It does not rebuild production artifacts. Publish changed compiled code with bash restartDevServers.sh --prod, after any separately required schema/dependency preparation. Use the scoped hotfix procedure for one service.
Quick start requires previous successful preparation. It rejects stale configuration or incompatible containers. Do not edit fingerprints to bypass rejection. Use ordinary restart after configuration or source changes.
Scoped recovery and profiles
bash devstack.sh restart bff
bash devstack.sh start bff
bash devstack.sh start bff --prod
Scoped start/restart resumes saved execution. start SERVICE --prod explicitly selects production for that service. Scoped restart rejects --prod. New services without saved state default to development on resume. See execution modes.
A scoped start requires prepared dependencies. It does not bootstrap missing infrastructure or activate a disabled provider. Global endpoint, credential, or placement changes need ordinary full restart.
Optional profiles add services. tools covers initialization helpers. lab adds Kafka laboratory tools. docs-mcp adds documentation MCP. FormGen and Google profiles select their applications. Provider feature flags derive Google OAuth/sync/watch selection.
Browser and observable checks
Open the printed Account and BFF HTTPS URLs. Complete first login, connect/select a permitted mailbox, and unlock the correct personal email key. A new login does not recover an old key.
Check message bodies, search, the affected feature, and operation confirmation. Accepted or awaiting_confirmation writes are not confirmed provider state. Inspect the operation and a later canonical read.
Logs and terminals
bash devstack.sh status
bash devstack.sh logs bff
bash devServerAttach.sh
Ctrl+C ends log viewing. In an attached service terminal, it can stop the service. Ctrl+P, then Ctrl+Q, detaches without stopping. Do not start a duplicate Node/.NET process. Interactive start and prod select that service's development or compiled execution.
Configuration and startup order
The unified stack reads the root .env. Environment reference explains all retained variable groups. Never publish raw docker compose config output because resolved values can contain secrets.
| Input | Responsibility |
|---|---|
BETTER_AUTH_URL, NEXT_PUBLIC_BETTER_AUTH_URL | Matching external Account HTTPS origins |
BFF_MICROSOFT_CLIENT_ID, BFF_MICROSOFT_CLIENT_SECRET, BFF_MICROSOFT_TENANT_ID | Application login |
BACKEND_CLIENT_ID, BACKEND_CLIENT_SECRET, BACKEND_TENANT_ID | Backend/.NET app-only Graph access |
DATABASE_URL, BETTERAUTH_DATABASE_URL, POSTGRES_CONNECTION_STRING | Consistent internal database connections |
POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD | New database initialization |
S3_*, SEAWEEDFS_ADMIN_USER, SEAWEEDFS_ADMIN_PASSWORD | Object access and separate storage administration |
KAFKA_UI_USER, KAFKA_UI_PASSWORD | UI login, separate from broker SASL |
GRAPH_WEBHOOK_URL, GRAPH_LIFECYCLE_WEBHOOK_URL | This instance's public notification callbacks |
CLOUDFLARE_TOKEN, CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_ZONE_ID | DNS/tunnel automation |
PROXY_ADMIN_BIND_IP, ECS_PROXY_STATE_DIR | Shared proxy administration and state |
ECS_CPUSET, ECS_BUILD_CPUSET | Runtime and build CPU lists |
Server-only BFF API_URL and SORTER_SERVER_URL use Docker DNS. They are not browser public endpoints. Keep application-login and Graph credentials distinct. See Microsoft Graph setup.
Startup checks configuration, ownership, retained rollback state, and memory. It drains old application writers, prepares state/tunnel, builds images, installs dependencies, and builds .NET helpers. It waits for infrastructure before database/mailbox initialization. Kafka and dependency preflight precede application startup. Proxy reconciliation and webhook checks follow. Stack ready marks startup completion, not successful mailbox acceptance.
Setup supports an existing configuration, an archive password, or the chip '<passphrase>' '<token>' --decrypt form. Old --chbranch / --pinned behavior is not part of this entrypoint.
Services and live development
Application services run in separate containers with clone-specific bind mounts and cache volumes. Development watchers load source changes. Dependency changes can require the full controlled preparation path again. Restart alone does not run that path. Production uses compiled artifacts, which require an explicit build for code publication. Production docs need their corresponding build; development npm start watches source.
Backend, BFF, documentation, and .NET dependency/build jobs complete before dependent services. Keep generated clients and database schema compatible. Preserve mapped tables, keys, and foreign-key actions. A destructive schema difference is a stop condition, not a reason to add --accept-data-loss.
Root lifecycle scripts own Docker startup and shutdown. Older host-process/tmux scripts are not an alternate default. Stop preserves persistent volumes. Reset requires explicit data-deletion intent.
Reviewed source with existing data
ECS_APPLICATION_SOURCE_ROOT selects an existing physical source checkout or reviewed snapshot for application/runtime mounts and FormGen image sources. Missing, empty, or . retains the current checkout.
The selected path must exist on the Docker host and be readable. Source selection does not change the Compose project, clone owner, root .env, application volumes, authorization keys, Graph certificates, or shared proxy. Generated clients/build outputs remain in the original instance's cache volumes.
Use an identified repository revision when working-tree edits must not run against retained data. Check the snapshot manifest before startup. The selector does not create or authenticate snapshots. Keep lifecycle commands in the original checkout. Never point it at another instance's configuration/data directory.
A restart can still run the pinned source rather than your edited checkout. Replace production pins only through an agreed code release. See reviewed-source hotfixes.
Shared proxy and HTTPS
NPM attaches to clone ingress networks and routes hostnames to per-instance aliases. Application networks and data volumes remain separate. Application containers do not need public host ports. Shared proxy state survives application stop.
Example aliases for instance beni:
| Browser hostname | NPM target | Internal port |
|---|---|---|
bff.beni.ecas.local | beni-bff | 5173 |
kafka.beni.ecas.local | beni-kafka | 8080 |
seaweed.beni.ecas.local | beni-seaweed | 5600 |
docs.beni.ecas.local | beni-docs | 3000 |
prisma.beni.ecas.local | beni-prisma | 5555 |
These are examples. Use the generated URLs for your own instance, including Account and any selected FormGen host.
Automatic NPM setup
Proxy automation rechecks resources before creating hosts. It preserves existing hosts, certificates, access lists, and administrator credentials. NPM does not offer a batch transaction. If a later action fails, earlier created resources remain, with identifiers for recovery. A proxy failure is reported without destroying an otherwise running stack.
Locate certificates and trust them on clients
bash configureNpmProxyHosts.sh --show
This shows absolute certificate paths and client instructions without an NPM login or stack mutation. Generated certificates can reside in auto-* directories. Reused public certificates can export as .docker-state/certs/npm-ID.crt.
Trust the public certificate actually selected for your application's NPM host. Copy only .crt files to clients. Keep private .key files on the server.
From WSL, open the local certificate directory:
explorer.exe "$(wslpath -w "$PWD/.docker-state/certs")"
On Windows, copy the public certificate to Downloads as proxy.crt. In PowerShell:
certutil.exe -user -addstore Root "$env:USERPROFILE\Downloads\proxy.crt"
This trusts it for the current Windows user. On macOS, open Keychain Access → login → File → Import Items. Select the public certificate. Open its Trust section and set Secure Sockets Layer (SSL) → Always Trust. Authenticate if requested.
Restart the browser and open the full HTTPS URL, normally with :8443. --show prints instructions but does not modify a client trust store. Renew self-signed certificates before expiry. Preserve their files and shared NPM state for recovery.
The standard Force SSL redirect can discard the nonstandard HTTPS port and send clients to 443. Keep it disabled unless a checked redirect preserves the actual port. Register the exact Account OAuth callback separately. Certificate trust does not replace callback registration. Never disable application TLS checks to repair trust.
WebSockets and cookie synchronization
Automatic setup applies docker/proxy/websocket.conf.example with the clone's BFF alias. It covers /ws and /ws/.... Do not add duplicate GUI/Advanced locations beside it.
The main BFF route targets 5173 for pages and Next HMR. The WebSocket route targets 6589. Remove /ws exactly once without an HTTP redirect. Preserve $http_host including :8443, browser Origin, cookies, and Upgrade/Connection headers.
Equivalent manual routing for the example instance:
location = /ws {
proxy_pass http://beni-bff:6589/;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
}
location /ws/ {
proxy_pass http://beni-bff:6589/;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
}
The browser uses wss://bff.beni.ecas.local:8443/ws. Cookie synchronization uses /ws/zilaws/cookieSync, which must reach internal /zilaws/cookieSync. Check authenticated RPC, cookie sync, reconnect, session expiry, and provider reconnection. A 101 response alone is insufficient.
If docs-mcp is selected, route docs /mcp/ to beni-docs-mcp:3456 and remove the prefix once. Configure the actual public MCP URL. Leave its control disabled if no endpoint runs.
Public Graph webhook ingress
Complete the Cloudflare ingress prerequisites and ownership backup before startup. Use this developer's own public HTTPS domain/tunnel for both Graph callbacks. Route them to WebhookCatcher, not IUDG. Keep GRAPH_WEBHOOK_CLIENT_STATE stable. Microsoft cannot call localhost. See Graph keys and certificates.
For a failed subscription challenge, check public DNS, TLS, proxy routing, and WebhookCatcher logs without exposing the validation secret.
Memory, isolation, and data preservation
Supported ECS_MEMORY_GIB profiles are 8, 12, 16, 30, 35, 40, and 44. Omission selects 30. The selected services determine required minimums. A profile below those minimums stops startup. Full standard selection uses 40 GiB, and full Google/Microsoft/FormGen uses 44 GiB. Host/build reserves are additional.
bounded applies service limits. hybrid needs Docker systemd cgroups v2 and noninteractive sudo. ECS_SWAP_GIB configures additional swap budget. See environment reference.
Containers share a configurable set of up to 20 logical CPUs by default. Omitted runtime/build lists derive from Docker-visible CPUs independently. Explicit lists must contain existing CPUs. CPU, RAM, and swap remain shared across clones.
Unique instance IDs isolate networks/volumes and ownership. Reusing another clone's identity fails ownership checks. It does not make two complete cold starts safe. A service cgroup-memory failure needs a scoped limit correction/restart. Host-wide RAM/swap exhaustion needs host evidence. Docker OOMKilled=true alone is insufficient.
Normal stop/restart preserves volumes. Project reset removes a defined subset of owned data volumes. Read the exact reset boundary. Bare down -v has a different, broader scope. Remote placement rejects reset/cleanup even in simulation. Do not change placement to bypass the guard. Keep configuration, database, object-store, and encryption-key backups separately.
Troubleshooting
| Symptom | Check |
|---|---|
Container is Up, application unavailable | Service readiness and actual worker/application process |
502 after recreation | Ingress network, alias, readiness, and proxy upstream DNS refresh |
Login returns to 443 | Exact public origin and a redirect that preserves :8443 |
| Quick start rejects stale state | Ordinary restart with correct source/configuration, not fingerprint edits |
| Graph sync stalls | Provider setup, partition assignment, webhook lifecycle, delta projection, and S3 access |
| Memory failure | Selected profile, actual host/Docker headroom, and service versus host OOM evidence |
Before delivery, check the affected browser flow, WebSocket/cookie path, file access, and data preservation on the actual supported machine. Configuration validation alone does not establish these results.