Something went wrong. Try again.
math snipping extension
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106# syntax=docker/dockerfile:1## Two images out of one tree, because the two halves of this service have# nothing in common but the code.## converter sympy, antlr and fastapi. Small, starts instantly, and is the# part that has to be up whenever you snip.# ocr the same code plus a model and torch, which is gigabytes and# wants a machine with room.## Splitting them means the always-on part is the cheap part, the expensive part# can be moved, restarted or switched off without taking convert and hint down# with it, and neither needs redeploying when the other changes. They meet at# one route: POST /v1/ocr, image in and LaTeX out.## Each half is built with uv and then *left behind*: the runtime stages start# from plain python and receive only the virtualenv. A build toolchain in a# deployed image is a couple of hundred megabytes of attack surface that never# runs.
# ---- building ----------------------------------------------------------------
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS build
ENV UV_COMPILE_BYTECODE=1 \ UV_LINK_MODE=copy \ UV_PYTHON_DOWNLOADS=never
WORKDIR /appCOPY pyproject.toml uv.lock README.md ./COPY src ./src
FROM build AS build-converterRUN --mount=type=cache,target=/root/.cache/uv \ uv sync --frozen --no-dev --group service
FROM build AS build-ocrRUN --mount=type=cache,target=/root/.cache/uv \ uv sync --frozen --no-dev --group service --group ocr
# ---- running -----------------------------------------------------------------
FROM python:3.12-slim-bookworm AS runtime
ENV PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 \ PATH="/app/.venv/bin:$PATH"
WORKDIR /app# The project is installed into the venv as a link back to /app/src, so the# source has to land at the same path it had at build time.COPY src ./src
RUN useradd --system --create-home --uid 999 integrandEXPOSE 8765CMD ["uvicorn", "integrand.service:app", "--host", "0.0.0.0", "--port", "8765"]
FROM runtime AS converter
COPY --from=build-converter /app/.venv /app/.venv
# The page and its download live only here — the OCR image answers /v1/ocr and# nothing else. Copied after the venv so editing the page rebuilds one small# layer rather than everything above it.COPY landing ./landingCOPY extension ./extensionRUN python -c "import shutil; shutil.make_archive('/app/landing/integrand', 'zip', '.', 'extension')"
# Talks to the other image by default. Point INTEGRAND_OCR_URL wherever it# lives, or set INTEGRAND_OCR=symbolab to do without one entirely.ENV INTEGRAND_OCR=remote \ INTEGRAND_OCR_URL=http://ocr:8765/v1/ocr
USER integrand
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ CMD python -c "import urllib.request as u; u.urlopen('http://127.0.0.1:8765/healthz', timeout=3)"
FROM runtime AS ocr
COPY --from=build-ocr /app/.venv /app/.venv
ENV INTEGRAND_OCR=pix2tex \ HF_HOME=/home/integrand/.cache/huggingface
# Pull the weights at build time. Downloading them on first use instead means# the first snip after every deploy is the slow one, and a box with no outbound# network is one that never works but looks healthy until someone uses it.## Before dropping privileges, because pix2tex writes the checkpoint inside its# own site-packages directory rather than a cache dir. Root writes it once at# build; the runtime user only ever reads it.RUN python -c "from pix2tex.cli import LatexOCR; LatexOCR()" && echo "weights cached"
USER integrand
# Loading the model is slow enough that a short start period would restart a# container that was only still starting.HEALTHCHECK --interval=30s --timeout=10s --start-period=180s --retries=3 \ CMD python -c "import urllib.request as u; u.urlopen('http://127.0.0.1:8765/healthz', timeout=5)"