6.9 KiB
Lightweight Docker Image Without AI/ML Tools
Date: 2026-04-04 Issue: stirling-image/stirling-image#1 Status: Design approved
Problem
The full Docker image is ~11 GB, mostly Python ML dependencies (rembg, RealESRGAN, PaddleOCR, MediaPipe, LaMa) and pre-downloaded model weights. Users on constrained hardware (Raspberry Pi, small VPS) or those who only need image processing tools are paying for size they don't use. First community feedback on r/selfhosted flagged this.
Solution
Ship a :lite Docker tag that drops the Python sidecar and all ML dependencies. Keep every Sharp-based tool. Target size: 1-2 GB.
Decisions
| Decision | Choice | Rationale |
|---|---|---|
| Build strategy | Single Dockerfile, ARG VARIANT=full |
One file to maintain. Avoids drift between two Dockerfiles. |
| Detection mechanism | Build-time ENV STIRLING_VARIANT |
Explicit, instant, testable. No startup probing. |
| API behavior (lite) | AI routes return 501 | Clear signal vs confusing 404. Tells consumers what to do. |
| Frontend-API bridge | Extend /v1/settings response |
Reuses existing fetch. Avoids extra endpoint complexity. |
| Frontend UX | Grey out AI tools + "AI" badge + toast on click | Users see what they're missing. Toast links to docs for upgrade path. |
| Tag naming | :lite / :latest (full) |
"lite" = fewer features (accurate). "slim" = smaller OS base (misleading in Docker convention). |
| Feature scope | All Sharp tools stay, 5 Python tools dropped | Sharp tools add zero meaningful size. All savings come from Python. |
| Shared constants | PYTHON_SIDECAR_TOOLS in packages/shared/ |
Single source of truth for AI tool IDs across API and frontend. |
Architecture
Dockerfile (docker/Dockerfile)
A build arg controls the variant, defaulting to full:
ARG VARIANT=full
In the production stage, the Python installation block (system packages, venv, pip installs, model downloads) is wrapped in a shell conditional:
ARG VARIANT
RUN if [ "$VARIANT" = "full" ]; then \
apt-get install -y python3 python3-pip python3-venv python3-dev \
tesseract-ocr tesseract-ocr-deu tesseract-ocr-fra \
tesseract-ocr-spa tesseract-ocr-chi-sim \
build-essential libgl1 libglib2.0-0 && \
python3 -m venv /opt/venv && \
/opt/venv/bin/pip install ... && \
python3 docker/download_models.py && \
... model downloads ... \
; fi
A runtime env var is set from the build arg:
ENV STIRLING_VARIANT=${VARIANT}
Packages kept in both variants (used by Sharp-based tools): imagemagick, libraw-dev, potrace, libheif-examples, gosu.
Packages dropped in lite: python3, python3-pip, python3-venv, python3-dev, tesseract-ocr (+ language packs), build-essential, libgl1, libglib2.0-0.
Base image stays node:22-bookworm for both variants. Switching lite to bookworm-slim is a future optimization, not in scope for the first pass.
Building the lite image: docker build --build-arg VARIANT=lite -t stirling-image:lite .
Shared Constants (packages/shared/)
A new constant in packages/shared/src/constants.ts:
export const PYTHON_SIDECAR_TOOLS = [
"remove-background",
"upscale",
"blur-faces",
"erase-object",
"ocr",
] as const;
This replaces the hardcoded AI_PYTHON_TOOLS set in apps/web/src/hooks/use-tool-processor.ts and is used by the API for route registration and settings response.
API Changes (apps/api/)
Route registration (apps/api/src/routes/tools/index.ts):
When STIRLING_VARIANT === "lite", the 5 AI tool routes are registered as lightweight stub handlers returning 501. The @stirling-image/ai package is not imported at all in lite mode (conditional import), avoiding any accidental Python spawn attempt.
if (process.env.STIRLING_VARIANT !== "lite") {
// Register actual AI tool routes (import @stirling-image/ai)
} else {
// Register stub routes returning 501 for each PYTHON_SIDECAR_TOOLS entry
}
The 501 response:
{
"statusCode": 501,
"error": "Not Available",
"message": "This tool requires the full image. See docs at <link>"
}
Settings endpoint (/v1/settings response):
Two new fields, derived at startup from process.env.STIRLING_VARIANT and the shared constant. Not stored in the database.
{
"...existing settings...",
"variant": "lite",
"variantUnavailableTools": ["remove-background", "upscale", "blur-faces", "erase-object", "ocr"]
}
In full mode: variant: "full", variantUnavailableTools: [].
Frontend Changes (apps/web/)
Settings state: Variant info is fetched once via a shared Zustand store (or shared hook) so both ToolPanel and HomePage access it without duplicate requests.
Tool rendering: Tools listed in variantUnavailableTools are rendered greyed out with an "AI" badge. Clicking shows a toast: "This tool requires the full image" with a link to the docs page.
This is distinct from user-disabled tools (disabledTools), which are hidden entirely with no toast.
use-tool-processor.ts: Replaces the hardcoded AI_PYTHON_TOOLS set with the shared PYTHON_SIDECAR_TOOLS constant import.
CI/CD Changes (.github/workflows/release.yml)
The Docker build job uses a matrix strategy:
strategy:
matrix:
variant: [full, lite]
Tags published per variant:
| Variant | Tags |
|---|---|
| full | latest, 1.6.0, 1.6, 1 |
| lite | lite, 1.6.0-lite, 1.6-lite, 1-lite |
Both variants are multi-arch (linux/amd64,linux/arm64) and pushed to Docker Hub (stirlingimage/stirling-image) and GHCR (ghcr.io/stirling-image/stirling-image).
The CI workflow (ci.yml) also builds both variants as a smoke test (build only, no push).
Documentation (apps/docs/)
A new docs page covering:
- What the lite image is and why it exists
- Which tools are included vs excluded (the 5 AI tools)
- Pull commands:
docker pull stirlingimage/stirling-image:lite - Docker Compose examples for both variants
- How to switch from lite to full when AI tools are needed
This is the page linked from the frontend toast and the 501 API response.
Tools by Variant
Included in lite (all Sharp-based, ~27 tools)
resize, crop, rotate, convert, compress, strip-metadata, color-adjustments, watermark-text, watermark-image, text-overlay, compose, info, compare, find-duplicates, color-palette, qr-generate, barcode-read, collage, split, border, svg-to-raster, vectorize, gif-tools, bulk-rename, favicon, image-to-pdf, replace-color, smart-crop
Excluded from lite (Python sidecar required, 5 tools)
remove-background, upscale, blur-faces, erase-object, ocr
Testing
- Build both variants in CI and verify they start successfully
- Verify lite image does not contain Python, pip, or model weights
- Verify AI routes return 501 in lite mode
- Verify frontend shows greyed-out AI tools with correct toast in lite mode
- Verify all Sharp-based tools work identically in both variants
- Verify lite image size is in the 1-2 GB target range