docs/usage/docker_deployment.md (new file)
docs/usage/docker_deployment.md @ PR head
tt1# Docker Deployment
2
3Optics ships container images for its two long-running servers — the REST API
4(`optics serve`) and the MCP server (`optics mcp`). The Dockerfiles and the
5Compose file live in the repository under
6[`Docker/`](https://github.com/mozarkai/optics-framework/tree/main/Docker); this
7page is the guide to building and running them.
8
9## The four images
10
11Each server has a **production** image that installs a released
12`optics-framework` from PyPI and a **development** image that installs a wheel
13you built locally with `poetry build`.
14
15| Dockerfile | Serves | Runs | Container port |
16|---|---|---|---|
17| [`Docker/prod/Dockerfile`](https://github.com/mozarkai/optics-framework/blob/main/Docker/prod/Dockerfile) | REST API, from PyPI | `optics serve` | 8000 |
18| [`Docker/dev/Dockerfile`](https://github.com/mozarkai/optics-framework/blob/main/Docker/dev/Dockerfile) | REST API, from a local wheel | `optics serve` | 8000 |
19| [`Docker/mcp/prod/Dockerfile`](https://github.com/mozarkai/optics-framework/blob/main/Docker/mcp/prod/Dockerfile) | MCP, from PyPI | `optics mcp --transport http` | 8090 |
20| [`Docker/mcp/dev/Dockerfile`](https://github.com/mozarkai/optics-framework/blob/main/Docker/mcp/dev/Dockerfile) | MCP, from a local wheel | `optics mcp --transport http` | 8090 |
21
22All four are `python:3.12-slim`, run as the non-root `appuser`, and pin
23`appium-python-client`, `playwright` and the vision backend to the versions in
24`poetry.lock` (read at build time by `scripts/lock_pins.py`). Playwright's
25Chromium and Firefox are installed into the image, and an `Xvfb` display is
26started on `:99` before the server, so headed browsers work inside the
27container.
28
29!!! warning "Build from the repository root"
30 Every image copies `poetry.lock` and `scripts/lock_pins.py`, so the build
31 context must be the repository root. Always build with `-f Docker/...` from
32 the root — `cd Docker/prod && docker build .` cannot work.
33
34## Prerequisites
35
36- Docker (Desktop, or Engine with the Compose plugin)
37- A clone of the repository, for the build context
38- Python 3.12+ and [Poetry](https://python-poetry.org/docs/), for the
39 development images only (to build the wheel)
40
41## Docker Compose
42
43[`Docker/docker-compose.yml`](https://github.com/mozarkai/optics-framework/blob/main/Docker/docker-compose.yml)
44defines one service per image, on distinct host ports so several can run at
45once. Run it from the repository root:
46
47| Service | Image | Host port | Build args needed |
48|---|---|---|---|
49| `app` | REST API, PyPI | 8000 | — |
50| `dev` | REST API, local wheel | 8001 | a wheel in `dist/` |
51| `mcp` | MCP, PyPI | 8090 | — |
52| `mcp-dev` | MCP, local wheel | 8091 | a wheel in `dist/` |
53
54```bash
55docker compose -f Docker/docker-compose.yml up --build app # REST API on :8000
56docker compose -f Docker/docker-compose.yml up --build mcp # MCP on :8090
57docker compose -f Docker/docker-compose.yml up --build mcp-dev # MCP on :8091
58```
59
60The `dev` and `mcp-dev` services bind-mount the repository at `/app`, so an
61edit to a test project on the host is visible inside the container. The `dev`
62service also overrides the image's start command, which means it starts
63`optics serve` without the `Xvfb` display — run the `app` service instead if
64your suite drives a headed browser.
65
66## Building by hand
67
68### REST API, production
69
70```bash
71docker build -f Docker/prod/Dockerfile -t optics-api-prod .
72docker run -d -p 8000:8000 --name optics-api-prod optics-api-prod
73```
74
75Which release it installs is the `OPTICS_FRAMEWORK_VERSION` build argument.
76Its default is a fixed version in the Dockerfile, not "the latest", so pass
77the version you actually want to deploy:
78
79```bash
80docker build -f Docker/prod/Dockerfile \
81 --build-arg OPTICS_FRAMEWORK_VERSION=1.10.4 \
82 -t optics-api-prod .
83```
84
85`optics serve` runs with a single worker by default. Raise it with the
86`UVICORN_WORKERS` environment variable at run time:
87
88```bash
89docker run -d -p 8000:8000 -e UVICORN_WORKERS=4 --name optics-api-prod optics-api-prod
90```
91
92### REST API, development wheel
93
94Build the wheel first — it lands in `dist/`, which is where the image expects
95it:
96
97```bash
98poetry build
99docker build -f Docker/dev/Dockerfile -t optics-api-dev .
100docker run -d -p 8000:8000 --name optics-api-dev optics-api-dev
101```
102
103With `dist/` holding more than one wheel, name the one you want with the
104`WHL_FILE` build argument; otherwise the build picks the first it finds:
105
106```bash
107docker build -f Docker/dev/Dockerfile \
108 --build-arg WHL_FILE=optics_framework-1.10.4-py3-none-any.whl \
109 -t optics-api-dev .
110```
111
112### MCP, production
113
114```bash
115docker build -f Docker/mcp/prod/Dockerfile -t optics-mcp-prod .
116docker run -d -p 8090:8090 --name optics-mcp-prod optics-mcp-prod
117```
118
119### MCP, development wheel
120
121```bash
122poetry build
123docker build -f Docker/mcp/dev/Dockerfile -t optics-mcp-dev .
124docker run -d -p 8091:8090 --name optics-mcp-dev optics-mcp-dev
125```
126
127Both MCP images install the `[mcp]` extra and bind to `0.0.0.0` inside the
128container. `MCP_PORT` changes the port they listen on (default 8090).
129
130## Choosing a vision backend
131
132All four images take a `VISION_BACKEND` build argument. The default is
133`easyocr`, whose models are pre-downloaded into the image at build time.
134
135| Value | Installs |
136|---|---|
137| `easyocr` (default) | `easyocr` |
138| `google-vision` | `google-cloud-vision` |
139| `pytesseract` | `pytesseract` (the Tesseract binary is already in the image) |
140
141```bash
142docker build -f Docker/mcp/prod/Dockerfile \
143 --build-arg VISION_BACKEND=google-vision \
144 -t optics-mcp-prod .
145```
146
147Google Vision reads its credentials from the environment, so mount the service
148account JSON and point `GOOGLE_APPLICATION_CREDENTIALS` at it:
149
150```bash
151docker run -d -p 8090:8090 \
152 -e GOOGLE_APPLICATION_CREDENTIALS=/app/service-account.json \
153 -v /path/to/service-account.json:/app/service-account.json \
154 --name optics-mcp-prod optics-mcp-prod
155```
156
157!!! warning "Never bake credentials into an image"
158 Mount the service account file at run time. A `COPY` of it into the image
159 leaves the key in a layer that travels with every copy of that image.
160
161## Reaching Appium from inside a container
162
163An Appium server running on the host is not on `localhost` from the
164container's point of view. Use `host.docker.internal` instead — in a project's
165`config.yaml`:
166
167```yaml
168driver_sources:
169 - appium:
170 enabled: true
171 url: "http://host.docker.internal:4723"
172```
173
174or in an MCP `start_session` call:
175
176```json
177{
178 "driver": "appium",
179 "url": "http://host.docker.internal:4723",
180 "capabilities": { "...": "..." }
181}
182```
183
184Docker Desktop resolves that name for you. On Linux it does not exist unless
185you add it, and none of the Compose services declares it — so pass it to
186`docker run`:
187
188```bash
189docker run -d -p 8000:8000 \
190 --add-host=host.docker.internal:host-gateway \
191 --name optics-api-prod optics-api-prod
192```
193
194or layer it onto Compose with a second file:
195
196```yaml
197# Docker/docker-compose.host.yml
198services:
199 app:
200 extra_hosts:
201 - "host.docker.internal:host-gateway"
202```
203
204```bash
205docker compose -f Docker/docker-compose.yml -f Docker/docker-compose.host.yml up app
206```
207
208## Connecting a client
209
210The REST API answers on `http://<host>:8000`, with `/health` for liveness (the
211Compose healthcheck uses it) and FastAPI's generated OpenAPI browser at
212`/docs`. See [REST API Usage](REST_API_usage.md).
213
214!!! warning "The API is unauthenticated"
215 Neither server ships authentication, and both bind to `0.0.0.0` inside the
216 container. Publish their ports only on a trusted network, or put a
217 reverse proxy that authenticates in front.
218
219A containerized MCP server always speaks **HTTP transport** — `stdio` is for
220local clients that spawn the process themselves. Point your client at the
221container:
222
223```json
224{
225 "mcpServers": {
226 "optics": { "url": "http://127.0.0.1:8090/mcp" }
227 }
228}
229```
230
231Use port **8091** for the `mcp-dev` Compose service. See
232[MCP Usage](mcp_usage.md).
233
234!!! note "Sessions are not shared"
235 `optics serve` and `optics mcp` are separate processes with separate
236 in-memory session managers, whether or not they run in the same Compose
237 project. A session started against one is invisible to the other, and
238 restarting a container clears its sessions.
Legends
Colors
Added
Changed
Deleted
Links
(f)irst change
(n)ext change
(t)op