diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8486afe --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +.git +.idea +.pytest_cache +.ruff_cache +__pycache__ +*.py[cod] +*.iml +.env +.DS_Store +build +dist +out +target +graphify-out diff --git a/Dockerfile b/Dockerfile index 1f99143..72b47ef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,42 +1,85 @@ -# Use a base image with CUDA support and the desired Python version -FROM python:3.12-slim-bookworm +FROM python:3.12-slim-bookworm AS builder + +ARG CPU_ONLY=true +ARG PRELOAD_MODELS=true +ARG POETRY_VERSION=1.8.4 +ARG VIRTUAL_ENV=/opt/venv + +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 \ + PIP_NO_CACHE_DIR=1 \ + PIP_ROOT_USER_ACTION=ignore \ + VIRTUAL_ENV="${VIRTUAL_ENV}" \ + POETRY_VIRTUALENVS_CREATE=false \ + HF_HOME=/models/huggingface \ + TORCH_HOME=/models/torch \ + OMP_NUM_THREADS=4 \ + PATH="${VIRTUAL_ENV}/bin:${PATH}" -ARG CPU_ONLY=false WORKDIR /app RUN apt-get update \ - && apt-get install -y redis-server libgl1 libglib2.0-0 curl wget git procps \ - && apt-get clean + && apt-get install -y --no-install-recommends libgl1 libglib2.0-0 \ + && rm -rf /var/lib/apt/lists/* -# Install Poetry and configure it -RUN pip install poetry \ - && poetry config virtualenvs.create false +RUN python -m venv "${VIRTUAL_ENV}" \ + && /usr/local/bin/pip install "poetry==${POETRY_VERSION}" COPY pyproject.toml poetry.lock ./ -# Install dependencies before torch -RUN poetry install --no-interaction --no-root +# Install locked application dependencies into the runtime virtual environment. +RUN poetry install --only main --no-interaction --no-root -# Install PyTorch separately based on CPU_ONLY flag -RUN if [ "$CPU_ONLY" = "true" ]; then \ - pip install --no-cache-dir torch torchvision --extra-index-url https://download.pytorch.org/whl/cpu; \ +# Replace the PyTorch wheel from the lock file with the target CPU/GPU variant. +RUN packages="$("${VIRTUAL_ENV}/bin/python" -c 'import importlib.metadata as m; names = {"torch", "torchvision", "torchaudio", "triton"}; print(" ".join(d.metadata["Name"] for d in m.distributions() if d.metadata["Name"] in names or d.metadata["Name"].startswith("nvidia-")))')"; \ + if [ -n "$packages" ]; then "${VIRTUAL_ENV}/bin/pip" uninstall --yes $packages; fi; \ + if [ "$CPU_ONLY" = "true" ]; then \ + "${VIRTUAL_ENV}/bin/pip" install --no-cache-dir torch torchvision --index-url https://download.pytorch.org/whl/cpu; \ else \ - pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121; \ + "${VIRTUAL_ENV}/bin/pip" install --no-cache-dir torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121; \ + fi + +RUN mkdir -p /models/huggingface /models/torch + +RUN if [ "$PRELOAD_MODELS" = "true" ]; then \ + python -c 'from docling.pipeline.standard_pdf_pipeline import StandardPdfPipeline; StandardPdfPipeline.download_models_hf(force=True);'; \ + fi + +RUN if [ "$PRELOAD_MODELS" = "true" ]; then \ + CPU_ONLY="$CPU_ONLY" python -c 'import os, easyocr; gpu = os.environ.get("CPU_ONLY", "true").lower() != "true"; easyocr.Reader(["fr", "de", "es", "en", "it", "pt"], gpu=gpu); print("EasyOCR models downloaded successfully")'; \ fi -ENV HF_HOME=/tmp/ \ - TORCH_HOME=/tmp/ \ - OMP_NUM_THREADS=4 +FROM python:3.12-slim-bookworm AS runtime + +ARG VIRTUAL_ENV=/opt/venv +ARG APP_UID=10001 +ARG APP_GID=10001 + +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + HF_HOME=/models/huggingface \ + TORCH_HOME=/models/torch \ + OMP_NUM_THREADS=4 \ + PATH="${VIRTUAL_ENV}/bin:${PATH}" -RUN python -c 'from docling.pipeline.standard_pdf_pipeline import StandardPdfPipeline; artifacts_path = StandardPdfPipeline.download_models_hf(force=True);' +WORKDIR /app + +RUN apt-get update \ + && apt-get install -y --no-install-recommends libgl1 libglib2.0-0 libgomp1 tini \ + && rm -rf /var/lib/apt/lists/* \ + && groupadd --system --gid "${APP_GID}" docling \ + && useradd --system --uid "${APP_UID}" --gid docling --home-dir /app --shell /usr/sbin/nologin docling \ + && mkdir -p /models/huggingface /models/torch \ + && chown -R docling:docling /app /models -# Pre-download EasyOCR models in compatible groups -RUN python -c 'import easyocr; \ - reader = easyocr.Reader(["fr", "de", "es", "en", "it", "pt"], gpu=True); \ - print("EasyOCR models downloaded successfully")' +COPY --from=builder "${VIRTUAL_ENV}" "${VIRTUAL_ENV}" +COPY --from=builder --chown=docling:docling /models /models +COPY --chown=docling:docling . . -COPY . . +USER docling EXPOSE 8080 -CMD ["poetry", "run", "uvicorn", "--port", "8080", "--host", "0.0.0.0", "main:app"] +ENTRYPOINT ["tini", "--"] +CMD ["uvicorn", "--port", "8080", "--host", "0.0.0.0", "main:app"] diff --git a/README.md b/README.md index 35c1f62..9a3bd3c 100644 --- a/README.md +++ b/README.md @@ -132,13 +132,15 @@ curl -X POST "http://localhost:8080/documents/convert" \ ## Environment Setup (Running in Docker) +For detailed Docker instructions, including CPU mode, GPU mode, service URLs, verification commands, configuration, and troubleshooting, see [docs/docker.md](docs/docker.md). + 1. Clone the repository: ```bash git clone https://github.com/drmingler/docling-api.git cd docling-api ``` -2. Create a `.env` file: +2. Optional: create a `.env` file if you need to override the default Docker Redis connection: ```bash REDIS_HOST=redis://redis:6379/0 ENV=production @@ -147,13 +149,13 @@ ENV=production ### CPU Mode To start the service using CPU-only processing, use the following command. You can adjust the number of Celery workers by specifying the --scale option. In this example, 1 worker will be created: ```bash -docker-compose -f docker-compose.cpu.yml up --build --scale celery_worker=1 +docker compose up --build --scale celery_worker=1 ``` ### GPU Mode (Recommend for production) For production, it is recommended to enable GPU acceleration, as it significantly improves performance. Use the command below to start the service with GPU support. You can also scale the number of Celery workers using the --scale option; here, 3 workers will be launched: ```bash -docker-compose -f docker-compose.gpu.yml up --build --scale celery_worker=3 +docker compose -f docker-compose.gpu.yml up --build --scale celery_worker=3 ``` ## Service Components diff --git a/docker-compose.cpu.yml b/docker-compose.cpu.yml index 9867b07..601332e 100644 --- a/docker-compose.cpu.yml +++ b/docker-compose.cpu.yml @@ -1,70 +1,90 @@ -version: "3.8" - services: celery_worker: build: context: . args: CPU_ONLY: "true" - image: converter-cpu-image - command: poetry run celery -A worker.celery_config worker --pool=solo -n worker_primary --loglevel=info + image: docling-api:cpu + command: celery -A worker.celery_config worker --pool=solo -n worker_primary --loglevel=info + user: "10001:10001" + security_opt: + - no-new-privileges:true volumes: - - .:/app - - model_cache:/tmp + - model_cache:/models environment: - - REDIS_HOST=${REDIS_HOST} - - ENV=production + REDIS_HOST: redis://redis:6379/0 + ENV: production + MALLOC_ARENA_MAX: "2" + OMP_NUM_THREADS: "2" + PYTHONMALLOC: malloc restart: on-failure depends_on: - redis app: - container_name: marker-api-cpu + container_name: docling-api-cpu build: context: . args: CPU_ONLY: "true" - image: converter-cpu-image - command: poetry run uvicorn --port 8080 --host 0.0.0.0 main:app + image: docling-api:cpu + command: uvicorn --port 8080 --host 0.0.0.0 main:app + user: "10001:10001" + security_opt: + - no-new-privileges:true environment: - - REDIS_HOST=${REDIS_HOST} - - ENV=production - - MALLOC_ARENA_MAX=2 - - OMP_NUM_THREADS=2 - - PYTHONMALLOC=malloc + REDIS_HOST: redis://redis:6379/0 + ENV: production + MALLOC_ARENA_MAX: "2" + OMP_NUM_THREADS: "2" + PYTHONMALLOC: malloc ports: - "8080:8080" volumes: - - .:/app - - model_cache:/tmp + - model_cache:/models + healthcheck: + test: + [ + "CMD", + "python", + "-c", + "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/docs', timeout=5).read()", + ] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s restart: on-failure depends_on: - redis redis: - container_name: redis + container_name: docling-api-redis image: redis:7.2.4-alpine ports: - "6379:6379" + restart: on-failure flower: - container_name: flower_cpu + container_name: docling-api-flower-cpu build: context: . args: CPU_ONLY: "true" - image: converter-cpu-image - command: poetry run celery -A worker.celery_config flower --port=5555 + image: docling-api:cpu + command: celery -A worker.celery_config flower --port=5555 + user: "10001:10001" + security_opt: + - no-new-privileges:true ports: - "5556:5555" volumes: - - .:/app - - model_cache:/tmp + - model_cache:/models environment: - - REDIS_HOST=${REDIS_HOST} - - ENV=production + REDIS_HOST: redis://redis:6379/0 + ENV: production + restart: on-failure depends_on: - - app - redis - celery_worker diff --git a/docker-compose.gpu.yml b/docker-compose.gpu.yml index cfed965..8885883 100644 --- a/docker-compose.gpu.yml +++ b/docker-compose.gpu.yml @@ -1,18 +1,21 @@ -version: "3.8" - services: celery_worker: build: context: . args: CPU_ONLY: "false" - image: converter-gpu-image - command: poetry run celery -A worker.celery_config worker --pool=solo -n worker_primary --loglevel=info + image: docling-api:gpu + command: celery -A worker.celery_config worker --pool=solo -n worker_primary --loglevel=info + user: "10001:10001" + security_opt: + - no-new-privileges:true volumes: - - .:/app + - model_cache:/models environment: - - REDIS_HOST=${REDIS_HOST} - - ENV=production + REDIS_HOST: redis://redis:6379/0 + ENV: production + NVIDIA_VISIBLE_DEVICES: all + restart: on-failure deploy: resources: reservations: @@ -24,21 +27,37 @@ services: - redis app: - container_name: marker-api-gpu + container_name: docling-api-gpu build: context: . args: CPU_ONLY: "false" - image: converter-gpu-image - command: poetry run uvicorn --port 8080 --host 0.0.0.0 main:app + image: docling-api:gpu + command: uvicorn --port 8080 --host 0.0.0.0 main:app + user: "10001:10001" + security_opt: + - no-new-privileges:true environment: - - REDIS_HOST=${REDIS_HOST} - - ENV=production - - NVIDIA_VISIBLE_DEVICES=all + REDIS_HOST: redis://redis:6379/0 + ENV: production + NVIDIA_VISIBLE_DEVICES: all ports: - "8080:8080" volumes: - - .:/app + - model_cache:/models + healthcheck: + test: + [ + "CMD", + "python", + "-c", + "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/docs', timeout=5).read()", + ] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + restart: on-failure deploy: resources: reservations: @@ -51,28 +70,33 @@ services: - celery_worker redis: - container_name: redis + container_name: docling-api-redis image: redis:7.2.4-alpine ports: - "6379:6379" + restart: on-failure flower: - container_name: flower_gpu + container_name: docling-api-flower-gpu build: context: . args: CPU_ONLY: "false" - image: converter-gpu-image - command: poetry run celery -A worker.celery_config flower --port=5555 + image: docling-api:gpu + command: celery -A worker.celery_config flower --port=5555 + user: "10001:10001" + security_opt: + - no-new-privileges:true ports: - "5556:5555" volumes: - - .:/app + - model_cache:/models environment: - - REDIS_HOST=${REDIS_HOST} - - ENV=production + REDIS_HOST: redis://redis:6379/0 + ENV: production + NVIDIA_VISIBLE_DEVICES: all + restart: on-failure depends_on: - - app - redis - celery_worker deploy: @@ -82,3 +106,6 @@ services: - driver: nvidia count: 1 capabilities: [gpu] + +volumes: + model_cache: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..61ee9f5 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,93 @@ +services: + app: + container_name: docling-api + build: + context: . + args: + CPU_ONLY: "true" + image: docling-api:cpu + command: uvicorn --port 8080 --host 0.0.0.0 main:app + user: "10001:10001" + security_opt: + - no-new-privileges:true + environment: + REDIS_HOST: redis://redis:6379/0 + ENV: production + MALLOC_ARENA_MAX: "2" + OMP_NUM_THREADS: "2" + PYTHONMALLOC: malloc + ports: + - "8080:8080" + volumes: + - model_cache:/models + healthcheck: + test: + [ + "CMD", + "python", + "-c", + "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/docs', timeout=5).read()", + ] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + restart: on-failure + depends_on: + - redis + - celery_worker + + celery_worker: + build: + context: . + args: + CPU_ONLY: "true" + image: docling-api:cpu + command: celery -A worker.celery_config worker --pool=solo -n worker_primary --loglevel=info + user: "10001:10001" + security_opt: + - no-new-privileges:true + environment: + REDIS_HOST: redis://redis:6379/0 + ENV: production + MALLOC_ARENA_MAX: "2" + OMP_NUM_THREADS: "2" + PYTHONMALLOC: malloc + volumes: + - model_cache:/models + restart: on-failure + depends_on: + - redis + + redis: + container_name: docling-api-redis + image: redis:7.2.4-alpine + ports: + - "6379:6379" + restart: on-failure + + flower: + container_name: docling-api-flower + build: + context: . + args: + CPU_ONLY: "true" + image: docling-api:cpu + command: celery -A worker.celery_config flower --port=5555 + user: "10001:10001" + security_opt: + - no-new-privileges:true + environment: + REDIS_HOST: redis://redis:6379/0 + ENV: production + ports: + - "5556:5555" + volumes: + - model_cache:/models + restart: on-failure + depends_on: + - redis + - celery_worker + +volumes: + model_cache: diff --git a/docs/docker.md b/docs/docker.md new file mode 100644 index 0000000..d08c9b7 --- /dev/null +++ b/docs/docker.md @@ -0,0 +1,146 @@ +# Running Docling API with Docker + +This guide runs the FastAPI app, a Celery worker, Redis, and the Flower dashboard with Docker Compose. + +The application image uses a multi-stage Docker build. Python dependencies and optional model downloads are prepared in a builder stage, while the final runtime image contains only the virtual environment, application code, runtime system libraries, and a non-root `docling` user. Compose also runs the app containers with `no-new-privileges` enabled and an API healthcheck. + +## Prerequisites + +- Docker Engine with Docker Compose v2. +- At least 8 GB RAM available to Docker for CPU mode. +- For GPU mode, install the NVIDIA driver and NVIDIA Container Toolkit on the host. + +## Services + +| Service | URL | Purpose | +| --- | --- | --- | +| API | http://localhost:8080 | FastAPI document conversion API | +| API docs | http://localhost:8080/docs | Interactive OpenAPI documentation | +| Redis | localhost:6379 | Celery broker and result backend | +| Flower | http://localhost:5556 | Celery worker monitoring | + +## CPU mode + +1. Clone the repository. + +```bash +git clone https://github.com/drmingler/docling-api.git +cd docling-api +``` + +2. Build and start the stack. + +```bash +docker compose up --build +``` + +The default `docker-compose.yml` is CPU-only. It builds the app image, starts Redis, starts one Celery worker, and exposes the API on port `8080`. + +3. Scale workers when needed. + +```bash +docker compose up --build --scale celery_worker=2 +``` + +4. Stop the stack. + +```bash +docker compose down +``` + +## GPU mode + +1. Confirm Docker can access the GPU. + +```bash +docker run --rm --gpus all nvidia/cuda:12.1.1-base-ubuntu22.04 nvidia-smi +``` + +2. Build and start the GPU stack. + +```bash +docker compose -f docker-compose.gpu.yml up --build +``` + +3. Scale GPU workers if the host has enough GPU memory. + +```bash +docker compose -f docker-compose.gpu.yml up --build --scale celery_worker=3 +``` + +## Existing CPU compose file + +The repository also includes `docker-compose.cpu.yml`. It is equivalent to the default compose file and can be used explicitly: + +```bash +docker compose -f docker-compose.cpu.yml up --build +``` + +## Verify the API + +1. Open the API docs: + +```bash +curl http://localhost:8080/docs +``` + +2. Convert one document synchronously: + +```bash +curl -X POST "http://localhost:8080/documents/convert" \ + -H "accept: application/json" \ + -H "Content-Type: multipart/form-data" \ + -F "document=@/path/to/document.pdf" \ + -F "extract_tables_as_images=true" \ + -F "image_resolution_scale=4" +``` + +3. Submit one document asynchronously: + +```bash +curl -X POST "http://localhost:8080/conversion-jobs" \ + -H "accept: application/json" \ + -H "Content-Type: multipart/form-data" \ + -F "document=@/path/to/document.pdf" +``` + +4. Check the asynchronous job status. Replace `{job_id}` with the returned job id. + +```bash +curl -X GET "http://localhost:8080/conversion-jobs/{job_id}" \ + -H "accept: application/json" +``` + +## Configuration + +The Compose files set `REDIS_HOST=redis://redis:6379/0` for the API, worker, and Flower containers. Use a `.env` file only when you need to override values for a custom deployment. + +The Docker image accepts these build arguments: + +| Argument | Default | Description | +| --- | --- | --- | +| `CPU_ONLY` | `true` | Installs CPU PyTorch wheels when `true`; installs CUDA wheels when `false`. | +| `PRELOAD_MODELS` | `true` | Downloads Docling and EasyOCR model files during image build. | +| `POETRY_VERSION` | `1.8.4` | Poetry version used only in the builder stage. | +| `APP_UID` | `10001` | Runtime user id for the non-root `docling` user. | +| `APP_GID` | `10001` | Runtime group id for the non-root `docling` group. | + +Example without preloading models: + +```bash +docker compose build --build-arg PRELOAD_MODELS=false +docker compose up +``` + +Model caches are stored in the `model_cache` Docker volume and mounted at `/models` in the app, worker, and Flower containers. + +## Troubleshooting + +- If port `8080`, `5556`, or `6379` is already in use, update the host-side port in the compose file. +- The first build can take several minutes because PyTorch, Docling, and OCR model dependencies are large. +- If GPU containers cannot see the GPU, reinstall or reconfigure the NVIDIA Container Toolkit and rerun the `nvidia-smi` Docker test above. +- To remove containers and cached model volume data, run: + +```bash +docker compose down -v +```