build(docker): slim backend image, static frontend image, -docling variant

Backend (arc53/docsgpt): 4.5 GB compressed -> 0.9 GB with both embedding
models and tiktoken baked in.
- torch/transformers gone from the default install (docling extra only).
- Ubuntu 24.04 ships python3.12: no deadsnakes PPA, no software-properties-
  common; every pin is a wheel, so no gcc/g++/rust in the builder.
- COPY --chown and a prefetch that runs as the process user replace the
  trailing chown -R, which duplicated the 600 MB model layer.
- .dockerignore keeps __pycache__, .coverage, local indexes and .env out.
- EXTRAS build arg (INSTALL_DOCLING kept as an alias); the docling variant
  also bakes docling's layout/table/RapidOCR models (DOCLING_ARTIFACTS_PATH)
  and tesseract, and drops only the discovery documents of Google APIs the
  app never builds.
- FLASK_DEBUG env removed (unused); OCI labels added.

Frontend (arc53/docsgpt-fe): 302 MB Vite dev server -> 25 MB static build
behind nginx. VITE_* variables are injected at container start into
/config.js and read through src/env.ts, so the image no longer needs a
rebuild per deployment; docker-compose.yaml keeps hot reload via the dev
target.

Publishing: every release and develop build now pushes a slim tag and a
-docling tag (docling engine + models + tesseract). docker-compose-hub.yaml
takes DOCSGPT_IMAGE_TAG / DOCSGPT_IMAGE_VARIANT; docker-compose-standalone.yaml
runs the stack from pre-built images without a checkout and is attached to
each release. setup.sh selects the -docling variant for OCR instead of
requiring a local build. A new workflow builds the image on PRs that touch
it and runs verify_offline under --network none; lint checks the exported
requirements match uv.lock.
This commit is contained in:
Alex committed 2026-09-05 15:50:21 +01:00
1 parent 4707c45b93
commit aecb596e99
30 files changed
+706 -247

No files matched your search

