Skip to main content
Docker is the recommended way to self-host BentoPDF in a production environment. The official image is based on nginx-unprivileged and runs as a non-root user on port 8080.
Office conversions require HTTPS on non-loopback addresses.LibreOffice-based tools (Word, Excel, PowerPoint conversion) require SharedArrayBuffer, which browsers only enable when the page is cross-origin isolated and served from a secure context. http://localhost works for same-device testing, but http://192.168.x.x or any LAN IP requires HTTPS. The official image already sends the required Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp headers — if you add a reverse proxy, ensure those headers are preserved.
All docker commands work with Podman. Replace docker with podman and docker-compose with podman-compose.

Quick start

docker run -d \
  --name bentopdf \
  -p 3000:8080 \
  --restart unless-stopped \
  ghcr.io/alam00000/bentopdf:latest
Open http://localhost:3000 in your browser.

Docker Compose

services:
  bentopdf:
    image: ghcr.io/alam00000/bentopdf:latest
    container_name: bentopdf
    ports:
      - '3000:8080'
    restart: unless-stopped
    healthcheck:
      test: ['CMD', 'wget', '--spider', '-q', 'http://localhost:8080']
      interval: 30s
      timeout: 10s
      retries: 3
docker compose up -d

Podman Quadlet (systemd integration)

Quadlet lets you run Podman containers as systemd services — ideal for production Linux deployments.

Basic Quadlet setup

Create a container unit file at ~/.config/containers/systemd/bentopdf.container (user) or /etc/containers/systemd/bentopdf.container (system):
bentopdf.container
[Unit]
Description=BentoPDF - Privacy-first PDF toolkit
After=network-online.target
Wants=network-online.target

[Container]
Image=ghcr.io/alam00000/bentopdf:latest
ContainerName=bentopdf
PublishPort=3000:8080
AutoUpdate=registry

[Service]
Restart=always
TimeoutStartSec=300

[Install]
WantedBy=default.target

Enable and start

# Reload systemd to detect the new unit
systemctl --user daemon-reload

# Start the service
systemctl --user start bentopdf

# Enable on boot
systemctl --user enable bentopdf

# Check status
systemctl --user status bentopdf

# Follow logs
journalctl --user -u bentopdf -f
For system-wide deployment, use systemctl without --user and place the unit file in /etc/containers/systemd/.

Quadlet with health check

bentopdf.container (with health check)
[Unit]
Description=BentoPDF with health monitoring
After=network-online.target
Wants=network-online.target

[Container]
Image=ghcr.io/alam00000/bentopdf:latest
ContainerName=bentopdf
PublishPort=3000:8080
AutoUpdate=registry
HealthCmd=wget --spider -q http://localhost:8080 || exit 1
HealthInterval=30s
HealthTimeout=10s
HealthRetries=3

[Service]
Restart=always
TimeoutStartSec=300

[Install]
WantedBy=default.target

Auto-update with Quadlet

# Enable the auto-update timer
systemctl --user enable --now podman-auto-update.timer

# Check for updates manually
podman auto-update

# Dry run — check without updating
podman auto-update --dry-run

Build arguments

All configuration is passed as Docker build arguments (baked into the image at build time, not available at runtime via -e).

Build options

Build argDescriptionDefault
SIMPLE_MODEHide branding, hero, FAQ, and footer — show only PDF toolsfalse
COMPRESSION_MODEAsset compression: g (gzip only), b (brotli only), o (none), all (all formats)all
BASE_URLDeploy to a subdirectory, e.g. /pdf-tools/ (must have leading and trailing slash)/

WASM module URLs

Build argDescriptionDefault
VITE_WASM_PYMUPDF_URLPyMuPDF WASM module URLhttps://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.16/
VITE_WASM_GS_URLGhostscript WASM module URLhttps://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm/assets/
VITE_WASM_CPDF_URLCoherentPDF WASM module URLhttps://cdn.jsdelivr.net/npm/coherentpdf/dist/

OCR configuration

Build argDescriptionDefault
VITE_TESSERACT_WORKER_URLOCR worker script URL(empty; uses Tesseract.js CDN)
VITE_TESSERACT_CORE_URLOCR core runtime directory(empty; uses Tesseract.js CDN)
VITE_TESSERACT_LANG_URLOCR traineddata directory(empty; uses Tesseract.js CDN)
VITE_TESSERACT_AVAILABLE_LANGUAGESComma-separated OCR language codes exposed in the UI(empty; shows full catalog)
VITE_OCR_FONT_BASE_URLOCR text-layer font directory (NotoSans)(empty; uses remote Noto font URLs)

Language and branding

Build argDescriptionDefault
VITE_DEFAULT_LANGUAGEDefault UI language for first-time visitorsen
VITE_BRAND_NAMEBrand name shown in header and footerBentoPDF
VITE_BRAND_LOGOLogo path relative to public/images/favicon-no-bg.svg
VITE_FOOTER_TEXTCustom footer/copyright text© 2026 BentoPDF. All rights reserved.
VITE_DEFAULT_LANGUAGE supported values: en, ar, be, fr, de, es, zh, zh-TW, vi, tr, id, it, pt, nl, da.

