Files
bentopdf/docs/self-hosting/docker.md

12 KiB

Deploy with Docker / Podman

The easiest way to self-host BentoPDF in a production environment.

Important

Required Headers for Office File Conversion

LibreOffice-based tools (Word, Excel, PowerPoint conversion) require these HTTP headers for SharedArrayBuffer support:

  • Cross-Origin-Opener-Policy: same-origin
  • Cross-Origin-Embedder-Policy: require-corp

The official container images include these headers. If using a reverse proxy (Traefik, Caddy, etc.), ensure these headers are preserved or added.

Tip

Podman Users: All docker commands work with Podman by replacing docker with podman and docker-compose with podman-compose.

Quick Start

# Docker
docker run -d \
  --name bentopdf \
  -p 3000:8080 \
  --restart unless-stopped \
  ghcr.io/alam00000/bentopdf:latest

# Podman
podman run -d \
  --name bentopdf \
  -p 3000:8080 \
  ghcr.io/alam00000/bentopdf:latest

Docker Compose / Podman Compose

Create docker-compose.yml:

services:
  bentopdf:
    image: ghcr.io/alam00000/bentopdf:latest
    container_name: bentopdf
    ports:
      - '3000:8080'
    restart: unless-stopped
    healthcheck:
      test: ['CMD', 'curl', '-f', 'http://localhost:8080']
      interval: 30s
      timeout: 10s
      retries: 3

Run:

# Docker Compose
docker compose up -d

# Podman Compose
podman-compose up -d

Build Your Own Image

# Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginxinc/nginx-unprivileged:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 8080
CMD ["nginx", "-g", "daemon off;"]

Build and run:

docker build -t bentopdf:custom .
docker run -d -p 3000:8080 bentopdf:custom

Environment Variables

Variable Description Default
SIMPLE_MODE Build without LibreOffice tools false
BASE_URL Deploy to subdirectory /
VITE_WASM_PYMUPDF_URL PyMuPDF WASM module URL https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.14/
VITE_WASM_GS_URL Ghostscript WASM module URL https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm/assets/
VITE_WASM_CPDF_URL CoherentPDF WASM module URL https://cdn.jsdelivr.net/npm/coherentpdf/dist/
VITE_DEFAULT_LANGUAGE Default UI language en
VITE_BRAND_NAME Custom brand name BentoPDF
VITE_BRAND_LOGO Logo path relative to public/ images/favicon-no-bg.svg
VITE_FOOTER_TEXT Custom footer/copyright text © 2026 BentoPDF. All rights reserved.

WASM module URLs are pre-configured with CDN defaults — all advanced features work out of the box. Override these for air-gapped or self-hosted deployments.

VITE_DEFAULT_LANGUAGE sets the UI language for first-time visitors. Supported values: en, ar, be, fr, de, es, zh, zh-TW, vi, tr, id, it, pt, nl, da. Users can still switch languages — this only changes the default.

Example:

# Build with French as the default language
docker build --build-arg VITE_DEFAULT_LANGUAGE=fr -t bentopdf .
docker run -d -p 3000:8080 bentopdf

Custom Branding

Replace the default BentoPDF logo, name, and footer text with your own. Place your logo file in the public/ folder (or use an existing image), then pass the branding variables at build time:

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." \
  -t acmepdf .

Branding works in both full mode and Simple Mode, and can be combined with all other build-time options.

Custom WASM URLs (Air-Gapped / Self-Hosted)

Important

WASM URLs are baked into the JavaScript at build time. The WASM files are downloaded by the user's browser at runtime — Docker does not download them during the build. For air-gapped networks, you must host the WASM files on an internal server that browsers can reach.

Full air-gapped workflow:

# 1. On a machine WITH internet — download WASM packages
npm pack @bentopdf/pymupdf-wasm@0.11.14
npm pack @bentopdf/gs-wasm
npm pack coherentpdf

