|
| 1 | +# Multi-instance manager |
| 2 | + |
| 3 | +External helper for advanced users who want to run multiple isolated Docker containers from a pool of saved auth profiles. |
| 4 | + |
| 5 | +## What it does |
| 6 | + |
| 7 | +The manager launches one container per auth profile from [`auth_profiles/saved/`](../../auth_profiles/saved/). Each container gets: |
| 8 | + |
| 9 | +- its own host API port |
| 10 | +- its own host stream port |
| 11 | +- its own dedicated startup profile via [`ACTIVE_AUTH_JSON_PATH`](../../launcher/runner.py) |
| 12 | +- the same Docker image and shared API key unless overridden |
| 13 | + |
| 14 | +By default, the manager enforces strict profile isolation at startup. A container starts with its matching file from [`auth_profiles/saved/`](../../auth_profiles/saved/) and does not auto-rotate into other profiles unless you explicitly enable that behavior. |
| 15 | + |
| 16 | +This component is intentionally isolated from the main application. It does not modify the core server code, the existing Docker setup, or the standard launch flow. |
| 17 | + |
| 18 | +## Resource warning |
| 19 | + |
| 20 | +> ⚠️ Each instance can consume around 1 GB RAM or more. |
| 21 | +> Running many containers at once may cause memory pressure or OOM kills. |
| 22 | +> Review available system memory before launching a large profile pool. |
| 23 | +
|
| 24 | +The script shows a visible warning and asks for confirmation before startup unless auto-confirm is enabled. |
| 25 | + |
| 26 | +## Requirements |
| 27 | + |
| 28 | +- Docker installed and running |
| 29 | +- Bash 3.2+ on macOS or a newer Bash on Linux |
| 30 | +- built image available locally |
| 31 | + - default expected name: `ai-studio-proxy:latest` |
| 32 | + - if you built via `cd docker && docker compose build`, Docker Compose may create `docker-ai-studio-proxy:latest`; the launcher now detects this and falls back automatically when the default image is still configured |
| 33 | + - if you use another tag, pass it explicitly with `--image ...` or set `IMAGE_NAME=...` |
| 34 | +- prepared auth files in [`auth_profiles/saved/`](../../auth_profiles/saved/) |
| 35 | +- optional [`docker/.env`](../../docker/.env) file if you want containers to inherit the usual Docker configuration |
| 36 | + |
| 37 | +## Files |
| 38 | + |
| 39 | +- [`run-multi-instance.sh`](run-multi-instance.sh) — launcher script |
| 40 | +- [`README.md`](README.md) — usage guide |
| 41 | + |
| 42 | +## Startup contract |
| 43 | + |
| 44 | +The launcher now follows a strict startup contract: |
| 45 | + |
| 46 | +1. It mounts the shared [`auth_profiles/`](../../auth_profiles/) tree into the container. |
| 47 | +2. It sets `ACTIVE_AUTH_JSON_PATH=/app/auth_profiles/saved/<profile>.json` for that specific container. |
| 48 | +3. It copies the selected host profile into [` .multi-instance-runtime/active/`](../../.multi-instance-runtime/) and bind-mounts that copy as `/app/auth_profiles/active/<profile>.json` inside the container. |
| 49 | +4. It disables `AUTO_AUTH_ROTATION_ON_STARTUP` and `AUTO_ROTATE_AUTH_PROFILE` by default. |
| 50 | + |
| 51 | +This dual-path startup is intentional. Although [`launcher/runner.py::_resolve_auth_file_path()`](../../launcher/runner.py:270) and [`browser_utils/initialization/core.py::initialize_page_logic()`](../../browser_utils/initialization/core.py:48) can consume [`ACTIVE_AUTH_JSON_PATH`](../../launcher/runner.py:413), real Docker headless startup may still traverse logic that expects at least one file under [`auth_profiles/active/`](../../auth_profiles/active/). The runtime copy avoids polluting the shared host [`auth_profiles/active/`](../../auth_profiles/active/) pool while still satisfying that container-local requirement. |
| 52 | + |
| 53 | +If you really want cross-profile rotation inside each container, use [`--enable-auth-rotation`](run-multi-instance.sh). |
| 54 | + |
| 55 | +## Quick start |
| 56 | + |
| 57 | +Build the image first if needed: |
| 58 | + |
| 59 | +```bash |
| 60 | +cd docker |
| 61 | +docker compose build |
| 62 | +``` |
| 63 | + |
| 64 | +After a regular Compose build, the local image is often named `docker-ai-studio-proxy:latest`. The manager still starts from its default `ai-studio-proxy:latest`, but now automatically falls back to `docker-ai-studio-proxy:latest` when that Compose image exists and no custom image override was requested. |
| 65 | + |
| 66 | +Then run the manager from the repository root: |
| 67 | + |
| 68 | +```bash |
| 69 | +bash scripts/multi-instance-manager/run-multi-instance.sh |
| 70 | +``` |
| 71 | + |
| 72 | +## Common examples |
| 73 | + |
| 74 | +Use defaults: |
| 75 | + |
| 76 | +```bash |
| 77 | +bash scripts/multi-instance-manager/run-multi-instance.sh |
| 78 | +``` |
| 79 | + |
| 80 | +Use another image and custom container prefix: |
| 81 | + |
| 82 | +```bash |
| 83 | +bash scripts/multi-instance-manager/run-multi-instance.sh \ |
| 84 | + --image docker-ai-studio-proxy:latest \ |
| 85 | + --container-prefix team-a |
| 86 | +``` |
| 87 | + |
| 88 | +Use an explicit image override via environment variable: |
| 89 | + |
| 90 | +```bash |
| 91 | +IMAGE_NAME=docker-ai-studio-proxy:latest \ |
| 92 | + bash scripts/multi-instance-manager/run-multi-instance.sh |
| 93 | +``` |
| 94 | + |
| 95 | +Shift ports upward to avoid collisions: |
| 96 | + |
| 97 | +```bash |
| 98 | +bash scripts/multi-instance-manager/run-multi-instance.sh \ |
| 99 | + --base-api-port 3048 \ |
| 100 | + --base-stream-port 4120 |
| 101 | +``` |
| 102 | + |
| 103 | +Skip confirmation for automation: |
| 104 | + |
| 105 | +```bash |
| 106 | +bash scripts/multi-instance-manager/run-multi-instance.sh --auto-confirm |
| 107 | +``` |
| 108 | + |
| 109 | +Preview actions without launching containers: |
| 110 | + |
| 111 | +```bash |
| 112 | +bash scripts/multi-instance-manager/run-multi-instance.sh --dry-run |
| 113 | +``` |
| 114 | + |
| 115 | +Allow auth rotation inside each launched container: |
| 116 | + |
| 117 | +```bash |
| 118 | +bash scripts/multi-instance-manager/run-multi-instance.sh --enable-auth-rotation |
| 119 | +``` |
| 120 | + |
| 121 | +## Options |
| 122 | + |
| 123 | +| Option | Description | Default | |
| 124 | +| --- | --- | --- | |
| 125 | +| `--project-root PATH` | Repository root path | auto-detected | |
| 126 | +| `--profile-dir PATH` | Directory with saved auth profiles | `auth_profiles/saved` | |
| 127 | +| `--docker-env-file PATH` | Path to mounted Docker env file | `docker/.env` | |
| 128 | +| `--image NAME` | Docker image name | `ai-studio-proxy:latest` with automatic fallback to `docker-ai-studio-proxy:latest` after a standard `docker compose build` | |
| 129 | +| `--container-prefix PREFIX` | Prefix for launched containers | `ai-proxy` | |
| 130 | +| `--api-key KEY` | Shared API key for all containers | `sk-dummy` | |
| 131 | +| `--log-level LEVEL` | `SERVER_LOG_LEVEL` value | `INFO` | |
| 132 | +| `--base-api-port PORT` | Starting host API port | `2048` | |
| 133 | +| `--base-stream-port PORT` | Starting host stream port | `3120` | |
| 134 | +| `--memory-per-instance-gb N` | Estimated RAM per container | `1` | |
| 135 | +| `--auto-confirm` | Skip confirmation prompt | disabled | |
| 136 | +| `--dry-run` | Show commands without running Docker | disabled | |
| 137 | +| `--no-docker-env` | Do not mount [`docker/.env`](../../docker/.env) | disabled | |
| 138 | +| `--enable-auth-rotation` | Re-enable auth rotation inside launched containers | disabled | |
| 139 | +| `--help` | Show help | disabled | |
| 140 | + |
| 141 | +## Environment variables |
| 142 | + |
| 143 | +All major parameters can also be set with environment variables before launch: |
| 144 | + |
| 145 | +```bash |
| 146 | +export IMAGE_NAME=docker-ai-studio-proxy:latest |
| 147 | +export BASE_API_PORT=3048 |
| 148 | +export BASE_STREAM_PORT=4120 |
| 149 | +export AUTO_CONFIRM=true |
| 150 | +bash scripts/multi-instance-manager/run-multi-instance.sh |
| 151 | +``` |
| 152 | + |
| 153 | +When `IMAGE_NAME` or `--image` is set explicitly, the launcher treats that as an intentional override and does not silently replace it with the Compose image name. |
| 154 | + |
| 155 | +Supported variables: |
| 156 | + |
| 157 | +- `PROJECT_ROOT` |
| 158 | +- `PROFILE_DIR` |
| 159 | +- `DOCKER_ENV_FILE` |
| 160 | +- `IMAGE_NAME` |
| 161 | +- `CONTAINER_PREFIX` |
| 162 | +- `API_KEY` |
| 163 | +- `SERVER_LOG_LEVEL` |
| 164 | +- `BASE_API_PORT` |
| 165 | +- `BASE_STREAM_PORT` |
| 166 | +- `MEMORY_PER_INSTANCE_GB` |
| 167 | +- `AUTO_CONFIRM` |
| 168 | +- `DRY_RUN` |
| 169 | +- `MOUNT_DOCKER_ENV` |
| 170 | +- `DISABLE_AUTH_ROTATION` |
| 171 | +- `EXTRA_DOCKER_ARGS` |
| 172 | + |
| 173 | +## How the manager maps profiles |
| 174 | + |
| 175 | +If the saved pool contains: |
| 176 | + |
| 177 | +- `auth_profiles/saved/user-a.json` |
| 178 | +- `auth_profiles/saved/user-b.json` |
| 179 | + |
| 180 | +The script will launch containers similar to: |
| 181 | + |
| 182 | +- `ai-proxy-user-a` on API `2048` and stream `3120` |
| 183 | +- `ai-proxy-user-b` on API `2049` and stream `3121` |
| 184 | + |
| 185 | +Inside each container, startup resolves through two aligned paths: |
| 186 | + |
| 187 | +- `/app/auth_profiles/saved/user-a.json` and `/app/auth_profiles/active/user-a.json` |
| 188 | +- `/app/auth_profiles/saved/user-b.json` and `/app/auth_profiles/active/user-b.json` |
| 189 | + |
| 190 | +The saved-path mapping is still passed through `ACTIVE_AUTH_JSON_PATH`, and the same host file is also mounted into the container-local [`auth_profiles/active/`](../../auth_profiles/active/) directory. This keeps one-container-one-profile isolation while avoiding the Docker headless startup failure that reports no active profile. |
| 191 | + |
| 192 | +## Port collision behavior |
| 193 | + |
| 194 | +The launcher checks host ports twice: |
| 195 | + |
| 196 | +1. before removing a same-named container |
| 197 | +2. again after removal and before starting the replacement container |
| 198 | + |
| 199 | +This prevents a false success path where an old container was removed even though the target port actually belonged to another listener. |
| 200 | + |
| 201 | +If a host port is occupied by another process or container, the script stops before launching replacements. |
| 202 | + |
| 203 | +## Safety checks |
| 204 | + |
| 205 | +Before launch the script validates: |
| 206 | + |
| 207 | +- Docker CLI availability |
| 208 | +- Docker daemon availability |
| 209 | +- Docker image existence |
| 210 | +- profile directory existence |
| 211 | +- presence of `*.json` profiles |
| 212 | +- host port collisions for all planned instances |
| 213 | +- optional [`docker/.env`](../../docker/.env) existence when mounting is enabled |
| 214 | + |
| 215 | +## Stop and inspect containers |
| 216 | + |
| 217 | +List launched containers: |
| 218 | + |
| 219 | +```bash |
| 220 | +docker ps --filter "name=^ai-proxy-" |
| 221 | +``` |
| 222 | + |
| 223 | +Stop one container: |
| 224 | + |
| 225 | +```bash |
| 226 | +docker rm -f ai-proxy-user-a |
| 227 | +``` |
| 228 | + |
| 229 | +Stop all containers for the default prefix: |
| 230 | + |
| 231 | +```bash |
| 232 | +containers=$(docker ps -aq --filter "name=^ai-proxy-") |
| 233 | +[ -n "$containers" ] && docker rm -f $containers |
| 234 | +``` |
| 235 | + |
| 236 | +Check logs for one instance: |
| 237 | + |
| 238 | +```bash |
| 239 | +docker logs -f ai-proxy-user-a |
| 240 | +``` |
| 241 | + |
| 242 | +## Notes |
| 243 | + |
| 244 | +- The manager is intended for advanced operators. |
| 245 | +- It is an external wrapper, not a replacement for the standard single-instance flow. |
| 246 | +- For the normal Docker path, continue using [`docker/README.md`](../../docker/README.md) and [`docs/deployment-and-operations.md`](../../docs/deployment-and-operations.md). |
0 commit comments