CORS proxy

Build argDescriptionDefault
VITE_CORS_PROXY_URLURL for the digital-signature CORS proxy (passed via --secret, never stored in image layers)(none)
Pass VITE_CORS_PROXY_URL as a BuildKit secret to avoid embedding it in image layers:
export VITE_CORS_PROXY_URL="https://your-worker.workers.dev"
DOCKER_BUILDKIT=1 docker build \
  --secret id=VITE_CORS_PROXY_URL,env=VITE_CORS_PROXY_URL \
  -t bentopdf .

Example: custom build

docker build \
  --build-arg VITE_BRAND_NAME="AcmePDF" \
  --build-arg VITE_BRAND_LOGO="images/acme-logo.svg" \
  --build-arg VITE_FOOTER_TEXT="© 2026 Acme Corp. Internal use only." \
  --build-arg VITE_DEFAULT_LANGUAGE=fr \
  --build-arg SIMPLE_MODE=true \
  -t acmepdf .

docker run -d -p 3000:8080 --restart unless-stopped acmepdf

Subdirectory deployment

To serve BentoPDF at a path other than / (e.g., https://example.com/pdf-tools/):
docker build --build-arg BASE_URL=/pdf-tools/ -t bentopdf-subdir .
docker run -d -p 3000:8080 bentopdf-subdir
# Accessible at http://localhost:3000/pdf-tools/
BASE_URL must have both a leading and a trailing slash — e.g., /pdf-tools/, not /pdf-tools.

Non-root Dockerfile (PUID/PGID)

For environments that require running as a specific user — NAS devices, Kubernetes with security contexts, organizational policies — BentoPDF provides Dockerfile.nonroot with LSIO-style PUID/PGID support.
The standard Dockerfile already runs as nginx-unprivileged (UID 101) and is recommended for most deployments. Use Dockerfile.nonroot only when you need a specific UID/GID.
# Build the non-root image
docker build -f Dockerfile.nonroot -t bentopdf-nonroot .

# Run with custom UID/GID
docker run -d \
  --name bentopdf \
  -p 3000:8080 \
  -e PUID=1000 \
  -e PGID=1000 \
  --restart unless-stopped \
  bentopdf-nonroot
VariableDescriptionDefault
PUIDUser ID to run as1000
PGIDGroup ID to run as1000
DISABLE_IPV6Disable the IPv6 listenerfalse
PUID and PGID cannot be 0 (root). The entrypoint validates inputs and exits with an error for invalid values.
The container starts as root, creates a user with the specified PUID/PGID, adjusts ownership on all writable directories, then drops privileges using su-exec. The nginx process runs entirely as your specified user. Docker Compose example:
services:
  bentopdf:
    build:
      context: .
      dockerfile: Dockerfile.nonroot
    container_name: bentopdf
    ports:
      - '3000:8080'
    environment:
      - PUID=1000
      - PGID=1000
    restart: unless-stopped

With Traefik

services:
  traefik:
    image: traefik:v2.10
    command:
      - '--providers.docker=true'
      - '--entrypoints.web.address=:80'
      - '--entrypoints.websecure.address=:443'
      - '--certificatesresolvers.letsencrypt.acme.email=you@example.com'
      - '--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json'
      - '--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web'
    ports:
      - '80:80'
      - '443:443'
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./letsencrypt:/letsencrypt

  bentopdf:
    image: ghcr.io/alam00000/bentopdf:latest
    labels:
      - 'traefik.enable=true'
      - 'traefik.http.routers.bentopdf.rule=Host(`pdf.example.com`)'
      - 'traefik.http.routers.bentopdf.entrypoints=websecure'
      - 'traefik.http.routers.bentopdf.tls.certresolver=letsencrypt'
      - 'traefik.http.services.bentopdf.loadbalancer.server.port=8080'
      - 'traefik.http.routers.bentopdf.middlewares=bentopdf-headers'
      - 'traefik.http.middlewares.bentopdf-headers.headers.customresponseheaders.Cross-Origin-Opener-Policy=same-origin'
      - 'traefik.http.middlewares.bentopdf-headers.headers.customresponseheaders.Cross-Origin-Embedder-Policy=require-corp'
    restart: unless-stopped

With Caddy

services:
  caddy:
    image: caddy:2
    ports:
      - '80:80'
      - '443:443'
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data

  bentopdf:
    image: ghcr.io/alam00000/bentopdf:latest
    restart: unless-stopped

volumes:
  caddy_data:
Caddyfile
pdf.example.com {
    reverse_proxy bentopdf:8080
    header Cross-Origin-Opener-Policy "same-origin"
    header Cross-Origin-Embedder-Policy "require-corp"
}

Resource limits

services:
  bentopdf:
    image: ghcr.io/alam00000/bentopdf:latest
    deploy:
      resources:
        limits:
          cpus: '1'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 128M

Updating

docker compose pull
docker compose up -d

Build docs developers (and LLMs) love