# 2. Build the image with your internal server URLs
docker build \
  --build-arg VITE_WASM_PYMUPDF_URL=https://internal-server.example.com/wasm/pymupdf/ \
  --build-arg VITE_WASM_GS_URL=https://internal-server.example.com/wasm/gs/ \
  --build-arg VITE_WASM_CPDF_URL=https://internal-server.example.com/wasm/cpdf/ \
  -t bentopdf .

# 3. Export the image
docker save bentopdf -o bentopdf.tar

# 4. Transfer bentopdf.tar + the .tgz WASM packages into the air-gapped network

# 5. Inside the air-gapped network — load and run
docker load -i bentopdf.tar

# Extract WASM packages to your internal web server
mkdir -p /var/www/wasm/pymupdf /var/www/wasm/gs /var/www/wasm/cpdf
tar xzf bentopdf-pymupdf-wasm-0.11.14.tgz -C /var/www/wasm/pymupdf --strip-components=1
tar xzf bentopdf-gs-wasm-*.tgz -C /var/www/wasm/gs --strip-components=1
tar xzf coherentpdf-*.tgz -C /var/www/wasm/cpdf --strip-components=1

# Run BentoPDF
docker run -d -p 3000:8080 --restart unless-stopped bentopdf

Set a variable to empty string to disable that module (users must configure manually via Advanced Settings).

Custom User ID (PUID/PGID)

For environments that require running as a specific non-root user (NAS devices, Kubernetes with security contexts, organizational policies), BentoPDF provides a separate Dockerfile with LSIO-style PUID/PGID support.

Build and Run

# 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

Environment Variables

Variable Description Default
PUID User ID to run as 1000
PGID Group ID to run as 1000
DISABLE_IPV6 Disable IPv6 listener false

Docker Compose

services:
  bentopdf:
    build:
      context: .
      dockerfile: Dockerfile.nonroot
    container_name: bentopdf
    ports:
      - '3000:8080'
    environment:
      - PUID=1000
      - PGID=1000
    restart: unless-stopped

How It Works

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.

Note

The standard Dockerfile uses nginx-unprivileged (UID 101) and is recommended for most deployments. Use Dockerfile.nonroot only when you need a specific UID/GID.

Warning

PUID/PGID cannot be 0 (root). The entrypoint validates inputs and will exit with an error for invalid values.

With Traefik (Reverse Proxy)

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'
      # Required headers for SharedArrayBuffer (LibreOffice WASM)
      - '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 (Reverse Proxy)

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

Podman Quadlet (Systemd Integration)

Quadlet allows you to run Podman containers as systemd services. This is ideal for production deployments on Linux systems.

Basic Quadlet Setup

Create a container unit file at ~/.config/containers/systemd/bentopdf.container (user) or /etc/containers/systemd/bentopdf.container (system):

[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 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

# View logs
journalctl --user -u bentopdf -f

Tip

For system-wide deployment, use systemctl without --user flag and place the file in /etc/containers/systemd/.

Simple Mode Quadlet

For Simple Mode deployment, create bentopdf-simple.container:

[Unit]
Description=BentoPDF Simple Mode - Clean PDF toolkit
After=network-online.target
Wants=network-online.target

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

[Service]
Restart=always
TimeoutStartSec=300

[Install]
WantedBy=default.target

Quadlet 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=curl -f http://localhost:8080 || exit 1
HealthInterval=30s
HealthTimeout=10s
HealthRetries=3

[Service]
Restart=always
TimeoutStartSec=300

[Install]
WantedBy=default.target

Auto-Update with Quadlet

Podman can automatically update containers when new images are available:

# Enable 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

Quadlet Network Configuration

For custom network configuration, create a network file bentopdf.network:

[Network]
Subnet=10.89.0.0/24
Gateway=10.89.0.1

Then reference it in your container file:

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

Updating

# Pull latest image
docker compose pull

# Recreate container
docker compose up -d