Docker Deployment¶
Optics ships container images for its two long-running servers — the REST API (optics serve) and the MCP server (optics mcp). The Dockerfiles and the Compose file live in the repository under Docker/; this page is the guide to building and running them.
The four images¶
Each server has a production image that installs a released optics-framework from PyPI and a development image that installs a wheel you built locally with poetry build.
| Dockerfile | Serves | Runs | Container port |
|---|---|---|---|
Docker/prod/Dockerfile | REST API, from PyPI | optics serve | 8000 |
Docker/dev/Dockerfile | REST API, from a local wheel | optics serve | 8000 |
Docker/mcp/prod/Dockerfile | MCP, from PyPI | optics mcp --transport http | 8090 |
Docker/mcp/dev/Dockerfile | MCP, from a local wheel | optics mcp --transport http | 8090 |
All four are python:3.12-slim, run as the non-root appuser, and pin appium-python-client, playwright and the vision backend to the versions in poetry.lock (read at build time by scripts/lock_pins.py). Playwright's Chromium and Firefox are installed into the image, and an Xvfb display is started on :99 before the server, so headed browsers work inside the container.
Build from the repository root
Every image copies poetry.lock and scripts/lock_pins.py, so the build context must be the repository root. Always build with -f Docker/... from the root — cd Docker/prod && docker build . cannot work.
Prerequisites¶
- Docker (Desktop, or Engine with the Compose plugin)
- A clone of the repository, for the build context
- Python 3.12+ and Poetry, for the development images only (to build the wheel)
Docker Compose¶
Docker/docker-compose.yml defines one service per image, on distinct host ports so several can run at once. Run it from the repository root:
| Service | Image | Host port | Build args needed |
|---|---|---|---|
app | REST API, PyPI | 8000 | — |
dev | REST API, local wheel | 8001 | a wheel in dist/ |
mcp | MCP, PyPI | 8090 | — |
mcp-dev | MCP, local wheel | 8091 | a wheel in dist/ |
docker compose -f Docker/docker-compose.yml up --build app # REST API on :8000
docker compose -f Docker/docker-compose.yml up --build mcp # MCP on :8090
docker compose -f Docker/docker-compose.yml up --build mcp-dev # MCP on :8091
The dev and mcp-dev services bind-mount the repository at /app, so an edit to a test project on the host is visible inside the container. The dev service also overrides the image's start command, which means it starts optics serve without the Xvfb display — run the app service instead if your suite drives a headed browser.
Building by hand¶
REST API, production¶
docker build -f Docker/prod/Dockerfile -t optics-api-prod .
docker run -d -p 8000:8000 --name optics-api-prod optics-api-prod
Which release it installs is the OPTICS_FRAMEWORK_VERSION build argument. Its default is a fixed version in the Dockerfile, not "the latest", so pass the version you actually want to deploy:
docker build -f Docker/prod/Dockerfile \
--build-arg OPTICS_FRAMEWORK_VERSION=1.10.4 \
-t optics-api-prod .
optics serve runs with a single worker by default. Raise it with the UVICORN_WORKERS environment variable at run time:
REST API, development wheel¶
Build the wheel first — it lands in dist/, which is where the image expects it:
poetry build
docker build -f Docker/dev/Dockerfile -t optics-api-dev .
docker run -d -p 8000:8000 --name optics-api-dev optics-api-dev
With dist/ holding more than one wheel, name the one you want with the WHL_FILE build argument; otherwise the build picks the first it finds:
docker build -f Docker/dev/Dockerfile \
--build-arg WHL_FILE=optics_framework-1.10.4-py3-none-any.whl \
-t optics-api-dev .
MCP, production¶
docker build -f Docker/mcp/prod/Dockerfile -t optics-mcp-prod .
docker run -d -p 8090:8090 --name optics-mcp-prod optics-mcp-prod
MCP, development wheel¶
poetry build
docker build -f Docker/mcp/dev/Dockerfile -t optics-mcp-dev .
docker run -d -p 8091:8090 --name optics-mcp-dev optics-mcp-dev
Both MCP images install the [mcp] extra and bind to 0.0.0.0 inside the container. MCP_PORT changes the port they listen on (default 8090).
Choosing a vision backend¶
All four images take a VISION_BACKEND build argument. The default is easyocr, whose models are pre-downloaded into the image at build time.
| Value | Installs |
|---|---|
easyocr (default) | easyocr |
google-vision | google-cloud-vision |
pytesseract | pytesseract (the Tesseract binary is already in the image) |
docker build -f Docker/mcp/prod/Dockerfile \
--build-arg VISION_BACKEND=google-vision \
-t optics-mcp-prod .
Google Vision reads its credentials from the environment, so mount the service account JSON and point GOOGLE_APPLICATION_CREDENTIALS at it:
docker run -d -p 8090:8090 \
-e GOOGLE_APPLICATION_CREDENTIALS=/app/service-account.json \
-v /path/to/service-account.json:/app/service-account.json \
--name optics-mcp-prod optics-mcp-prod
Never bake credentials into an image
Mount the service account file at run time. A COPY of it into the image leaves the key in a layer that travels with every copy of that image.
Reaching Appium from inside a container¶
An Appium server running on the host is not on localhost from the container's point of view. Use host.docker.internal instead — in a project's config.yaml:
or in an MCP start_session call:
Docker Desktop resolves that name for you. On Linux it does not exist unless you add it, and none of the Compose services declares it — so pass it to docker run:
docker run -d -p 8000:8000 \
--add-host=host.docker.internal:host-gateway \
--name optics-api-prod optics-api-prod
or layer it onto Compose with a second file:
Connecting a client¶
The REST API answers on http://<host>:8000, with /health for liveness (the Compose healthcheck uses it) and FastAPI's generated OpenAPI browser at /docs. See REST API Usage.
The API is unauthenticated
Neither server ships authentication, and both bind to 0.0.0.0 inside the container. Publish their ports only on a trusted network, or put a reverse proxy that authenticates in front.
A containerized MCP server always speaks HTTP transport — stdio is for local clients that spawn the process themselves. Point your client at the container:
Use port 8091 for the mcp-dev Compose service. See MCP Usage.
Sessions are not shared
optics serve and optics mcp are separate processes with separate in-memory session managers, whether or not they run in the same Compose project. A session started against one is invisible to the other, and restarting a container clears its sessions.