Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 102 additions & 3 deletions Docker/deployment.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# Optics Framework API Deployment Guide


This guide outlines how to deploy the Optics API using Docker in two modes:
This guide outlines how to deploy the Optics API and MCP server using Docker:

1. **Production**: Uses the published `optics-framework` from PyPI (see `prod/Dockerfile`)
2. **Development**: Uses a locally built `.whl` package from the Poetry-managed `optics-framework` source (see `dev/Dockerfile`)
1. **Production (REST API)**: Uses the published `optics-framework` from PyPI (see `prod/Dockerfile`)
2. **Development (REST API)**: Uses a locally built `.whl` package from the Poetry-managed `optics-framework` source (see `dev/Dockerfile`)
3. **MCP (HTTP transport)**: Uses separate images under `mcp/` that install `optics-framework[mcp]` and run `optics mcp --transport http` on port **8090**

---

Expand Down Expand Up @@ -100,3 +101,101 @@ If running Appium on your host machine, use this URL in your config:
```
appium_url: "http://host.docker.internal:4723"
```

---

## ✅ Deployment Mode 3: MCP Server (HTTP transport)

The MCP images expose the optics keyword engine over the [Model Context Protocol](https://modelcontextprotocol.io) at `http://<host>:8090/mcp`. Containerized MCP always uses **HTTP transport** (not stdio). Sessions are **not shared** with `optics serve` even if both containers run.

### 📁 Folder Structure
```
mcp/
├── prod/
│ └── Dockerfile
└── dev/
└── Dockerfile
```

### Production MCP (PyPI)

#### Build
```sh
cd /path/to/optics-framework
docker build -f Docker/mcp/prod/Dockerfile -t optics-mcp-prod .
```

#### Run
```sh
docker run -d -p 8090:8090 --name optics-mcp-prod optics-mcp-prod
```

#### Vision Backend Selection
Same as the REST API images: use `--build-arg VISION_BACKEND=...` (`easyocr`, `google-vision`, or `pytesseract`).

Example (Google Vision):
```sh
docker build -f Docker/mcp/prod/Dockerfile \
--build-arg VISION_BACKEND=google-vision \
-t optics-mcp-prod .
```

If using Google Vision, mount your service account JSON and set the env variable:
```sh
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
```

### Development MCP (Local .whl)

Build the wheel first (`poetry build` from the repo root), then:

```sh
docker build -f Docker/mcp/dev/Dockerfile \
--build-arg WHL_FILE=optics_framework-0.x.x-py3-none-any.whl \
-t optics-mcp-dev .
```

```sh
docker run -d -p 8091:8090 --name optics-mcp-dev optics-mcp-dev
```

### Docker Compose

From the repo root:

```sh
# Production MCP on host port 8090
docker compose -f Docker/docker-compose.yml up --build mcp

# Development MCP on host port 8091
docker compose -f Docker/docker-compose.yml up --build mcp-dev
```

### Connect an MCP client

Point your MCP client at the container's HTTP endpoint:

```json
{
"mcpServers": {
"optics": { "url": "http://127.0.0.1:8090/mcp" }
}
}
```

Use port **8091** when running the `mcp-dev` compose service.

#### Appium from inside the container

When `start_session` targets Appium on the host machine:

```json
{
"driver": "appium",
"url": "http://host.docker.internal:4723",
"capabilities": { "...": "..." }
}
```
30 changes: 25 additions & 5 deletions Docker/dev/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,23 @@
curl \
libgl1 \
libglib2.0-0 \
xvfb \
dbus-x11 \
tesseract-ocr \
&& rm -rf /var/lib/apt/lists/*

# Install common Python packages and optics-framework wheel and vision backend
RUN pip install --no-cache-dir \
appium-python-client \
&& pip install --no-cache-dir "/app/${WHL_FILE}" \
appium-python-client playwright \
&& playwright install-deps chromium firefox \
&& WHEEL=$(if [ -n "$WHL_FILE" ]; then echo "/app/${WHL_FILE}"; else ls /app/*.whl 2>/dev/null | head -1; fi) \
&& if [ -z "$WHEEL" ] || [ ! -f "$WHEEL" ]; then \
echo "ERROR: No wheel in dist/. Run 'poetry build' first or set WHL_FILE to the .whl filename."; \
exit 1; \
fi \
&& pip install --no-cache-dir "$WHEEL" \
&& if [ "$VISION_BACKEND" = "easyocr" ]; then \
pip install --no-cache-dir easyocr && \
python3 -c "import easyocr; easyocr.Reader(['en'], download_enabled=True)"; \
pip install --no-cache-dir easyocr; \
elif [ "$VISION_BACKEND" = "google-vision" ]; then \
pip install --no-cache-dir google-cloud-vision; \
elif [ "$VISION_BACKEND" = "pytesseract" ]; then \
Expand All @@ -49,10 +57,22 @@
# Switch to non-root user
USER appuser

# Pre-download EasyOCR models as appuser (must match runtime user's ~/.EasyOCR/)
RUN if [ "$VISION_BACKEND" = "easyocr" ]; then \

Check warning on line 61 in Docker/dev/Dockerfile

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Merge this RUN instruction with the consecutive ones.

See more on https://sonarcloud.io/project/issues?id=mozarkai_optics-framework&issues=AZ8ZyPBWL4hp_cKEsxRf&open=AZ8ZyPBWL4hp_cKEsxRf&pullRequest=322
python3 -c "import easyocr; easyocr.Reader(['en'], download_enabled=True)"; \
fi

# Install Playwright browsers (Chromium and Firefox) as appuser (must be done as the user who will run the app)
RUN playwright install chromium firefox

EXPOSE 8000

# Allow dynamic worker count via UVICORN_WORKERS env variable (default 1)
ENV UVICORN_WORKERS=1

# Set DISPLAY for X11 (xvfb will provide virtual display)
ENV DISPLAY=:99

# Use exec form for CMD with shell for env var substitution
CMD ["/bin/sh", "-c", "exec optics serve --host 0.0.0.0 --port 8000 --workers \"${UVICORN_WORKERS:-1}\""]
# Start xvfb in the background, then run optics serve
CMD ["/bin/sh", "-c", "Xvfb :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & exec optics serve --host 0.0.0.0 --port 8000 --workers \"${UVICORN_WORKERS:-1}\""]
45 changes: 45 additions & 0 deletions Docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,52 @@ services:
timeout: 10s
retries: 5

mcp:
build:
context: ..
dockerfile: Docker/mcp/prod/Dockerfile
image: optics/mcp:prod
container_name: optics_mcp_prod
ports:
- "8090:8090"
restart: unless-stopped
environment:
- PYTHONUNBUFFERED=1
healthcheck:
test: ["CMD-SHELL", "python -c \"import socket; s=socket.create_connection(('127.0.0.1',8090),2); s.close()\""]
interval: 45s
timeout: 10s
retries: 5
deploy:
resources:
limits:
cpus: '1.0'
memory: 1024M

mcp-dev:
build:
context: ..
dockerfile: Docker/mcp/dev/Dockerfile
args:
# pass WHL_FILE if you want to install from a specific wheel in dist/
WHL_FILE: ""
Comment thread
malto101 marked this conversation as resolved.
image: optics/mcp:dev
container_name: optics_mcp_dev
ports:
- "8091:8090" # maps host 8091 -> container 8090 so prod MCP / mock OCR don't conflict
volumes:
- ..:/app:cached
environment:
- PYTHONUNBUFFERED=1
healthcheck:
test: ["CMD-SHELL", "python -c \"import socket; s=socket.create_connection(('127.0.0.1',8090),2); s.close()\""]
interval: 45s
timeout: 10s
retries: 5

# Usage examples:
# docker compose -f Docker/docker-compose.yml up --build app
# docker compose -f Docker/docker-compose.yml up --build dev
# docker compose -f Docker/docker-compose.yml up --build mcp
# docker compose -f Docker/docker-compose.yml up --build mcp-dev
# docker compose -f Docker/docker-compose.yml up --build
77 changes: 77 additions & 0 deletions Docker/mcp/dev/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# syntax=docker/dockerfile:1

FROM python:3.12-slim

# Create a non-root user
RUN useradd --create-home --shell /bin/bash appuser

WORKDIR /app

# Accept the .whl filename as a build argument
ARG WHL_FILE
ARG VISION_BACKEND=easyocr

ENV VISION_BACKEND=${VISION_BACKEND}
ENV WHL_FILE=${WHL_FILE}

# Copy wheel(s) from dist/ (empty WHL_FILE copies all of dist/, matching Docker/dev/Dockerfile)
COPY dist/${WHL_FILE} /app/


# Install system dependencies (first layer for cache)
RUN apt-get update && apt-get install --no-install-recommends -y \
build-essential \
curl \
libgl1 \
libglib2.0-0 \
xvfb \
dbus-x11 \
tesseract-ocr \
&& rm -rf /var/lib/apt/lists/*

# Install common Python packages and optics-framework wheel with MCP extra and vision backend
RUN pip install --no-cache-dir \
appium-python-client playwright \
&& playwright install-deps chromium firefox \
&& WHEEL=$(if [ -n "$WHL_FILE" ]; then echo "/app/${WHL_FILE}"; else ls /app/*.whl 2>/dev/null | head -1; fi) \
&& if [ -z "$WHEEL" ] || [ ! -f "$WHEEL" ]; then \
echo "ERROR: No wheel in dist/. Run 'poetry build' first or set WHL_FILE to the .whl filename."; \
exit 1; \
fi \
&& pip install --no-cache-dir "${WHEEL}[mcp]" \
&& if [ "$VISION_BACKEND" = "easyocr" ]; then \
pip install --no-cache-dir easyocr; \
elif [ "$VISION_BACKEND" = "google-vision" ]; then \
pip install --no-cache-dir google-cloud-vision; \
elif [ "$VISION_BACKEND" = "pytesseract" ]; then \
pip install --no-cache-dir pytesseract; \
fi

# If using google-vision, user must mount service account json and set GOOGLE_APPLICATION_CREDENTIALS
# Example: docker run -e GOOGLE_APPLICATION_CREDENTIALS=/app/service-account.json -v /path/to/service-account.json:/app/service-account.json ...

# Set permissions
RUN chown -R appuser:appuser /app

# Switch to non-root user
USER appuser

# Pre-download EasyOCR models as appuser (must match runtime user's ~/.EasyOCR/)
RUN if [ "$VISION_BACKEND" = "easyocr" ]; then \

Check warning on line 60 in Docker/mcp/dev/Dockerfile

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Merge this RUN instruction with the consecutive ones.

See more on https://sonarcloud.io/project/issues?id=mozarkai_optics-framework&issues=AZ8ZyPBAL4hp_cKEsxRe&open=AZ8ZyPBAL4hp_cKEsxRe&pullRequest=322
python3 -c "import easyocr; easyocr.Reader(['en'], download_enabled=True)"; \
fi

# Install Playwright browsers (Chromium and Firefox) as appuser (must be done as the user who will run the app)
RUN playwright install chromium firefox

EXPOSE 8090

# Allow dynamic port via MCP_PORT env variable (default 8090)
ENV MCP_PORT=8090

# Set DISPLAY for X11 (xvfb will provide virtual display)
ENV DISPLAY=:99

# Use exec form for CMD with shell for env var substitution
# Start xvfb in the background, then run optics mcp over HTTP
CMD ["/bin/sh", "-c", "Xvfb :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & exec optics mcp --transport http --host 0.0.0.0 --port \"${MCP_PORT:-8090}\""]
69 changes: 69 additions & 0 deletions Docker/mcp/prod/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# syntax=docker/dockerfile:1
FROM python:3.12-slim

# Create a non-root user
RUN useradd --create-home --shell /bin/bash appuser

# Set working directory
WORKDIR /app

# Build arguments for vision backend
ARG VISION_BACKEND=easyocr
ENV VISION_BACKEND=${VISION_BACKEND}


# Install system dependencies (first layer for cache)
RUN apt-get update && apt-get install --no-install-recommends -y \
build-essential \
curl \
libgl1 \
libglib2.0-0 \
xvfb \
dbus-x11 \
tesseract-ocr \
&& rm -rf /var/lib/apt/lists/*

RUN pip install --no-cache-dir uv \
&& uv pip install --system appium-python-client playwright \
&& playwright install-deps chromium firefox

# Install optics-framework with MCP extra and vision backend (late layer for cache efficiency)
RUN uv pip install --system "optics-framework[mcp]" \
&& if [ "$VISION_BACKEND" = "easyocr" ]; then \
uv pip install --system easyocr; \
elif [ "$VISION_BACKEND" = "google-vision" ]; then \
uv pip install --system google-cloud-vision; \
elif [ "$VISION_BACKEND" = "pytesseract" ]; then \
Comment thread
malto101 marked this conversation as resolved.
uv pip install --system pytesseract; \
fi

# If using google-vision, user must mount service account json and set GOOGLE_APPLICATION_CREDENTIALS
# Example: docker run -e GOOGLE_APPLICATION_CREDENTIALS=/app/service-account.json -v /path/to/service-account.json:/app/service-account.json ...

# Set permissions
RUN chown -R appuser:appuser /app

# Switch to non-root user to install Playwright browsers in user's cache
USER appuser

# Pre-download EasyOCR models as appuser (must match runtime user's ~/.EasyOCR/)
RUN if [ "$VISION_BACKEND" = "easyocr" ]; then \

Check warning on line 50 in Docker/mcp/prod/Dockerfile

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Merge this RUN instruction with the consecutive ones.

See more on https://sonarcloud.io/project/issues?id=mozarkai_optics-framework&issues=AZ8ZyPAzL4hp_cKEsxRd&open=AZ8ZyPAzL4hp_cKEsxRd&pullRequest=322
python3 -c "import easyocr; easyocr.Reader(['en'], download_enabled=True)"; \
fi

# Install Playwright browsers (Chromium and Firefox) as appuser (must be done as the user who will run the app)
RUN playwright install chromium firefox

# Note: We stay as appuser for the rest of the Dockerfile

EXPOSE 8090

# Allow dynamic port via MCP_PORT env variable (default 8090)
ENV MCP_PORT=8090

# Set DISPLAY for X11 (xvfb will provide virtual display)
ENV DISPLAY=:99

# Use exec form for CMD with shell for env var substitution
# Start xvfb in the background, then run optics mcp over HTTP
CMD ["/bin/sh", "-c", "Xvfb :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & exec optics mcp --transport http --host 0.0.0.0 --port \"${MCP_PORT:-8090}\""]
9 changes: 7 additions & 2 deletions Docker/prod/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
libglib2.0-0 \
xvfb \
dbus-x11 \
tesseract-ocr \
&& rm -rf /var/lib/apt/lists/*

RUN pip install --no-cache-dir uv \
Expand All @@ -29,8 +30,7 @@
# Install optics-framework and vision backend (late layer for cache efficiency)
RUN uv pip install --system optics-framework \
&& if [ "$VISION_BACKEND" = "easyocr" ]; then \
uv pip install --system easyocr && \
python3 -c "import easyocr; easyocr.Reader(['en'], download_enabled=True)"; \
uv pip install --system easyocr; \
elif [ "$VISION_BACKEND" = "google-vision" ]; then \
uv pip install --system google-cloud-vision; \
elif [ "$VISION_BACKEND" = "pytesseract" ]; then \
Expand All @@ -46,6 +46,11 @@
# Switch to non-root user to install Playwright browsers in user's cache
USER appuser

# Pre-download EasyOCR models as appuser (must match runtime user's ~/.EasyOCR/)
RUN if [ "$VISION_BACKEND" = "easyocr" ]; then \

Check warning on line 50 in Docker/prod/Dockerfile

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Merge this RUN instruction with the consecutive ones.

See more on https://sonarcloud.io/project/issues?id=mozarkai_optics-framework&issues=AZ8ZyO9fL4hp_cKEsxRc&open=AZ8ZyO9fL4hp_cKEsxRc&pullRequest=322
python3 -c "import easyocr; easyocr.Reader(['en'], download_enabled=True)"; \
fi

# Install Playwright browsers (Chromium and Firefox) as appuser (must be done as the user who will run the app)
RUN playwright install chromium firefox

Expand Down
Loading
Loading