+52 -34
View File
@@ -9,24 +9,17 @@ jobs:
if: github.repository == 'arc53/DocsGPT'
strategy:
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
suffix: amd64
- platform: linux/arm64
runner: ubuntu-24.04-arm
suffix: arm64
runs-on: ${{ matrix.runner }}
platform: [linux/amd64, linux/arm64]
# "" is the slim default image; "-docling" bakes the docling parser
# engine, its models and tesseract in (OCR-ready).
variant: ["", "-docling"]
runs-on: ${{ matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }}
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up QEMU # Only needed for emulation, not for native arm64 builds
if: matrix.platform == 'linux/arm64'
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
@@ -46,6 +39,17 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Image metadata (OCI labels)
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ secrets.DOCKER_USERNAME }}/docsgpt
ghcr.io/${{ github.repository_owner }}/docsgpt
labels: |
org.opencontainers.image.title=DocsGPT${{ matrix.variant }}
org.opencontainers.image.version=${{ github.event.release.tag_name }}
- name: Build and push platform-specific images
uses: docker/build-push-action@v6
with:
@@ -53,17 +57,24 @@ jobs:
platforms: ${{ matrix.platform }}
context: ./application
push: true
build-args: |
EXTRAS=${{ matrix.variant == '-docling' && 'docling' || '' }}
INSTALL_TESSERACT=${{ matrix.variant == '-docling' && 'true' || 'false' }}
tags: |
${{ secrets.DOCKER_USERNAME }}/docsgpt:${{ github.event.release.tag_name }}-${{ matrix.suffix }}
ghcr.io/${{ github.repository_owner }}/docsgpt:${{ github.event.release.tag_name }}-${{ matrix.suffix }}
${{ secrets.DOCKER_USERNAME }}/docsgpt:${{ github.event.release.tag_name }}${{ matrix.variant }}-${{ matrix.platform == 'linux/arm64' && 'arm64' || 'amd64' }}
ghcr.io/${{ github.repository_owner }}/docsgpt:${{ github.event.release.tag_name }}${{ matrix.variant }}-${{ matrix.platform == 'linux/arm64' && 'arm64' || 'amd64' }}
labels: ${{ steps.meta.outputs.labels }}
provenance: false
sbom: false
cache-from: type=registry,ref=${{ secrets.DOCKER_USERNAME }}/docsgpt:latest
cache-from: type=registry,ref=${{ secrets.DOCKER_USERNAME }}/docsgpt:latest${{ matrix.variant }}
cache-to: type=inline
manifest:
if: github.repository == 'arc53/DocsGPT'
needs: build
strategy:
matrix:
variant: ["", "-docling"]
runs-on: ubuntu-latest
permissions:
packages: write
@@ -87,26 +98,33 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create and push manifest for DockerHub
- name: Create and push multi-arch manifests
env:
TAG: ${{ github.event.release.tag_name }}${{ matrix.variant }}
LATEST: latest${{ matrix.variant }}
run: |
set -e
docker manifest create ${{ secrets.DOCKER_USERNAME }}/docsgpt:${{ github.event.release.tag_name }} \
--amend ${{ secrets.DOCKER_USERNAME }}/docsgpt:${{ github.event.release.tag_name }}-amd64 \
--amend ${{ secrets.DOCKER_USERNAME }}/docsgpt:${{ github.event.release.tag_name }}-arm64
docker manifest push ${{ secrets.DOCKER_USERNAME }}/docsgpt:${{ github.event.release.tag_name }}
docker manifest create ${{ secrets.DOCKER_USERNAME }}/docsgpt:latest \
--amend ${{ secrets.DOCKER_USERNAME }}/docsgpt:${{ github.event.release.tag_name }}-amd64 \
--amend ${{ secrets.DOCKER_USERNAME }}/docsgpt:${{ github.event.release.tag_name }}-arm64
docker manifest push ${{ secrets.DOCKER_USERNAME }}/docsgpt:latest
for repo in "${{ secrets.DOCKER_USERNAME }}/docsgpt" "ghcr.io/${{ github.repository_owner }}/docsgpt"; do
for name in "$TAG" "$LATEST"; do
docker manifest create "$repo:$name" \
--amend "$repo:$TAG-amd64" \
--amend "$repo:$TAG-arm64"
docker manifest push "$repo:$name"
done
done
- name: Create and push manifest for ghcr.io
release-assets:
if: github.repository == 'arc53/DocsGPT'
needs: manifest
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Attach the standalone compose file to the release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -e
docker manifest create ghcr.io/${{ github.repository_owner }}/docsgpt:${{ github.event.release.tag_name }} \
--amend ghcr.io/${{ github.repository_owner }}/docsgpt:${{ github.event.release.tag_name }}-amd64 \
--amend ghcr.io/${{ github.repository_owner }}/docsgpt:${{ github.event.release.tag_name }}-arm64
docker manifest push ghcr.io/${{ github.repository_owner }}/docsgpt:${{ github.event.release.tag_name }}
docker manifest create ghcr.io/${{ github.repository_owner }}/docsgpt:latest \
--amend ghcr.io/${{ github.repository_owner }}/docsgpt:${{ github.event.release.tag_name }}-amd64 \
--amend ghcr.io/${{ github.repository_owner }}/docsgpt:${{ github.event.release.tag_name }}-arm64
docker manifest push ghcr.io/${{ github.repository_owner }}/docsgpt:latest
gh release upload "${{ github.event.release.tag_name }}" \
deployment/docker-compose-standalone.yaml --clobber
+38 -25
View File
@@ -11,14 +11,11 @@ jobs:
if: github.repository == 'arc53/DocsGPT'
strategy:
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
suffix: amd64
- platform: linux/arm64
runner: ubuntu-24.04-arm
suffix: arm64
runs-on: ${{ matrix.runner }}
platform: [linux/amd64, linux/arm64]
# "" is the slim default image; "-docling" bakes the docling parser
# engine, its models and tesseract in (OCR-ready).
variant: ["", "-docling"]
runs-on: ${{ matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }}
permissions:
contents: read
packages: write
@@ -36,7 +33,7 @@ jobs:
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to ghcr.io
uses: docker/login-action@v3
with:
@@ -44,6 +41,17 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Image metadata (OCI labels)
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ secrets.DOCKER_USERNAME }}/docsgpt
ghcr.io/${{ github.repository_owner }}/docsgpt
labels: |
org.opencontainers.image.title=DocsGPT${{ matrix.variant }}
org.opencontainers.image.version=develop
- name: Build and push platform-specific images
uses: docker/build-push-action@v6
with:
@@ -51,17 +59,24 @@ jobs:
platforms: ${{ matrix.platform }}
context: ./application
push: true
build-args: |
EXTRAS=${{ matrix.variant == '-docling' && 'docling' || '' }}
INSTALL_TESSERACT=${{ matrix.variant == '-docling' && 'true' || 'false' }}
tags: |
${{ secrets.DOCKER_USERNAME }}/docsgpt:develop-${{ matrix.suffix }}
ghcr.io/${{ github.repository_owner }}/docsgpt:develop-${{ matrix.suffix }}
${{ secrets.DOCKER_USERNAME }}/docsgpt:develop${{ matrix.variant }}-${{ matrix.platform == 'linux/arm64' && 'arm64' || 'amd64' }}
ghcr.io/${{ github.repository_owner }}/docsgpt:develop${{ matrix.variant }}-${{ matrix.platform == 'linux/arm64' && 'arm64' || 'amd64' }}
labels: ${{ steps.meta.outputs.labels }}
provenance: false
sbom: false
cache-from: type=registry,ref=${{ secrets.DOCKER_USERNAME }}/docsgpt:develop
cache-from: type=registry,ref=${{ secrets.DOCKER_USERNAME }}/docsgpt:develop${{ matrix.variant }}
cache-to: type=inline
manifest:
if: github.repository == 'arc53/DocsGPT'
needs: build
strategy:
matrix:
variant: ["", "-docling"]
runs-on: ubuntu-latest
permissions:
packages: write
@@ -77,24 +92,22 @@ jobs:
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to ghcr.io
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create and push manifest for DockerHub
run: |
docker manifest create ${{ secrets.DOCKER_USERNAME }}/docsgpt:develop \
--amend ${{ secrets.DOCKER_USERNAME }}/docsgpt:develop-amd64 \
--amend ${{ secrets.DOCKER_USERNAME }}/docsgpt:develop-arm64
docker manifest push ${{ secrets.DOCKER_USERNAME }}/docsgpt:develop
- name: Create and push manifest for ghcr.io
- name: Create and push multi-arch manifests
env:
TAG: develop${{ matrix.variant }}
run: |
docker manifest create ghcr.io/${{ github.repository_owner }}/docsgpt:develop \
--amend ghcr.io/${{ github.repository_owner }}/docsgpt:develop-amd64 \
--amend ghcr.io/${{ github.repository_owner }}/docsgpt:develop-arm64
docker manifest push ghcr.io/${{ github.repository_owner }}/docsgpt:develop
set -e
for repo in "${{ secrets.DOCKER_USERNAME }}/docsgpt" "ghcr.io/${{ github.repository_owner }}/docsgpt"; do
docker manifest create "$repo:$TAG" \
--amend "$repo:$TAG-amd64" \
--amend "$repo:$TAG-arm64"
docker manifest push "$repo:$TAG"
done
+52
View File
@@ -0,0 +1,52 @@
name: Verify the Docker image works offline
# Builds the backend image and runs its offline check with networking off, so
# a change that reintroduces a first-request download (a tokenizer, tiktoken's
# encoding, an embedding model) fails here instead of in an air-gapped install.
on:
workflow_dispatch:
pull_request:
paths:
- 'application/Dockerfile'
- 'application/.dockerignore'
- 'application/requirements*.txt'
- 'application/scripts/prefetch_models.py'
- 'application/scripts/verify_offline.py'
- 'application/vectorstore/model_registry.py'
- 'application/parser/tokenization.py'
- 'application/vectorstore/embeddings_local.py'
- '.github/workflows/docker-image-verify.yml'
permissions:
contents: read
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build the slim image
uses: docker/build-push-action@v6
with:
file: ./application/Dockerfile
context: ./application
platforms: linux/amd64
load: true
tags: docsgpt:verify
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Image size
run: |
docker image inspect docsgpt:verify --format '{{.Size}}' | awk '{printf "uncompressed: %.2f GB\n", $1/1e9}'
docker history docsgpt:verify --format '{{.Size}}\t{{.CreatedBy}}' | head -20
- name: Offline verification (no network)
run: |
docker run --rm --network none docsgpt:verify \
python -m application.scripts.verify_offline
+15
View File
@@ -20,3 +20,18 @@ jobs:
uses: chartboost/ruff-action@v1
with:
version: 0.14.10
requirements-in-sync:
# application/requirements*.txt are exported from uv.lock; fail when a
# change to pyproject.toml or uv.lock was not re-exported.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
- name: Re-export and diff
run: |
uv lock --check
bash scripts/export_requirements.sh
git diff --exit-code -- application/requirements.txt application/requirements-docling.txt application/requirements-milvus.txt
+23
View File
@@ -0,0 +1,23 @@
# Build context is application/. Keep local state and caches out of the image.
__pycache__/
*.py[cod]
.pytest_cache/
.ruff_cache/
.coverage
htmlcov/
*.log
# Runtime data: bind-mounted or created at run time, never baked in.
indexes/
inputs/
vectors/
*.faiss
*.pkl
# Secrets and local config.
.env
.env.*
# Not needed inside the image.
Dockerfile
.dockerignore
+112 -96
View File
@@ -1,74 +1,81 @@
# Builder Stage
FROM ubuntu:24.04 as builder
# DocsGPT backend image.
#
# Build args:
# EXTRAS comma-separated optional extras to bake in, matching the
# pyproject extras / requirements-<extra>.txt files:
# docling (layout-model parser + OCR backend), milvus.
# INSTALL_DOCLING legacy alias for EXTRAS=docling (setup.sh writes it).
# INSTALL_TESSERACT bake the tesseract binary for OCR_ENGINE=tesseract.
# EMBEDDINGS_PREFETCH registry names of the embedding models to bake; empty
# bakes both defaults (mpnet for upgrades, granite for
# new installs).
#
# Everything the default configuration needs is inside the image: embedding
# models, their tokenizers, tiktoken's encoding and, with the docling extra,
# docling's layout/table/OCR models. `python -m application.scripts.verify_offline`
# under `docker run --network none` proves it.
FROM ubuntu:24.04 AS builder
ENV DEBIAN_FRONTEND=noninteractive
# Ubuntu 24.04 ships Python 3.12 in its main archive: no PPA needed. Every pin
# resolves to a wheel, so no compiler toolchain either.
RUN apt-get update && \
apt-get install -y --no-install-recommends python3.12 python3.12-venv ca-certificates && \
rm -rf /var/lib/apt/lists/*
COPY requirements.txt requirements-docling.txt requirements-milvus.txt ./
RUN python3.12 -m venv /venv
ENV PATH="/venv/bin:$PATH"
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir --only-binary=:all: -r requirements.txt
# Optional extras. Each requirements-<extra>.txt is exported from the same
# lock as requirements.txt, so installing it on top only adds the extra's
# packages. The docling file takes torch from the CPU-only PyTorch index.
# Not wheels-only: docling's antlr4 runtime ships as a pure-Python sdist.
ARG EXTRAS=""
ARG INSTALL_DOCLING=false
RUN set -e; \
extras="$EXTRAS"; \
if [ "$INSTALL_DOCLING" = "true" ]; then extras="$extras,docling"; fi; \
for extra in $(echo "$extras" | tr ',' ' '); do \
echo "Installing extra: $extra"; \
pip install --no-cache-dir -r "requirements-$extra.txt"; \
done
# google-api-python-client bundles discovery documents for ~600 Google APIs
# (99 MB). The application builds one client, Drive v3; keep only its document.
# Building another API's client needs its file back, or static_discovery=False.
RUN find /venv/lib/python3.12/site-packages/googleapiclient/discovery_cache/documents \
-type f ! -name 'drive.v3.json' -delete
FROM ubuntu:24.04 AS final
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && \
apt-get install -y software-properties-common && \
add-apt-repository ppa:deadsnakes/ppa && \
apt-get update && \
apt-get install -y --no-install-recommends gcc g++ wget unzip libc6-dev python3.12 python3.12-venv python3.12-dev && \
rm -rf /var/lib/apt/lists/*
# Verify Python installation and setup symlink
RUN if [ -f /usr/bin/python3.12 ]; then \
ln -s /usr/bin/python3.12 /usr/bin/python; \
else \
echo "Python 3.12 not found"; exit 1; \
fi
# Install Rust
RUN wget -q -O - https://sh.rustup.rs | sh -s -- -y
# Clean up to reduce container size
RUN apt-get remove --purge -y wget unzip && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*
# Copy requirements manifests
COPY requirements.txt requirements-docling.txt ./
# Setup Python virtual environment
RUN python3.12 -m venv /venv
# Activate virtual environment and install Python packages
ENV PATH="/venv/bin:$PATH"
# Install Python packages
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir tiktoken && \
pip install --no-cache-dir -r requirements.txt
# Optional docling parser engine (DOC_PARSER_ENGINE=docling, the docling OCR
# backend, read_document's structured output) — OFF by default: it pulls the
# layout/OCR model stack and adds gigabytes to the image. anydoc (in
# requirements.txt) is the default parser and needs none of it, and OCR runs
# natively on tesseract (below, also opt-in) without it.
ARG INSTALL_DOCLING=false
RUN if [ "$INSTALL_DOCLING" = "true" ]; then \
pip install --no-cache-dir -r requirements-docling.txt; \
fi
# Final Stage
FROM ubuntu:24.04 as final
RUN apt-get update && \
apt-get install -y software-properties-common && \
add-apt-repository ppa:deadsnakes/ppa && \
apt-get update && apt-get install -y --no-install-recommends \
python3.12 \
libgl1 \
libglib2.0-0 \
poppler-utils \
&& \
apt-get install -y --no-install-recommends python3.12 poppler-utils ca-certificates && \
ln -s /usr/bin/python3.12 /usr/bin/python && \
rm -rf /var/lib/apt/lists/*
# opencv (rapidocr, part of the docling extra) needs libGL at import time.
ARG EXTRAS=""
ARG INSTALL_DOCLING=false
RUN if [ "$INSTALL_DOCLING" = "true" ] || echo ",$EXTRAS," | grep -q ",docling,"; then \
apt-get update && \
apt-get install -y --no-install-recommends libgl1 libglib2.0-0 && \
rm -rf /var/lib/apt/lists/*; \
fi
# Optional tesseract OCR engine (OCR_ENABLED=true with OCR_ENGINE=tesseract,
# the default engine) — OFF by default like every other OCR dependency; OCR
# itself is off unless configured. Opt in with --build-arg
# INSTALL_TESSERACT=true (setup.sh writes it to .env when OCR is enabled);
# ~35 MB of system packages. Extra language packs are a deployment concern
# (apt: tesseract-ocr-<lang>, then list them in OCR_LANGS). A DeepSeek-OCR
# endpoint (OCR_ENGINE=deepseek) needs none of this.
# the default engine); ~35 MB of system packages. Extra language packs are a
# deployment concern (apt: tesseract-ocr-<lang>, then list them in OCR_LANGS).
# A DeepSeek-OCR endpoint (OCR_ENGINE=deepseek) needs none of this.
ARG INSTALL_TESSERACT=false
RUN if [ "$INSTALL_TESSERACT" = "true" ]; then \
apt-get update && \
@@ -76,61 +83,70 @@ RUN if [ "$INSTALL_TESSERACT" = "true" ]; then \
rm -rf /var/lib/apt/lists/*; \
fi
# Set working directory
LABEL org.opencontainers.image.source="https://github.com/arc53/DocsGPT" \
org.opencontainers.image.title="DocsGPT" \
org.opencontainers.image.description="DocsGPT backend: API and Celery worker" \
org.opencontainers.image.licenses="MIT"
WORKDIR /app
# Create a non-root user: `appuser` (Feel free to choose a name)
# The process user owns /app so the model prefetch below can run as it: an
# unprivileged prefetch writes the model files with the right owner up front,
# instead of a trailing chown -R that rewrites every model file into a second
# layer.
RUN groupadd -r appuser && \
useradd -r -g appuser -d /app -s /sbin/nologin -c "Docker image user" appuser
useradd -r -g appuser -d /app -s /sbin/nologin -c "Docker image user" appuser && \
chown appuser:appuser /app && \
install -d -o appuser -g appuser /app/models /app/application
# Copy the virtual environment and model from the builder stage
COPY --from=builder /venv /venv
# Pre-fetch the embedding models into FastEmbed's cache so a fresh container
# does not download on first ingest and an air-gapped install works at all.
# Both defaults are baked: an upgraded deployment keeps using mpnet until it
# runs the re-embed script, while a new one starts on granite.
# The prefetch writes hub-layout snapshots (including tokenizer.json) here, so
# HF_HUB_CACHE has to point at the same directory: chunking loads the tokenizer
# through ``tokenizers``, which reads the hub cache and would otherwise fetch
# over the network on first ingest -- and fall back to cl100k when offline.
# Every cache the application reads at run time lives under /app/models and is
# filled at build time:
# EMBEDDINGS_CACHE_DIR / HF_HUB_CACHE FastEmbed models and their tokenizers
# (chunking reads tokenizer.json from
# the same hub-layout snapshot)
# TIKTOKEN_CACHE_DIR cl100k_base for token accounting
# DOCLING_ARTIFACTS_PATH docling's models (docling extra only)
ENV EMBEDDINGS_CACHE_DIR=/app/models \
HF_HUB_CACHE=/app/models
HF_HUB_CACHE=/app/models \
TIKTOKEN_CACHE_DIR=/app/models/tiktoken \
DOCLING_ARTIFACTS_PATH=/app/models/docling \
HF_HUB_DISABLE_TELEMETRY=1 \
PATH="/venv/bin:$PATH"
# Only the modules the prefetch imports are copied first, so an unrelated
# source edit does not invalidate the model layer.
COPY --chown=appuser:appuser __init__.py /app/application/__init__.py
COPY --chown=appuser:appuser scripts/__init__.py scripts/prefetch_models.py /app/application/scripts/
COPY --chown=appuser:appuser vectorstore/__init__.py vectorstore/model_registry.py /app/application/vectorstore/
USER appuser
# Only the modules the prefetch imports are copied first. It reaches nothing
# beyond model_registry, which is stdlib-only, so keeping the full source copy
# below this layer stops an unrelated edit from re-downloading ~780 MB of model
# artifacts on every build.
COPY __init__.py /app/application/__init__.py
COPY scripts/__init__.py scripts/prefetch_models.py /app/application/scripts/
COPY vectorstore/__init__.py vectorstore/model_registry.py /app/application/vectorstore/
ARG EMBEDDINGS_PREFETCH=""
RUN PYTHONPATH=/app /venv/bin/python -m application.scripts.prefetch_models ${EMBEDDINGS_PREFETCH}
RUN PYTHONPATH=/app python -m application.scripts.prefetch_models ${EMBEDDINGS_PREFETCH} && \
rm -rf /app/models/.locks /app/.cache
# Copy your application code
COPY . /app/application
# docling downloads its layout, table-structure and OCR models on first parse;
# bake them so the docling variant is as self-contained as the default image.
RUN if python -c "import docling" 2>/dev/null; then \
docling-tools models download --output-dir /app/models/docling layout tableformer rapidocr && \
rm -rf /app/.cache; \
fi
# Change the ownership of the /app directory to the appuser
COPY --chown=appuser:appuser . /app/application
RUN mkdir -p /app/application/inputs/local
RUN chown -R appuser:appuser /app
# Set environment variables
ENV FLASK_APP=app.py \
FLASK_DEBUG=true \
PATH="/venv/bin:$PATH"
ENV FLASK_APP=app.py
ENV MALLOC_ARENA_MAX=2 \
OMP_NUM_THREADS=4 \
MKL_NUM_THREADS=4 \
OPENBLAS_NUM_THREADS=4
# Expose the port the app runs on
EXPOSE 7091
# Switch to non-root user
USER appuser
# BoundedDrainUvicornWorker makes max_requests recycles safe with held-open SSE
# connections (see application/gunicorn_worker.py); with recycles now safe,
# --max-requests is raised (kept for memory hygiene) to cut churn.
+5 -1
View File
@@ -1,6 +1,8 @@
services:
frontend:
build: ../frontend
build:
context: ../frontend
target: dev
environment:
- VITE_API_HOST=http://localhost:7091
- VITE_API_STREAMING=$VITE_API_STREAMING
@@ -13,6 +15,7 @@ services:
build:
context: ../application
args:
EXTRAS: ${EXTRAS:-}
INSTALL_DOCLING: ${INSTALL_DOCLING:-false}
# Off by default; deployments running OCR_ENABLED=true with tesseract
# must set INSTALL_TESSERACT=true before rebuilding (see docker-compose.yaml).
@@ -40,6 +43,7 @@ services:
build:
context: ../application
args:
EXTRAS: ${EXTRAS:-}
INSTALL_DOCLING: ${INSTALL_DOCLING:-false}
# Off by default; deployments running OCR_ENABLED=true with tesseract
# must set INSTALL_TESSERACT=true before rebuilding (see docker-compose.yaml).
+9 -3
View File
@@ -1,8 +1,14 @@
# Pre-built images from Docker Hub (mirrored at ghcr.io/arc53).
# DOCSGPT_IMAGE_TAG develop (default, follows main) or a release, e.g. 0.20.0
# DOCSGPT_IMAGE_VARIANT empty (default, slim) or -docling: docling parser engine,
# its models, and tesseract baked in (OCR-ready)
# Set them in ../.env or the shell. deployment/docker-compose-standalone.yaml is
# the same stack without a git checkout.
name: docsgpt-oss
services:
frontend:
image: arc53/docsgpt-fe:develop
image: arc53/docsgpt-fe:${DOCSGPT_IMAGE_TAG:-develop}
environment:
- VITE_API_HOST=http://localhost:7091
- VITE_API_STREAMING=${VITE_API_STREAMING:-true}
@@ -15,7 +21,7 @@ services:
backend:
user: root
image: arc53/docsgpt:develop
image: arc53/docsgpt:${DOCSGPT_IMAGE_TAG:-develop}${DOCSGPT_IMAGE_VARIANT:-}
env_file:
- ../.env
environment:
@@ -38,7 +44,7 @@ services:
worker:
user: root
image: arc53/docsgpt:develop
image: arc53/docsgpt:${DOCSGPT_IMAGE_TAG:-develop}${DOCSGPT_IMAGE_VARIANT:-}
# `parsing` queue carries read_document/parse_document; required for its await to resolve.
command: celery -A application.app.celery worker -l INFO -B -Q docsgpt,parsing,embeddings
env_file:
+104
View File
@@ -0,0 +1,104 @@
# DocsGPT from pre-built images, with no git checkout.
#
# curl -fsSLO https://raw.githubusercontent.com/arc53/DocsGPT/main/deployment/docker-compose-standalone.yaml
# printf 'LLM_PROVIDER=docsgpt\nVITE_API_STREAMING=true\n' > .env # or any provider, see the settings guide
# docker compose up -d
# open http://localhost:5173
#
# Every release also attaches this file as an asset. Settings come from .env
# next to this file (any DocsGPT setting; the compose-internal service URLs
# below take precedence). Data lives in named volumes, so `docker compose
# down` keeps it and `docker compose down -v` removes it.
#
# DOCSGPT_IMAGE_TAG release to run, e.g. 0.20.0 (default: latest release);
# develop follows the main branch
# DOCSGPT_IMAGE_VARIANT empty (slim, default) or -docling: docling parser
# engine, its models, and tesseract baked in (OCR-ready)
name: docsgpt
services:
frontend:
image: arc53/docsgpt-fe:${DOCSGPT_IMAGE_TAG:-latest}
env_file:
- path: .env
required: false
environment:
- VITE_API_HOST=${VITE_API_HOST:-http://localhost:7091}
- VITE_API_STREAMING=${VITE_API_STREAMING:-true}
ports:
- "5173:5173"
depends_on:
- backend
backend:
image: arc53/docsgpt:${DOCSGPT_IMAGE_TAG:-latest}${DOCSGPT_IMAGE_VARIANT:-}
env_file:
- path: .env
required: false
environment:
- CELERY_BROKER_URL=redis://redis:6379/0
- CELERY_RESULT_BACKEND=redis://redis:6379/1
- CACHE_REDIS_URL=redis://redis:6379/2
- POSTGRES_URI=postgresql://docsgpt:docsgpt@postgres:5432/docsgpt
ports:
- "7091:7091"
volumes:
- indexes:/app/indexes
- inputs:/app/inputs
- vectors:/app/vectors
depends_on:
redis:
condition: service_started
postgres:
condition: service_healthy
restart: unless-stopped
worker:
image: arc53/docsgpt:${DOCSGPT_IMAGE_TAG:-latest}${DOCSGPT_IMAGE_VARIANT:-}
# Consumes the default queue plus `parsing` (read_document) and `embeddings`
# (query embedding); without the latter every search times out.
command: celery -A application.app.celery worker -l INFO -B -Q docsgpt,parsing,embeddings
env_file:
- path: .env
required: false
environment:
- CELERY_BROKER_URL=redis://redis:6379/0
- CELERY_RESULT_BACKEND=redis://redis:6379/1
- CACHE_REDIS_URL=redis://redis:6379/2
- POSTGRES_URI=postgresql://docsgpt:docsgpt@postgres:5432/docsgpt
- API_URL=http://backend:7091
volumes:
- indexes:/app/indexes
- inputs:/app/inputs
- vectors:/app/vectors
depends_on:
redis:
condition: service_started
postgres:
condition: service_healthy
restart: unless-stopped
redis:
image: redis:6-alpine
restart: unless-stopped
postgres:
image: postgres:16-alpine
environment:
- POSTGRES_USER=docsgpt
- POSTGRES_PASSWORD=docsgpt
- POSTGRES_DB=docsgpt
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U docsgpt -d docsgpt"]
interval: 5s
timeout: 5s
retries: 10
restart: unless-stopped
volumes:
indexes:
inputs:
vectors:
postgres_data:
+10 -3
View File
@@ -1,7 +1,11 @@
name: docsgpt-oss
services:
frontend:
build: ../frontend
build:
context: ../frontend
# Vite dev server with hot reload over the bind mount below. The default
# target (what Docker Hub publishes) is a static build behind nginx.
target: dev
volumes:
- ../frontend/src:/app/src
environment:
@@ -18,8 +22,10 @@ services:
build:
context: ../application
args:
# Bake the optional docling engine (layout-model OCR backend, structured
# output) into the image: set INSTALL_DOCLING=true in ../.env or the shell.
# Optional extras to bake in (comma-separated): docling, milvus. The
# docling extra brings the layout-model parser/OCR backend and its
# models. INSTALL_DOCLING=true is the older spelling of EXTRAS=docling.
EXTRAS: ${EXTRAS:-}
INSTALL_DOCLING: ${INSTALL_DOCLING:-false}
# Bake the tesseract binary behind OCR_ENABLED=true (~35 MB): set
# INSTALL_TESSERACT=true in ../.env or the shell (setup.sh does this
@@ -53,6 +59,7 @@ services:
build:
context: ../application
args:
EXTRAS: ${EXTRAS:-}
INSTALL_DOCLING: ${INSTALL_DOCLING:-false}
INSTALL_TESSERACT: ${INSTALL_TESSERACT:-false}
# Consumes the default queue AND the dedicated `parsing` (read_document /
+60 -10
View File
@@ -17,9 +17,57 @@ Docker is the recommended method for deploying DocsGPT, providing a consistent a
**Important Note for Windows Users:** Docker Desktop on Windows generally requires the WSL 2 backend to function correctly, especially when using features like host networking which are utilized in DocsGPT's Docker Compose setup. Ensure WSL 2 is enabled and configured in Docker Desktop settings.
## Quickest Setup: Using DocsGPT Public API
## Quickest Setup: Pre-built Images, No Checkout
The fastest way to try out DocsGPT is by using the public API endpoint. This requires minimal configuration and no local LLM setup.
Every release publishes ready-to-run images to Docker Hub (`arc53/docsgpt`,
`arc53/docsgpt-fe`) and GitHub Container Registry (`ghcr.io/arc53/docsgpt`,
`ghcr.io/arc53/docsgpt-fe`) for `linux/amd64` and `linux/arm64`. The images
contain everything the default configuration needs (embedding models,
tokenizers, tiktoken's encoding), so a fresh container makes no downloads on
first use. You do not need the source tree to run them:
1. **Download the standalone Compose file** (also attached to every
[release](https://github.com/arc53/DocsGPT/releases)):
```bash
mkdir docsgpt && cd docsgpt
curl -fsSLO https://raw.githubusercontent.com/arc53/DocsGPT/main/deployment/docker-compose-standalone.yaml
```
2. **Create a `.env` next to it** with your settings, for example the public API:
```
LLM_PROVIDER=docsgpt
VITE_API_STREAMING=true
```
3. **Start it:**
```bash
docker compose -f docker-compose-standalone.yaml up -d
```
Then open [http://localhost:5173/](http://localhost:5173/). Data lives in
named Docker volumes; `docker compose -f docker-compose-standalone.yaml down`
keeps it and `down -v` removes it.
**Tags and variants.** `DOCSGPT_IMAGE_TAG` picks the version: a release such
as `0.20.0`, `latest` (the newest release, the default) or `develop` (follows
the `main` branch). `DOCSGPT_IMAGE_VARIANT` picks the flavour: empty for the
slim default image, or `-docling` for the image with the docling parser
engine, its models and tesseract baked in (needed for OCR of scanned
documents, see the [OCR guide](/Guides/ocr)). Both are read from `.env` or
the shell, e.g. `DOCSGPT_IMAGE_TAG=0.20.0 DOCSGPT_IMAGE_VARIANT=-docling`.
The same two variables drive `deployment/docker-compose-hub.yaml` in a
checkout.
## Using the Source Checkout
With a clone of the repository, `deployment/docker-compose-hub.yaml` runs the
same pre-built images while keeping your data in `application/indexes`,
`application/inputs` and `application/vectors`, and `deployment/docker-compose.yaml`
builds the images from your working tree (for local changes, or a build with
extra packages: `EXTRAS=docling` in `.env`).
1. **Clone the DocsGPT Repository (if you haven't already):**
@@ -48,10 +96,12 @@ The fastest way to try out DocsGPT is by using the public API endpoint. This req
Navigate to the root directory of the DocsGPT repository in your terminal and run:
```bash
docker compose --env-file .env -f deployment/docker-compose.yaml up -d
docker compose --env-file .env -f deployment/docker-compose-hub.yaml up -d
```
The `-d` flag runs Docker Compose in detached mode (in the background).
To build the images from your working tree instead of pulling them, use
`deployment/docker-compose.yaml` with `up --build -d`.
5. **Access DocsGPT in your browser:**
@@ -62,7 +112,7 @@ The fastest way to try out DocsGPT is by using the public API endpoint. This req
To stop the application, navigate to the same directory in your terminal and run:
```bash
docker compose -f deployment/docker-compose.yaml down
docker compose -f deployment/docker-compose-hub.yaml down
```
## Optional Ollama Setup (Local Models)
@@ -84,11 +134,11 @@ There are two Ollama optional files:
**CPU:**
```bash
docker compose --env-file .env -f deployment/docker-compose.yaml -f deployment/optional/docker-compose.optional.ollama-cpu.yaml up -d
docker compose --env-file .env -f deployment/docker-compose-hub.yaml -f deployment/optional/docker-compose.optional.ollama-cpu.yaml up -d
```
**GPU:**
```bash
docker compose --env-file .env -f deployment/docker-compose.yaml -f deployment/optional/docker-compose.optional.ollama-gpu.yaml up -d
docker compose --env-file .env -f deployment/docker-compose-hub.yaml -f deployment/optional/docker-compose.optional.ollama-gpu.yaml up -d
```
3. **Pull the Ollama Model:**
@@ -96,11 +146,11 @@ There are two Ollama optional files:
**Crucially, after launching with Ollama, you need to pull the desired model into the Ollama container.** Find the `LLM_NAME` you configured in your `.env` file (e.g., `llama3.2:1b`). Then execute the following command to pull the model *inside* the running Ollama container:
```bash
docker compose -f deployment/docker-compose.yaml -f deployment/optional/docker-compose.optional.ollama-cpu.yaml exec -it ollama ollama pull <LLM_NAME>
docker compose -f deployment/docker-compose-hub.yaml -f deployment/optional/docker-compose.optional.ollama-cpu.yaml exec -it ollama ollama pull <LLM_NAME>
```
or (for GPU):
```bash
docker compose -f deployment/docker-compose.yaml -f deployment/optional/docker-compose.optional.ollama-gpu.yaml exec -it ollama ollama pull <LLM_NAME>
docker compose -f deployment/docker-compose-hub.yaml -f deployment/optional/docker-compose.optional.ollama-gpu.yaml exec -it ollama ollama pull <LLM_NAME>
```
Replace `<LLM_NAME>` with the actual model name from your `.env` file.
@@ -113,12 +163,12 @@ There are two Ollama optional files:
To stop a DocsGPT setup launched with Ollama optional files, use `docker compose down` and include all the compose files used during the `up` command:
```bash
docker compose -f deployment/docker-compose.yaml -f deployment/optional/docker-compose.optional.ollama-cpu.yaml down
docker compose -f deployment/docker-compose-hub.yaml -f deployment/optional/docker-compose.optional.ollama-cpu.yaml down
```
or
```bash
docker compose -f deployment/docker-compose.yaml -f deployment/optional/docker-compose.optional.ollama-gpu.yaml down
docker compose -f deployment/docker-compose-hub.yaml -f deployment/optional/docker-compose.optional.ollama-gpu.yaml down
```
**Important for GPU Usage:**
+25 -11
View File
@@ -64,11 +64,17 @@ docker build --build-arg INSTALL_TESSERACT=true ./application
`INSTALL_TESSERACT=true` in `.env` (or the shell) bakes tesseract plus the
English pack into locally built backend and worker images; `setup.sh` writes
it when you answer yes to the OCR question after choosing to build images
locally. Pre-built Docker Hub images (`docker-compose-hub.yaml`) do not
include it, so `setup.sh` leaves OCR at its default (off) for them; to OCR
there, point `OCR_ENGINE=deepseek` at a DeepSeek-OCR endpoint or install
`tesseract-ocr` in a derived image. With `OCR_ENABLED=true` and no binary on
`PATH`, scanned pages fail with an install hint (text-layer documents are
locally.
With pre-built images the switch is the image variant: every tag is
published twice, slim (`arc53/docsgpt:<tag>`) and `-docling`
(`arc53/docsgpt:<tag>-docling`), and the latter bakes tesseract, the docling
engine and its models in. Set `DOCSGPT_IMAGE_VARIANT=-docling` in `.env` for
`docker-compose-hub.yaml` or `docker-compose-standalone.yaml`; `setup.sh`
writes it when you answer yes to the OCR question with Docker Hub images.
Alternatively point `OCR_ENGINE=deepseek` at a DeepSeek-OCR endpoint, which
needs no system package. With `OCR_ENABLED=true` and no binary on `PATH`,
scanned pages fail with an install hint (text-layer documents are
unaffected).
<Callout type="warning" emoji="⚠️">
@@ -88,19 +94,27 @@ docling is not part of the base install, and OCR does not need it (see
output:
```bash
pip install -r application/requirements-docling.txt
pip install -r application/requirements-docling.txt # or: uv sync --extra docling
```
Docker images build without it by default; opt in with the build argument:
That file is the core set plus the `docling` extra, exported from the same
lock. On Linux it takes torch from the CPU-only PyTorch index, so the extra
costs about 1.5 GB rather than the 2.7 GB the CUDA build of torch would; a
GPU deployment can reinstall torch from PyPI on top.
Pre-built images: use the `-docling` variant (`arc53/docsgpt:<tag>-docling`,
`DOCSGPT_IMAGE_VARIANT=-docling` in `.env`), which also bakes docling's
layout, table-structure and RapidOCR models in so the first parse does not
download them. Local builds opt in with the build argument:
```bash
docker build --build-arg INSTALL_DOCLING=true ./application
docker build --build-arg EXTRAS=docling ./application
```
`deployment/docker-compose.yaml` forwards the same switch, so setting
`INSTALL_DOCLING=true` in `.env` (or the shell) bakes docling into locally
built backend and worker images; `setup.sh` offers it as a follow-up to the
OCR question. Compose reads build arguments from the shell or from the
`EXTRAS=docling` (or the older `INSTALL_DOCLING=true`) in `.env` (or the
shell) bakes docling into locally built backend and worker images; `setup.sh`
offers it as a follow-up to the OCR question. Compose reads build arguments from the shell or from the
`.env` you pass with `--env-file .env` (not from the containers' `env_file`),
so build with `docker compose --env-file .env -f deployment/docker-compose.yaml build`
as `setup.sh` does. Pre-built Docker Hub images (`docker-compose-hub.yaml`)
+7
View File
@@ -0,0 +1,7 @@
node_modules/
dist/
.env
.env.*
Dockerfile
.dockerignore
*.log
+45 -4
View File
@@ -1,11 +1,52 @@
FROM node:22-bullseye-slim
# DocsGPT frontend image: a static production build served by nginx.
#
# Vite inlines VITE_* settings at build time, so the old image ran the Vite
# dev server just to read VITE_API_HOST from the container environment. This
# image builds once and injects the container's VITE_* variables at start-up
# instead (docker/40-runtime-env.sh writes them to /config.js, which the app
# reads before its own bundle; see src/env.ts). Same env vars, same port.
#
# Targets:
# (default) nginx serving the built bundle -- what Docker Hub publishes
# dev the Vite dev server with hot reload, for docker-compose.yaml's
# bind-mounted frontend (build: target: dev)
FROM node:22-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm install
RUN npm ci --no-audit --no-fund
FROM deps AS dev
COPY . .
# vite.config.ts polls the filesystem when DOCKER is set: native fs events do
# not cross a Windows host into a Linux container.
ENV DOCKER=1
EXPOSE 5173
CMD ["npm", "run", "dev", "--", "--host"]
CMD [ "npm", "run", "dev", "--" , "--host"]
FROM deps AS build
COPY . .
# Bake nothing deployment-specific; the runtime script supplies it.
ENV VITE_API_HOST=""
RUN npm run build && \
sed -i 's|<head>|<head><script src="/config.js"></script>|' dist/index.html
FROM nginx:1.27-alpine
LABEL org.opencontainers.image.source="https://github.com/arc53/DocsGPT" \
org.opencontainers.image.title="DocsGPT frontend" \
org.opencontainers.image.licenses="MIT"
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
COPY docker/40-runtime-env.sh /docker-entrypoint.d/40-runtime-env.sh
RUN chmod +x /docker-entrypoint.d/40-runtime-env.sh
COPY --from=build /app/dist /usr/share/nginx/html
# Same port the dev server used, so compose files and docs keep working.
EXPOSE 5173
+23
View File
@@ -0,0 +1,23 @@
#!/bin/sh
# Expose the container's VITE_* environment to the static bundle.
#
# nginx's entrypoint runs everything in /docker-entrypoint.d before serving.
# The generated /config.js is loaded by index.html ahead of the app bundle and
# read by src/env.ts, so VITE_API_HOST and friends can differ per deployment
# without rebuilding the image.
set -eu
out=/usr/share/nginx/html/config.js
{
printf 'window.__DOCSGPT_ENV__ = {'
first=1
env | grep -E '^VITE_[A-Za-z0-9_]+=' | while IFS='=' read -r key value; do
# JSON-escape backslashes and double quotes; values are plain URLs/ids.
escaped=$(printf '%s' "$value" | sed 's/\\/\\\\/g; s/"/\\"/g')
if [ "$first" -eq 1 ]; then first=0; else printf ','; fi
printf '"%s":"%s"' "$key" "$escaped"
done
printf '};\n'
} > "$out"
echo "runtime-env: wrote $(grep -o 'VITE_[A-Za-z0-9_]*' "$out" | wc -l | tr -d ' ') VITE_* values to /config.js"
+25
View File
@@ -0,0 +1,25 @@
server {
listen 5173;
server_name _;
root /usr/share/nginx/html;
index index.html;
gzip on;
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
# Hashed bundle assets are immutable; index.html and config.js are not.
location /assets/ {
add_header Cache-Control "public, max-age=31536000, immutable";
try_files $uri =404;
}
location = /config.js {
add_header Cache-Control "no-store";
}
# Single-page app: every unknown path renders index.html.
location / {
add_header Cache-Control "no-cache";
try_files $uri $uri/ /index.html;
}
}
+3 -2
View File
@@ -1,3 +1,4 @@
import { envVar } from '@/env';
import './locale/i18n';
import { useState } from 'react';
@@ -108,8 +109,8 @@ export default function App() {
const saved = localStorage.getItem('showNotification');
return saved ? JSON.parse(saved) : true;
});
const notificationText = import.meta.env.VITE_NOTIFICATION_TEXT;
const notificationLink = import.meta.env.VITE_NOTIFICATION_LINK;
const notificationText = envVar('VITE_NOTIFICATION_TEXT');
const notificationLink = envVar('VITE_NOTIFICATION_LINK');
// Hide the changelog banner on public share routes — those pages are
// embedded / shared externally and shouldn't carry product chrome.
const isPublicShareRoute =
+2 -1
View File
@@ -1,3 +1,4 @@
import { envVar } from '@/env';
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
import {
@@ -26,7 +27,7 @@ const initialState: ConversationState = {
conversationId: null,
};
const API_STREAMING = import.meta.env.VITE_API_STREAMING === 'true';
const API_STREAMING = envVar('VITE_API_STREAMING') === 'true';
let abortController: AbortController | null = null;
export function handlePreviewAbort() {
+2 -1
View File
@@ -1,7 +1,8 @@
import { envVar } from '@/env';
import { withThrottle, type FetchLike } from './throttle';
export const baseURL =
import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
envVar('VITE_API_HOST') || 'https://docsapi.arc53.com';
const getHeaders = (
token: string | null,
@@ -1,3 +1,4 @@
import { envVar } from '@/env';
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import drivePickerImport from 'react-google-drive-picker';
@@ -121,9 +122,9 @@ const GoogleDrivePicker: React.FC<GoogleDrivePickerProps> = ({
}
try {
const clientId: string = import.meta.env.VITE_GOOGLE_CLIENT_ID;
const clientId: string = envVar('VITE_GOOGLE_CLIENT_ID');
const developerKey: string =
import.meta.env.VITE_GOOGLE_PICKER_API_KEY ?? '';
envVar('VITE_GOOGLE_PICKER_API_KEY') ?? '';
// Derive appId from clientId (extract numeric part before first dash)
const appId = clientId ? clientId.split('-')[0] : null;
+3 -2
View File
@@ -1,3 +1,4 @@
import { envVar } from '@/env';
import {
useCallback,
useEffect,
@@ -63,7 +64,7 @@ const LIVE_TRANSCRIPTION_TIMESLICE_MS = 1000;
const LIVE_CAPTURE_SAMPLE_RATE = 16000;
const LIVE_CAPTURE_MAX_BUFFER_SECONDS = 20;
const LIVE_SILENCE_RMS_THRESHOLD = 0.015;
const ENABLE_VOICE_INPUT = import.meta.env.VITE_ENABLE_VOICE_INPUT === 'true';
const ENABLE_VOICE_INPUT = envVar('VITE_ENABLE_VOICE_INPUT') === 'true';
type AudioContextWindow = Window &
typeof globalThis & {
@@ -526,7 +527,7 @@ export default function MessageInput({
if (supported.length === 0) return;
const files = supported;
const apiHost = import.meta.env.VITE_API_HOST;
const apiHost = envVar('VITE_API_HOST');
if (files.length > 1) {
const formData = new FormData();
@@ -1,3 +1,4 @@
import { envVar } from '@/env';
import 'katex/dist/katex.min.css';
import { Pencil } from 'lucide-react';
@@ -37,7 +38,7 @@ import ResearchProgress from './ResearchProgress';
import { ToolCallsType } from './types';
import { wikiWriteActionKey, wikiWritePath } from './wikiToolCall';
const DisableSourceFE = import.meta.env.VITE_DISABLE_SOURCE_FE || false;
const DisableSourceFE = envVar('VITE_DISABLE_SOURCE_FE') || false;
const ConversationBubble = forwardRef<
HTMLDivElement,
@@ -1,3 +1,4 @@
import { envVar } from '@/env';
import {
createAsyncThunk,
createListenerMiddleware,
@@ -90,8 +91,8 @@ const initialState: ConversationState = {
conversationId: null,
};
const API_STREAMING = import.meta.env.VITE_API_STREAMING === 'true';
const USE_V1_API = import.meta.env.VITE_USE_V1_API === 'true';
const API_STREAMING = envVar('VITE_API_STREAMING') === 'true';
const USE_V1_API = envVar('VITE_USE_V1_API') === 'true';
let abortController: AbortController | null = null;
export function handleAbort() {
@@ -1,3 +1,4 @@
import { envVar } from '@/env';
import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
import store from '../store';
@@ -12,7 +13,7 @@ import {
clearAttachments,
} from '../upload/uploadSlice';
const API_STREAMING = import.meta.env.VITE_API_STREAMING === 'true';
const API_STREAMING = envVar('VITE_API_STREAMING') === 'true';
interface SharedConversationsType {
queries: Query[];
apiKey?: string;
+24
View File
@@ -0,0 +1,24 @@
/**
* Runtime-overridable build settings.
*
* Vite inlines `import.meta.env.VITE_*` at build time, which forced the
* Docker image to run the dev server so `VITE_API_HOST` could change per
* deployment. The production image serves a static build instead and writes
* the container's `VITE_*` environment into `window.__DOCSGPT_ENV__` (see
* frontend/docker/40-runtime-env.sh). That object wins over the build-time
* value; outside Docker nothing sets it and the build-time value applies.
*/
declare global {
interface Window {
__DOCSGPT_ENV__?: Record<string, string | undefined>;
}
}
export function envVar(name: string): string {
const runtime =
typeof window !== 'undefined' ? window.__DOCSGPT_ENV__?.[name] : undefined;
if (runtime !== undefined && runtime !== '') return runtime;
const buildTime = (import.meta.env as Record<string, unknown>)[name];
return typeof buildTime === 'string' ? buildTime : '';
}
+2 -1
View File
@@ -1,3 +1,4 @@
import { envVar } from '@/env';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
@@ -13,7 +14,7 @@ import { ActiveState } from '../models/misc';
import { selectToken } from '../preferences/preferenceSlice';
import ConfirmationModal from './ConfirmationModal';
const baseURL = import.meta.env.VITE_BASE_URL;
const baseURL = envVar('VITE_BASE_URL');
type AgentDetailsModalProps = {
agent: Agent;
+3 -2
View File
@@ -1,3 +1,4 @@
import { envVar } from '@/env';
import { useCallback, useEffect, useState } from 'react';
import { nanoid } from '@reduxjs/toolkit';
import { useDropzone } from 'react-dropzone';
@@ -575,7 +576,7 @@ function Upload({
JSON.stringify(optionsToConfig(retrievalOptions)),
);
const apiHost = import.meta.env.VITE_API_HOST;
const apiHost = envVar('VITE_API_HOST');
const xhr = new XMLHttpRequest();
dispatch(
@@ -706,7 +707,7 @@ function Upload({
formData.append('data', JSON.stringify(configData));
const apiHost: string = import.meta.env.VITE_API_HOST;
const apiHost: string = envVar('VITE_API_HOST');
const endpoint =
ingestor.type === 'local_file'
? `${apiHost}/api/upload`
+4 -3
View File
@@ -1,3 +1,4 @@
import { envVar } from '@/env';
import CrawlerIcon from '../../assets/crawler.svg';
import FileUploadIcon from '../../assets/file_upload.svg';
import UrlIcon from '../../assets/url.svg';
@@ -146,7 +147,7 @@ export const IngestorFormSchemas: IngestorSchema[] = [
icon: DriveIcon,
heading: 'Upload from Google Drive',
validate: () => {
const googleClientId = import.meta.env.VITE_GOOGLE_CLIENT_ID;
const googleClientId = envVar('VITE_GOOGLE_CLIENT_ID');
return !!googleClientId;
},
fields: [
@@ -208,7 +209,7 @@ export const IngestorFormSchemas: IngestorSchema[] = [
icon: SharePoint,
heading: 'Upload from Share Point',
validate: () => {
const sharePointClientId = import.meta.env.VITE_SHARE_POINT_CLIENT_ID;
const sharePointClientId = envVar('VITE_SHARE_POINT_CLIENT_ID');
return !!sharePointClientId;
},
fields: [
@@ -226,7 +227,7 @@ export const IngestorFormSchemas: IngestorSchema[] = [
icon: ConfluenceIcon,
heading: 'Upload from Confluence',
validate: () => {
const confluenceClientId = import.meta.env.VITE_CONFLUENCE_CLIENT_ID;
const confluenceClientId = envVar('VITE_CONFLUENCE_CLIENT_ID');
return !!confluenceClientId;
},
fields: [
+24 -21
View File
@@ -513,29 +513,32 @@ function Configure-DocProcessing {
Write-ColorText "PDF-as-image parsing enabled." -ForegroundColor "Green"
}
# OCR needs the tesseract binary, an optional system package that only
# locally built images can include (INSTALL_TESSERACT build arg). The
# pre-built Docker Hub images ship without it, so there OCR stays off
# (its default) rather than being switched on to fail on every scan.
if ($COMPOSE_FILE -ne $COMPOSE_FILE_LOCAL) {
Write-ColorText "OCR for scanned PDFs and images stays off: the pre-built Docker Hub images do not include tesseract. To use OCR, rerun setup and choose option 5 (build images locally), or use a DeepSeek-OCR endpoint by adding OCR_ENABLED=true, OCR_ENGINE=deepseek and OCR_DEEPSEEK_URL=<endpoint> to .env." -ForegroundColor "Yellow"
# OCR needs the tesseract binary. The default (slim) images ship without
# it; the pre-built "-docling" image variant bakes tesseract, the docling
# layout engine and its models in, so with Docker Hub images OCR means
# switching the variant. Locally built images get it via build args.
$ocr_enabled = Read-Host "Enable OCR for scanned PDFs and images? (y/N)"
if (-not ($ocr_enabled -eq "y" -or $ocr_enabled -eq "Y")) {
return
}
$ocr_enabled = Read-Host "Enable OCR for scanned PDFs and images? (y/N)"
if ($ocr_enabled -eq "y" -or $ocr_enabled -eq "Y") {
"OCR_ENABLED=true" | Add-Content -Path $ENV_FILE -Encoding utf8
# Bakes tesseract into the locally built images (docker compose
# --env-file .env build).
"INSTALL_TESSERACT=true" | Add-Content -Path $ENV_FILE -Encoding utf8
Write-ColorText "OCR enabled. tesseract will be built into the images (INSTALL_TESSERACT=true); for a DeepSeek-OCR endpoint instead, set OCR_ENGINE=deepseek and OCR_DEEPSEEK_URL=<endpoint> in .env." -ForegroundColor "Green"
$docling_ocr = Read-Host "Also install the Docling layout engine for OCR (better tables/reading order, several GB heavier)? (y/N)"
if ($docling_ocr -eq "y" -or $docling_ocr -eq "Y") {
# Locally built images include docling via this build arg; it becomes
# the OCR backend automatically (OCR_BACKEND=auto).
"INSTALL_DOCLING=true" | Add-Content -Path $ENV_FILE -Encoding utf8
Write-ColorText "Docling will be built into locally built images (docker compose --env-file .env build). Pre-built Docker Hub images do not include it." -ForegroundColor "Green"
}
"OCR_ENABLED=true" | Add-Content -Path $ENV_FILE -Encoding utf8
if ($COMPOSE_FILE -ne $COMPOSE_FILE_LOCAL) {
# Pre-built images: pull arc53/docsgpt:<tag>-docling instead of the
# slim default (about 1.5 GB more to download).
"DOCSGPT_IMAGE_VARIANT=-docling" | Add-Content -Path $ENV_FILE -Encoding utf8
Write-ColorText "OCR enabled. The -docling image variant will be pulled (tesseract, docling layout engine and its models included). For a DeepSeek-OCR endpoint instead, set OCR_ENGINE=deepseek and OCR_DEEPSEEK_URL=<endpoint> in .env." -ForegroundColor "Green"
return
}
# Bakes tesseract into the locally built images (docker compose
# --env-file .env build).
"INSTALL_TESSERACT=true" | Add-Content -Path $ENV_FILE -Encoding utf8
Write-ColorText "OCR enabled. tesseract will be built into the images (INSTALL_TESSERACT=true); for a DeepSeek-OCR endpoint instead, set OCR_ENGINE=deepseek and OCR_DEEPSEEK_URL=<endpoint> in .env." -ForegroundColor "Green"
$docling_ocr = Read-Host "Also install the Docling layout engine for OCR (better tables/reading order, several GB heavier)? (y/N)"
if ($docling_ocr -eq "y" -or $docling_ocr -eq "Y") {
# Locally built images include docling via this build arg; it becomes
# the OCR backend automatically (OCR_BACKEND=auto).
"INSTALL_DOCLING=true" | Add-Content -Path $ENV_FILE -Encoding utf8
Write-ColorText "Docling will be built into locally built images (docker compose --env-file .env build)." -ForegroundColor "Green"
}
}
+24 -21
View File
@@ -367,29 +367,32 @@ configure_doc_processing() {
echo -e "${GREEN}PDF-as-image parsing enabled.${NC}"
fi
# OCR needs the tesseract binary, an optional system package that only
# locally built images can include (INSTALL_TESSERACT build arg). The
# pre-built Docker Hub images ship without it, so there OCR stays off
# (its default) rather than being switched on to fail on every scan.
if [[ "$COMPOSE_FILE" != "$COMPOSE_FILE_LOCAL" ]]; then
echo -e "${YELLOW}OCR for scanned PDFs and images stays off: the pre-built Docker Hub images do not include tesseract. To use OCR, rerun setup and choose option 5 (build images locally), or use a DeepSeek-OCR endpoint by adding OCR_ENABLED=true, OCR_ENGINE=deepseek and OCR_DEEPSEEK_URL=<endpoint> to .env.${NC}"
# OCR needs the tesseract binary. The default (slim) images ship without
# it; the pre-built "-docling" image variant bakes tesseract, the docling
# layout engine and its models in, so with Docker Hub images OCR means
# switching the variant. Locally built images get it via build args.
read -p "$(echo -e "${DEFAULT_FG}Enable OCR for scanned PDFs and images? (y/N): ${NC}")" ocr_enabled
if [[ ! "$ocr_enabled" =~ ^[yY]$ ]]; then
return
fi
read -p "$(echo -e "${DEFAULT_FG}Enable OCR for scanned PDFs and images? (y/N): ${NC}")" ocr_enabled
if [[ "$ocr_enabled" =~ ^[yY]$ ]]; then
echo "OCR_ENABLED=true" >> "$ENV_FILE"
# Bakes tesseract into the locally built images (docker compose
# --env-file .env build).
echo "INSTALL_TESSERACT=true" >> "$ENV_FILE"
echo -e "${GREEN}OCR enabled. tesseract will be built into the images (INSTALL_TESSERACT=true); for a DeepSeek-OCR endpoint instead, set OCR_ENGINE=deepseek and OCR_DEEPSEEK_URL=<endpoint> in .env.${NC}"
read -p "$(echo -e "${DEFAULT_FG}Also install the Docling layout engine for OCR (better tables/reading order, several GB heavier)? (y/N): ${NC}")" docling_ocr
if [[ "$docling_ocr" =~ ^[yY]$ ]]; then
# Locally built images include docling via this build arg; it becomes
# the OCR backend automatically (OCR_BACKEND=auto).
echo "INSTALL_DOCLING=true" >> "$ENV_FILE"
echo -e "${GREEN}Docling will be built into locally built images (docker compose --env-file .env build). Pre-built Docker Hub images do not include it.${NC}"
fi
echo "OCR_ENABLED=true" >> "$ENV_FILE"
if [[ "$COMPOSE_FILE" != "$COMPOSE_FILE_LOCAL" ]]; then
# Pre-built images: pull arc53/docsgpt:<tag>-docling instead of the
# slim default (about 1.5 GB more to download).
echo "DOCSGPT_IMAGE_VARIANT=-docling" >> "$ENV_FILE"
echo -e "${GREEN}OCR enabled. The -docling image variant will be pulled (tesseract, docling layout engine and its models included). For a DeepSeek-OCR endpoint instead, set OCR_ENGINE=deepseek and OCR_DEEPSEEK_URL=<endpoint> in .env.${NC}"
return
fi
# Bakes tesseract into the locally built images (docker compose
# --env-file .env build).
echo "INSTALL_TESSERACT=true" >> "$ENV_FILE"
echo -e "${GREEN}OCR enabled. tesseract will be built into the images (INSTALL_TESSERACT=true); for a DeepSeek-OCR endpoint instead, set OCR_ENGINE=deepseek and OCR_DEEPSEEK_URL=<endpoint> in .env.${NC}"
read -p "$(echo -e "${DEFAULT_FG}Also install the Docling layout engine for OCR (better tables/reading order, several GB heavier)? (y/N): ${NC}")" docling_ocr
if [[ "$docling_ocr" =~ ^[yY]$ ]]; then
# Locally built images include docling via this build arg; it becomes
# the OCR backend automatically (OCR_BACKEND=auto).
echo "INSTALL_DOCLING=true" >> "$ENV_FILE"
echo -e "${GREEN}Docling will be built into locally built images (docker compose --env-file .env build).${NC}"
fi
}