feat: translate all pages, UI messages and docs and fix SEO redirects and indexing

This commit is contained in:
alam00000 committed 2026-10-25 12:55:21 +05:30
1 parent 5e019de5ae
commit 4edb1718cc
3344 files changed
+415643 -4759

No files matched your search

+275
View File
@@ -0,0 +1,275 @@
# Развёртывание на Apache
Размещение BentoPDF на Apache HTTP Server.
## Требования
- Apache 2.4+
- Включённый mod_rewrite
- SSL-сертификат (рекомендуется)
## Шаг 1: соберите проект
```bash
git clone https://github.com/alam00000/bentopdf.git
cd bentopdf
npm install
npm run build
```
Чтобы настроить брендинг, задайте переменные окружения перед сборкой:
```bash
VITE_BRAND_NAME="AcmePDF" VITE_BRAND_LOGO="images/acme-logo.svg" npm run build
```
## Шаг 2: скопируйте файлы
```bash
sudo mkdir -p /var/www/bentopdf
sudo cp -r dist/* /var/www/bentopdf/
sudo chown -R www-data:www-data /var/www/bentopdf
```
## Шаг 3: конфигурация Apache
Создайте `/etc/apache2/sites-available/bentopdf.conf`:
```apache
<VirtualHost *:80>
ServerName your-domain.com
Redirect permanent / https://your-domain.com/
</VirtualHost>
<VirtualHost *:443>
ServerName your-domain.com
DocumentRoot /var/www/bentopdf
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/your-domain.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/your-domain.com/privkey.pem
<Directory /var/www/bentopdf>
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
</Directory>
# WASM MIME type
AddType application/wasm .wasm
# Prevent double-compression of pre-compressed files
SetEnvIfNoCase Request_URI "\.gz$" no-gzip
# Compression
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/css application/javascript application/json application/wasm
</IfModule>
# Cache headers
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType application/javascript "access plus 1 year"
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/wasm "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/svg+xml "access plus 1 year"
</IfModule>
# Required headers for SharedArrayBuffer (LibreOffice WASM)
Header always set Cross-Origin-Embedder-Policy "require-corp"
Header always set Cross-Origin-Opener-Policy "same-origin"
Header always set Cross-Origin-Resource-Policy "cross-origin"
# Security headers
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
# Pre-compressed LibreOffice WASM files
<FilesMatch "soffice\.wasm\.gz$">
ForceType application/wasm
Header set Content-Encoding "gzip"
</FilesMatch>
<FilesMatch "soffice\.data\.gz$">
ForceType application/octet-stream
Header set Content-Encoding "gzip"
</FilesMatch>
</VirtualHost>
```
## Шаг 4: .htaccess для маршрутизации
Создайте `/var/www/bentopdf/.htaccess`:
```apache
RewriteEngine On
RewriteBase /
# Existing files/dirs - serve directly
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
# ============================================
# LANGUAGE ROUTES
# ============================================
# Supported languages: de, es, zh, zh-TW, vi, it, id, tr, fr, pt
# English has no prefix - served from root
# English prefix redirects to root
RewriteRule ^en/?$ / [R=301,L]
RewriteRule ^en/(.+)$ /$1 [R=301,L]
# Language prefix root (e.g., /de/ -> /de/index.html)
RewriteCond %{DOCUMENT_ROOT}/$1/index.html -f
RewriteRule ^(de|es|zh|zh-TW|vi|it|id|tr|fr|pt)/?$ /$1/index.html [L]
# Language prefix with path (e.g., /de/merge-pdf -> /de/merge-pdf.html)
RewriteCond %{DOCUMENT_ROOT}/$1/$2.html -f
RewriteRule ^(de|es|zh|zh-TW|vi|it|id|tr|fr|pt)/([^/]+)/?$ /$1/$2.html [L]
# ============================================
# ADD .HTML EXTENSION (ROOT LEVEL)
# ============================================
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME}.html -f
RewriteRule ^([^/]+)$ $1.html [L]
ErrorDocument 404 /404.html
```
## Шаг 5: включите нужные модули
```bash
sudo a2enmod rewrite
sudo a2enmod ssl
sudo a2enmod headers
sudo a2enmod expires
sudo a2enmod deflate
```
## Шаг 6: включите сайт
```bash
sudo a2ensite bentopdf.conf
sudo apache2ctl configtest
sudo systemctl reload apache2
```
## Развёртывание в подкаталоге
Чтобы разместить по адресу `/pdf/`:
1. Соберите проект с базовым URL:
```bash
BASE_URL=/pdf/ npm run build
```
2. Обновите `.htaccess`:
```apache
RewriteEngine On
RewriteBase /pdf/
# Existing files/dirs - serve directly
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
# Language routes
RewriteRule ^en/?$ /pdf/ [R=301,L]
RewriteRule ^en/(.+)$ /pdf/$1 [R=301,L]
RewriteCond %{DOCUMENT_ROOT}/pdf/$1/index.html -f
RewriteRule ^(de|es|zh|zh-TW|vi|it|id|tr|fr|pt)/?$ /pdf/$1/index.html [L]
RewriteCond %{DOCUMENT_ROOT}/pdf/$1/$2.html -f
RewriteRule ^(de|es|zh|zh-TW|vi|it|id|tr|fr|pt)/([^/]+)/?$ /pdf/$1/$2.html [L]
# Root level .html extension
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME}.html -f
RewriteRule ^([^/]+)$ $1.html [L]
ErrorDocument 404 /pdf/404.html
```
## Устранение неполадок
### Ошибки 404 для WASM
Убедитесь, что настроен MIME-тип:
```apache
AddType application/wasm .wasm
```
### Sign PDF или Form Filler показывают пустой просмотрщик (ошибка MIME для `.mjs`)
Если в консоли браузера видно:
```
Failed to load module script: The server responded with a non-JavaScript
MIME type of "application/octet-stream". Strict MIME type checking is
enforced for module scripts per HTML spec.
```
…и неудачный запрос заканчивается на `.mjs`, в конфигурации Apache нет сопоставления `.mjs → application/javascript`. Встроенный PDF-просмотрщик поставляется с ES-модулями с расширением `.mjs`, а стандартный `mime.types` Apache их по умолчанию не содержит.
Добавьте в VirtualHost или `.htaccess`:
```apache
AddType application/javascript .mjs
```
Затем перезагрузите Apache:
```bash
sudo apache2ctl configtest && sudo systemctl reload apache2
```
Для проверки откройте DevTools браузера → вкладка Network, найдите неудачный запрос `.mjs` и посмотрите заголовок ответа `Content-Type`. Теперь он должен быть `application/javascript`, а не `application/octet-stream`.
### Rewrite не работает
Проверьте, что mod_rewrite включён:
```bash
sudo a2enmod rewrite
```
### Конвертация Word/ODT/Excel в PDF не работает
LibreOffice WASM требует `SharedArrayBuffer`, для которого нужны такие заголовки:
```apache
Header always set Cross-Origin-Embedder-Policy "require-corp"
Header always set Cross-Origin-Opener-Policy "same-origin"
```
Также нужен безопасный контекст. `http://localhost` подходит для локального тестирования, а для `http://192.168.x.x` и других адресов локальной сети обычно требуется HTTPS. Если заголовки есть, но `window.crossOriginIsolated` по-прежнему `false`, проверьте, не открывается ли страница по обычному HTTP с адреса, отличного от loopback.
Предварительно сжатым файлам `.wasm.gz` и `.data.gz` также нужен правильный `Content-Encoding`:
```apache
<FilesMatch "soffice\.wasm\.gz$">
ForceType application/wasm
Header set Content-Encoding "gzip"
</FilesMatch>
<FilesMatch "soffice\.data\.gz$">
ForceType application/octet-stream
Header set Content-Encoding "gzip"
</FilesMatch>
```
Убедитесь, что `mod_headers` включён: `sudo a2enmod headers`
### Отказано в доступе
```bash
sudo chown -R www-data:www-data /var/www/bentopdf
sudo chmod -R 755 /var/www/bentopdf
```
+200
View File
@@ -0,0 +1,200 @@
# Развёртывание на AWS S3 + CloudFront
Размещение BentoPDF на AWS для максимального контроля и масштабируемости.
## Архитектура
```
User → CloudFront (CDN) → S3 (Static Files)
```
## Шаг 1: создайте S3-бакет
```bash
# Create bucket
aws s3 mb s3://your-bentopdf-bucket --region us-east-1
# Enable static website hosting
aws s3 website s3://your-bentopdf-bucket \
--index-document index.html \
--error-document index.html
```
## Шаг 2: сборка и загрузка
```bash
# Build the project (optionally with custom branding)
# VITE_BRAND_NAME="AcmePDF" VITE_BRAND_LOGO="images/acme-logo.svg" npm run build
npm run build
# Sync to S3
aws s3 sync dist/ s3://your-bentopdf-bucket \
--delete \
--cache-control "max-age=31536000"
# Set correct MIME types for WASM
aws s3 cp s3://your-bentopdf-bucket/ s3://your-bentopdf-bucket/ \
--recursive \
--exclude "*" \
--include "*.wasm" \
--content-type "application/wasm" \
--metadata-directive REPLACE
# Set correct MIME type for ES module files (.mjs)
# S3 defaults .mjs to application/octet-stream, which browsers refuse to
# execute as ES modules. The bundled PDF viewer (Sign PDF, Form Filler)
# ships ES modules with .mjs extensions and will fail without this step.
aws s3 cp s3://your-bentopdf-bucket/ s3://your-bentopdf-bucket/ \
--recursive \
--exclude "*" \
--include "*.mjs" \
--content-type "application/javascript" \
--metadata-directive REPLACE
```
## Шаг 3: создайте дистрибутив CloudFront
```bash
aws cloudfront create-distribution \
--origin-domain-name your-bentopdf-bucket.s3.amazonaws.com \
--default-root-object index.html
```
Или воспользуйтесь консолью AWS:
1. Откройте CloudFront → Create distribution
2. Origin domain: выберите свой S3-бакет
3. Включите "Origin Access Control"
4. Default root object: `index.html`
5. Создайте дистрибутив
## Шаг 3b: политика заголовков ответа (обязательна для LibreOffice WASM)
Конвертации на основе LibreOffice (Word, Excel, PowerPoint в PDF) требуют `SharedArrayBuffer`, для которого нужны определённые заголовки ответа. Создайте политику заголовков ответа CloudFront (Response Headers Policy):
1. Откройте CloudFront → Policies → Response headers
2. Создайте собственную политику с такими заголовками:
| Заголовок | Значение |
| ------------------------------ | -------------- |
| `Cross-Origin-Embedder-Policy` | `require-corp` |
| `Cross-Origin-Opener-Policy` | `same-origin` |
| `Cross-Origin-Resource-Policy` | `cross-origin` |
3. Привяжите политику к default cache behavior вашего дистрибутива
Или через CLI:
```bash
aws cloudfront create-response-headers-policy \
--response-headers-policy-config '{
"Name": "BentoPDF-COEP-COOP",
"CustomHeadersConfig": {
"Quantity": 3,
"Items": [
{"Header": "Cross-Origin-Embedder-Policy", "Value": "require-corp", "Override": true},
{"Header": "Cross-Origin-Opener-Policy", "Value": "same-origin", "Override": true},
{"Header": "Cross-Origin-Resource-Policy", "Value": "cross-origin", "Override": true}
]
}
}'
```
## Шаг 3c: метаданные S3 для предварительно сжатых файлов WASM
Файлы LibreOffice WASM предварительно сжаты (`.wasm.gz`, `.data.gz`). Задайте правильные Content-Type и Content-Encoding, чтобы браузеры их распаковывали:
```bash
# Set correct headers for soffice.wasm.gz
aws s3 cp s3://your-bentopdf-bucket/libreoffice-wasm/soffice.wasm.gz \
s3://your-bentopdf-bucket/libreoffice-wasm/soffice.wasm.gz \
--content-type "application/wasm" \
--content-encoding "gzip" \
--metadata-directive REPLACE
# Set correct headers for soffice.data.gz
aws s3 cp s3://your-bentopdf-bucket/libreoffice-wasm/soffice.data.gz \
s3://your-bentopdf-bucket/libreoffice-wasm/soffice.data.gz \
--content-type "application/octet-stream" \
--content-encoding "gzip" \
--metadata-directive REPLACE
```
::: warning Важно
Без политики заголовков ответа `SharedArrayBuffer` недоступен, и конвертации LibreOffice WASM зависнут примерно на 55%. Без правильного Content-Encoding у файлов `.gz` браузер получает необработанные байты gzip, и компиляция WASM завершается ошибкой.
:::
## Шаг 4: политика S3-бакета
Разрешите CloudFront доступ к бакету:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontAccess",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::your-bentopdf-bucket/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::ACCOUNT_ID:distribution/DISTRIBUTION_ID"
}
}
}
]
}
```
## Шаг 5: собственные страницы ошибок
Настройте возврат `index.html` при 404 для маршрутизации SPA:
1. CloudFront → Error pages
2. Создайте собственный ответ на ошибку:
- HTTP error code: 404
- Response page path: `/index.html`
- HTTP response code: 200
## Оценка стоимости
| Ресурс | Ориентировочная стоимость |
| -------------------------- | ------------------------- |
| S3 Storage (~500MB) | ~$0.01/мес. |
| CloudFront (1TB transfer) | ~$85/мес. |
| CloudFront (10GB transfer) | ~$0.85/мес. |
::: tip
Используйте S3 Intelligent Tiering, чтобы снизить расходы на редко запрашиваемые файлы.
:::
## Автоматизация с Terraform
```hcl
# main.tf
resource "aws_s3_bucket" "bentopdf" {
bucket = "your-bentopdf-bucket"
}
resource "aws_cloudfront_distribution" "bentopdf" {
origin {
domain_name = aws_s3_bucket.bentopdf.bucket_regional_domain_name
origin_id = "S3Origin"
}
enabled = true
default_root_object = "index.html"
default_cache_behavior {
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "S3Origin"
viewer_protocol_policy = "redirect-to-https"
}
}
```
+154
View File
@@ -0,0 +1,154 @@
# Развёртывание на Cloudflare Pages
[Cloudflare Pages](https://pages.cloudflare.com) предлагает быстрый глобальный хостинг статических сайтов с неограниченным трафиком.
## Быстрое развёртывание
1. Откройте [Cloudflare Pages](https://dash.cloudflare.com/?to=/:account/pages)
2. Нажмите "Create a project"
3. Подключите свой репозиторий GitHub
## Настройка сборки
| Параметр | Значение |
| ---------------------- | --------------- |
| Framework preset | None |
| Build command | `npm run build` |
| Build output directory | `dist` |
| Root directory | `/` |
## Переменные окружения
Добавьте их в Settings → Environment variables:
| Переменная | Значение |
| ----------------------- | ----------------------------------------------------------- |
| `NODE_VERSION` | `18` |
| `SIMPLE_MODE` | `false` (необязательно) |
| `VITE_BRAND_NAME` | Своё название бренда (необязательно) |
| `VITE_BRAND_LOGO` | Путь к логотипу относительно `public/` (необязательно) |
| `VITE_FOOTER_TEXT` | Свой текст подвала/копирайта (необязательно) |
| `VITE_DEFAULT_LANGUAGE` | Язык интерфейса по умолчанию, например `fr` (необязательно) |
## Файл конфигурации
Создайте `_headers` в папке `public`:
```
# Required security headers for SharedArrayBuffer (used by LibreOffice WASM)
/*
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: cross-origin
# Pre-compressed LibreOffice WASM binary
/libreoffice-wasm/soffice.wasm.gz
Content-Type: application/wasm
Content-Encoding: gzip
Cache-Control: public, max-age=31536000, immutable
# Pre-compressed LibreOffice WASM data
/libreoffice-wasm/soffice.data.gz
Content-Type: application/octet-stream
Content-Encoding: gzip
Cache-Control: public, max-age=31536000, immutable
# Cache WASM files aggressively
/*.wasm
Cache-Control: public, max-age=31536000, immutable
Content-Type: application/wasm
# Service worker
/sw.js
Cache-Control: no-cache
```
::: warning Важно
Заголовки `Cross-Origin-Embedder-Policy` и `Cross-Origin-Opener-Policy` обязательны для конвертации Word/ODT/Excel/PowerPoint в PDF. Без них `SharedArrayBuffer` недоступен, и движок LibreOffice WASM не сможет инициализироваться.
:::
Создайте `_redirects` для маршрутизации SPA:
```
/* /index.html 200
```
## Собственный домен
1. Откройте свой проект Pages
2. Нажмите "Custom domains"
3. Добавьте свой домен
4. Cloudflare автоматически настроит DNS, если домен находится в Cloudflare
## Преимущества
- **Бесплатный неограниченный трафик**
- **Глобальная CDN** с более чем 300 узлами
- **Автоматический HTTPS**
- **Предпросмотр развёртываний** для pull request
- **Быстрые сборки**
## Устранение неполадок
### Загрузка больших файлов
Cloudflare Pages поддерживает файлы размером до 25 МБ. Модули WASM должны проходить, но если вы упрётесь в лимит, попробуйте:
```bash
# Split large files during build
npm run build
```
### Ограничения размера Worker
Если вы используете Cloudflare Workers для сложной маршрутизации, учтите лимит 1 МБ на бесплатных тарифах.
## CORS-прокси Worker (для цифровых подписей)
Инструменту цифровой подписи нужен CORS-прокси для получения цепочек сертификатов. Разверните входящий в комплект worker:
```bash
cd cloudflare
npx wrangler login
npx wrangler deploy
```
### Функции безопасности
| Функция | Описание |
| ------------------------- | ----------------------------------- |
| **Ограничение URL** | Разрешены только URL сертификатов |
| **Лимит размера файла** | Максимум 10 МБ на запрос |
| **Ограничение частоты** | 60 запросов/IP/мин (нужен KV) |
| **Блокировка частных IP** | Блокирует localhost и внутренние IP |
### Включение ограничения частоты
```bash
# Create KV namespace
npx wrangler kv namespace create "RATE_LIMIT_KV"
# Add to wrangler.toml with returned ID:
# [[kv_namespaces]]
# binding = "RATE_LIMIT_KV"
# id = "YOUR_ID"
npx wrangler deploy
```
### Сборка с URL прокси
```bash
VITE_CORS_PROXY_URL=https://your-worker.workers.dev npm run build
```
Или с Docker:
```bash
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 your-bentopdf .
```
> **Примечание:** настройку подписи HMAC смотрите в [README](https://github.com/alam00000/bentopdf#digital-signature-cors-proxy-required).
+180
View File
@@ -0,0 +1,180 @@
# CORS-прокси для получения сертификатов
Инструмент цифровой подписи использует CORS-прокси для получения сертификатов издателей у внешних удостоверяющих центров (CA). Это нужно, потому что многие серверы CA не добавляют заголовки CORS в ответы, и браузер не может запросить сертификаты напрямую.
Кроме того, многие серверы CA отдают сертификаты по обычному HTTP. Если ваш экземпляр BentoPDF работает по HTTPS, браузеры блокируют такие HTTP-запросы (политика смешанного содержимого). CORS-прокси решает обе проблемы: запросы идут через HTTPS-endpoint с нужными заголовками.
## Как это работает
При подписании PDF сертификатом:
1. Библиотека `zgapdfsigner` пытается построить полную цепочку сертификатов
2. Она запрашивает сертификаты издателей по URL из расширения AIA (Authority Information Access) вашего сертификата
3. Эти запросы идут через CORS-прокси, который добавляет нужные заголовки `Access-Control-Allow-Origin`
4. Прокси возвращает данные сертификата в браузер
## Собственный CORS-прокси
Если вы размещаете BentoPDF у себя, для работы цифровых подписей с сертификатами, которым нужна загрузка цепочки, потребуется развернуть собственный CORS-прокси.
### Вариант 1: Cloudflare Workers (рекомендуется)
1. **Установите Wrangler CLI**:
```bash
npm install -g wrangler
```
2. **Войдите в Cloudflare**:
```bash
wrangler login
```
3. **Клонируйте BentoPDF и обновите разрешённые источники**:
```bash
git clone https://github.com/alam00000/bentopdf.git
cd bentopdf/cloudflare
```
Откройте `cors-proxy-worker.js` и замените массив `ALLOWED_ORIGINS` на свой домен:
```js
const ALLOWED_ORIGINS = [
'https://your-domain.com',
'https://www.your-domain.com',
];
```
::: warning Важно
Без этого изменения прокси отклонит все запросы с вашего сайта с ошибкой **403 Forbidden**. По умолчанию разрешены только запросы с `bentopdf.com`.
:::
4. **Разверните прокси**:
```bash
wrangler deploy
```
Запомните URL своего worker (например, `https://bentopdf-cors-proxy.your-subdomain.workers.dev`).
5. **Пересоберите BentoPDF с URL прокси**:
Если вы используете Docker:
```bash
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 your-bentopdf .
```
Если вы собираете из исходников:
```bash
VITE_CORS_PROXY_URL=https://your-worker.workers.dev npm run build
```
### Вариант 2: собственный серверный прокси
Можно создать и собственный endpoint прокси. Требования:
1. Принимать GET-запросы с параметром запроса `url`
2. Запрашивать этот URL со своего сервера (на стороне сервера ограничений CORS нет)
3. Возвращать ответ с такими заголовками:
- `Access-Control-Allow-Origin: https://your-domain.com`
- `Access-Control-Allow-Methods: GET, OPTIONS`
- `X-Content-Type-Options: nosniff`
Пример реализации на Express.js:
```javascript
app.get('/api/cert-proxy', async (req, res) => {
const targetUrl = req.query.url;
// Validate it's a certificate URL
if (!isValidCertUrl(targetUrl)) {
return res.status(400).json({ error: 'Invalid URL' });
}
try {
const response = await fetch(targetUrl);
const data = await response.arrayBuffer();
res.set('Access-Control-Allow-Origin', 'https://your-domain.com');
res.set('Content-Type', 'application/octet-stream');
res.set('X-Content-Type-Options', 'nosniff');
res.send(Buffer.from(data));
} catch (error) {
res.status(500).json({ error: 'Proxy error' });
}
});
```
## Вопросы безопасности
В поставляемом Cloudflare Worker предусмотрено несколько мер безопасности:
| Функция | Описание |
| --------------------------- | ------------------------------------------------------------------------------------------------ |
| **Проверка источника** | Разрешает запросы только с доменов из `ALLOWED_ORIGINS` |
| **Ограничение URL** | Разрешает только URL сертификатов (`.crt`, `.cer`, `.pem`, `/certs/`, `/ocsp`, `/crl`) |
| **Блокировка частных IP** | Блокирует частные диапазоны IPv4/IPv6, link-local, loopback, десятичные IP и облачные метаданные |
| **Безопасный Content-Type** | Возвращает только безопасные MIME-типы сертификатов, блокирует подмену content-type источником |
| **Лимит размера файла** | Передаёт ответ потоком с лимитом 10 МБ и прерывает загрузку при превышении |
| **Ограничение частоты** | 60 запросов в минуту на IP или IPv6 /64 (нужен KV, при ошибках KV пропускает запросы) |
| **Подписи HMAC** | Необязательная подпись на стороне клиента (сдерживает случайные злоупотребления) |
### DNS rebinding (хостинг вне Cloudflare)
Worker отклоняет любой URL, который разрешается в частный или внутренний IP. Фиксированный список разрешённых хостов использовать нельзя, так как URL цепочек сертификатов указывают на тот CA, который выдал сертификат (FNMT, корпоративные CA и так далее), и адреса заранее неизвестны. Поэтому остаётся небольшое окно для rebinding: тот, кто управляет DNS своего домена, может отдать проверке безопасности публичный IP, а настоящему запросу чуть позже частный.
В Cloudflare это не эксплуатируется: Workers не могут обращаться к частным IP и endpoint метаданных `169.254.169.254`, поэтому rebind ни к чему не приведёт. Только не давайте worker доступа внутрь: не подключайте Tunnel, Service или привязку к частной сети (ему нужен лишь `RATE_LIMIT_KV`).
Если вы запускаете его вне Cloudflare (workerd, Node и подобное) внутри VPC, среда _может_ обращаться к внутренним IP. Поставьте перед ней исходящий файрвол и заблокируйте частные диапазоны: `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `127.0.0.0/8`, `169.254.0.0/16` и их аналоги IPv6. Код приложения не может этого обеспечить, когда вы вне Cloudflare.
## Отключение прокси
Если CORS-прокси вам не нужен, задайте переменной окружения пустую строку:
```
VITE_CORS_PROXY_URL=
```
**Примечание**: без прокси подпись сертификатами, которым нужна внешняя загрузка цепочки (например, FNMT или некоторые корпоративные CA), завершится ошибкой "Failed to fetch".
## Устранение неполадок
### "Signing error: TypeError: Failed to fetch"
Обычно это означает одно из двух:
1. **CORS-прокси не настроен**: задайте `VITE_CORS_PROXY_URL` и пересоберите проект
2. **Смешанное содержимое заблокировано**: ваш сайт работает по HTTPS, а URL издателя сертификата по HTTP. CORS-прокси решает эту проблему.
3. **CORS-прокси отклоняет ваш источник**: проверьте, что ваш домен есть в массиве `ALLOWED_ORIGINS` в `cors-proxy-worker.js`
### "403 Forbidden" от прокси
Вашего домена нет в списке `ALLOWED_ORIGINS`. Отредактируйте `cors-proxy-worker.js`:
```js
const ALLOWED_ORIGINS = ['https://your-domain.com'];
```
Затем разверните заново: `npx wrangler deploy`
### Проверка прокси
```bash
curl -H "Origin: https://your-domain.com" \
"https://your-proxy.workers.dev?url=http://www.cert.fnmt.es/certs/ACUSU.crt"
```
### Сертификаты, работающие без прокси
Некоторые сертификаты содержат полную цепочку в файле P12/PFX и не требуют внешней загрузки:
- Самоподписанные сертификаты
- Некоторые коммерческие CA, которые включают промежуточные сертификаты
- Сертификаты, для которых вы вручную собрали полную цепочку
+622
View File
@@ -0,0 +1,622 @@
# Развёртывание с Docker / Podman
Самый простой способ развернуть BentoPDF у себя в рабочей среде.
> [!IMPORTANT]
> **Обязательные заголовки для конвертации файлов Office**
>
> Инструментам на основе LibreOffice (конвертация Word, Excel, PowerPoint) для поддержки `SharedArrayBuffer` нужны такие HTTP-заголовки:
>
> - `Cross-Origin-Opener-Policy: same-origin`
> - `Cross-Origin-Embedder-Policy: require-corp`
>
> Страница также должна отдаваться из безопасного контекста. `http://localhost` подходит для локального тестирования, а `http://192.168.x.x` и другие адреса локальной сети обычно под это не подпадают, поэтому конвертация Office по обычному HTTP не сработает, даже если заголовки есть.
>
> Официальные образы контейнеров включают эти заголовки. Если вы используете обратный прокси (Traefik, Caddy и т. д.), убедитесь, что эти заголовки сохраняются или добавляются, и используйте HTTPS для доступа не через loopback.
> [!TIP]
> **Пользователям Podman:** все команды `docker` работают и с Podman, если заменить `docker` на `podman`, а `docker-compose` на `podman-compose`.
## Быстрый старт
> [!TIP]
> BentoPDF выпускается в двух сборках:
>
> - **Сборка Self-Hosted**: `ghcr.io/alam00000/bentopdf-simple:latest`. Все PDF-инструменты **без** маркетинговой обвязки (без главного экрана, FAQ, отзывов и подвала). Используйте её для внутренних, командных и корпоративных развёртываний. Это **не** урезанная «лёгкая» версия.
> - **Сборка Commercial**: `ghcr.io/alam00000/bentopdf:latest`. Полный маркетинговый сайт, который использует сам bentopdf.com и владельцы коммерческой лицензии для публичных развёртываний.
>
> Если сомневаетесь, берите сборку Self-Hosted.
```bash
# Docker
docker run -d \
--name bentopdf \
-p 3000:8080 \
--restart unless-stopped \
ghcr.io/alam00000/bentopdf-simple:latest
# Podman
podman run -d \
--name bentopdf \
-p 3000:8080 \
ghcr.io/alam00000/bentopdf-simple:latest
```
## Docker Compose / Podman Compose
Создайте `docker-compose.yml`:
```yaml
services:
bentopdf:
image: ghcr.io/alam00000/bentopdf-simple: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
```
Запустите:
```bash
# Docker Compose
docker compose up -d
# Podman Compose
podman-compose up -d
```
## Собственный образ
```dockerfile
# 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;"]
```
Соберите и запустите:
```bash
docker build -t bentopdf:custom .
docker run -d -p 3000:8080 bentopdf:custom
```
## Переменные окружения
| Переменная | Описание | По умолчанию |
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| `SIMPLE_MODE` | Собрать вариант Self-Hosted (скрывает маркетинговую обвязку bentopdf.com: главный экран, FAQ, отзывы, подвал). Все PDF-инструменты остаются. | `false` |
| `BASE_URL` | Развёртывание в подкаталог | `/` |
| `VITE_WASM_PYMUPDF_URL` | URL модуля PyMuPDF WASM | `https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.16/` |
| `VITE_WASM_GS_URL` | URL модуля Ghostscript WASM | `https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm@0.1.1/assets/` |
| `VITE_WASM_CPDF_URL` | URL модуля CoherentPDF WASM | `https://cdn.jsdelivr.net/npm/coherentpdf@2.5.5/dist/` |
| `VITE_TESSERACT_WORKER_URL` | URL скрипта воркера OCR | _(пусто; CDN Tesseract.js по умолчанию)_ |
| `VITE_TESSERACT_CORE_URL` | Каталог основного рантайма OCR | _(пусто; CDN Tesseract.js по умолчанию)_ |
| `VITE_TESSERACT_LANG_URL` | Каталог traineddata для OCR | _(пусто; CDN Tesseract.js по умолчанию)_ |
| `VITE_TESSERACT_AVAILABLE_LANGUAGES` | Языки OCR через запятую, показываемые в интерфейсе | _(пусто; показать весь каталог)_ |
| `VITE_OCR_FONT_BASE_URL` | Каталог шрифтов текстового слоя OCR | _(пусто; удалённые URL шрифтов Noto)_ |
| `VITE_DEFAULT_LANGUAGE` | Язык интерфейса по умолчанию | `en` |
| `VITE_BRAND_NAME` | Своё название бренда | `BentoPDF` |
| `VITE_BRAND_LOGO` | Путь к логотипу относительно `public/` | `images/favicon-no-bg.svg` |
| `VITE_FOOTER_TEXT` | Свой текст подвала/копирайта | `© 2026 BentoPDF. All rights reserved.` |
| `DISABLE_TOOLS` | ID инструментов через запятую, которые нужно скрыть | _(пусто; все инструменты включены)_ |
| `DISABLE_GITHUB_STARS` | Пропускать запрос числа звёзд GitHub при загрузке страницы и убирать `api.github.com` из CSP. Сборки Simple Mode всегда его пропускают. | `false` |
| `SITE_URL` | Публичный URL, записываемый в canonical, hreflang и ссылки sitemap. Оставьте значение по умолчанию, если не хотите, чтобы поисковые системы считали ваше развёртывание отдельным сайтом. | `https://www.bentopdf.com` |
| `ALLOW_INDEXING` | При собственном `SITE_URL` каждая страница помечается `noindex`, чтобы ваша копия не конкурировала с bentopdf.com в поисковой выдаче. Задайте `true`, чтобы разрешить индексацию. | `false` |
URL модулей WASM заранее настроены на значения CDN по умолчанию, поэтому все расширенные функции работают сразу. Переопределите их для изолированных (air-gapped) или самостоятельно размещаемых развёртываний.
### Поисковые системы
По умолчанию каждая страница указывает поисковым системам на bentopdf.com как на оригинал, поэтому ваше развёртывание не считается дубликатом. Для частного или внутреннего экземпляра ничего менять не нужно.
- **Полностью скрыть экземпляр из поисковой выдачи:** запустите контейнер с `-e ROBOTS_NOINDEX=true`. Нужен только перезапуск, пересборка не требуется.
- **Сделать экземпляр отдельным сайтом в поисковой выдаче:** соберите с `--build-arg SITE_URL=https://your-domain.example --build-arg ALLOW_INDEXING=true`. Без `ALLOW_INDEXING=true` собственный `SITE_URL` помечает каждую страницу как `noindex`.
### Content-Security-Policy
Образ nginx поставляется с действующим заголовком `Content-Security-Policy`. Директивы CSP `script-src`, `connect-src` и `font-src` **генерируются при сборке** из перечисленных выше переменных URL `VITE_*`: все хосты, которые вы передаёте через `--build-arg`, автоматически добавляются в политику.
Как следствие:
- Если вы переопределяете `VITE_CORS_PROXY_URL` или `VITE_WASM_*_URL` при сборке, CSP автоматически разрешает эти источники, дополнительная настройка не нужна.
- Для URL на `cdn.jsdelivr.net` директива `script-src` разрешает только точную папку пакета с версией, например `https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.16/`, а не весь CDN. jsDelivr раздаёт любой публичный пакет npm или репозиторий GitHub, поэтому разрешение всего хоста позволило бы запускать любой из них на вашем экземпляре. URL на любых других хостах разрешаются по источнику (origin).
- Если вы задаёте собственные URL WASM во _время работы_ на странице расширенных настроек приложения, эти источники **не** попадают в CSP, и браузер заблокирует запросы к ним. Это относится и к другой версии того же пакета на `cdn.jsdelivr.net`, так как разрешена только версия, заданная при сборке. Настройка во время работы предназначена для экспериментов; для постоянных собственных URL задайте соответствующий аргумент сборки `VITE_*`.
- В изолированных развёртываниях, где переопределены все три значения `VITE_WASM_*_URL`, из CSP также удаляются эти папки пакетов `cdn.jsdelivr.net` (каждое значение по умолчанию заменяется, а не дополняется). Значения Tesseract OCR по умолчанию на `cdn.jsdelivr.net` остаются в политике, пока вы не зададите и три значения `VITE_TESSERACT_*`. Аналогично, задание `VITE_CORS_PROXY_URL` заменяет публичное значение по умолчанию `bentopdf-cors-proxy.bentopdf.workers.dev`.
CSP включает `'unsafe-eval'` в `script-src`, потому что среда LibreOffice WASM (используется инструментами конвертации Word/Excel/PowerPoint) и библиотека изображений wasm-vips (используется для PDF в TIFF) компилируют внутренний диспетчерский код через `new Function()`. Если убрать его, эти инструменты перестанут работать. Если вы собираете в `SIMPLE_MODE` (без LibreOffice) и PDF в TIFF вам не нужен, можно вручную отредактировать сгенерированный `security-headers.conf` и убрать `'unsafe-eval'` для более строгой политики.
Для OCR оставьте переменные `VITE_TESSERACT_*` пустыми, чтобы использовать онлайн-ресурсы по умолчанию, или задайте все три вместе для OCR на своём сервере или офлайн. Частичное переопределение отклоняется, потому что воркер, основной рантайм и каталог traineddata должны соответствовать друг другу. Для полностью офлайнового вывода PDF с возможностью поиска также задайте `VITE_OCR_FONT_BASE_URL`, чтобы шрифты текстового слоя OCR загружались с вашего внутреннего сервера, а не по публичным URL Noto.
`VITE_DEFAULT_LANGUAGE` задаёт язык интерфейса для новых посетителей. Поддерживаемые значения: `en`, `ar`, `be`, `fr`, `de`, `es`, `zh`, `zh-TW`, `vi`, `tr`, `id`, `it`, `pt`, `nl`, `da`. Пользователи по-прежнему могут переключать язык: меняется только значение по умолчанию.
Пример:
```bash
# Build with French as the default language
docker build --build-arg VITE_DEFAULT_LANGUAGE=fr -t bentopdf .
docker run -d -p 3000:8080 bentopdf
```
### Свой брендинг
Замените стандартные логотип, название и текст подвала BentoPDF своими. Поместите файл логотипа в папку `public/` (или используйте существующее изображение), затем передайте переменные брендинга при сборке:
```bash
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 .
```
Брендинг работает и в полном режиме, и в Simple Mode, и сочетается со всеми остальными параметрами сборки.
### Отключение отдельных инструментов
Скройте инструменты в интерфейсе по требованиям соответствия или безопасности. Отключённые инструменты удаляются с главной страницы, из результатов поиска, сочетаний клавиш и конструктора рабочих процессов. При прямом переходе по URL показывается страница «инструмент недоступен».
ID инструмента: URL страницы без `.html`. Например, если инструмент находится по адресу `bentopdf.com/edit-pdf.html`, его ID равен `edit-pdf`.
#### Как найти ID инструментов
Проще всего открыть любой инструмент в BentoPDF и посмотреть на URL. Последняя часть пути (без `.html`) и есть ID инструмента.
<details>
<summary>Полный список ID инструментов</summary>
**Правка и аннотации:** `edit-pdf`, `bookmark`, `table-of-contents`, `page-numbers`, `add-page-labels`, `bates-numbering`, `add-watermark`, `header-footer`, `invert-colors`, `scanner-effect`, `adjust-colors`, `background-color`, `text-color`, `sign-pdf`, `add-stamps`, `remove-annotations`, `crop-pdf`, `form-filler`, `form-creator`, `remove-blank-pages`
**Конвертация в PDF:** `image-to-pdf`, `jpg-to-pdf`, `png-to-pdf`, `webp-to-pdf`, `svg-to-pdf`, `bmp-to-pdf`, `heic-to-pdf`, `tiff-to-pdf`, `txt-to-pdf`, `markdown-to-pdf`, `json-to-pdf`, `csv-to-pdf`, `rtf-to-pdf`, `odt-to-pdf`, `word-to-pdf`, `excel-to-pdf`, `powerpoint-to-pdf`, `xps-to-pdf`, `mobi-to-pdf`, `epub-to-pdf`, `fb2-to-pdf`, `cbz-to-pdf`, `wpd-to-pdf`, `wps-to-pdf`, `xml-to-pdf`, `pages-to-pdf`, `odg-to-pdf`, `ods-to-pdf`, `odp-to-pdf`, `pub-to-pdf`, `vsd-to-pdf`, `psd-to-pdf`, `email-to-pdf`
**Конвертация из PDF:** `pdf-to-jpg`, `pdf-to-png`, `pdf-to-webp`, `pdf-to-bmp`, `pdf-to-tiff`, `pdf-to-cbz`, `pdf-to-svg`, `pdf-to-csv`, `pdf-to-excel`, `pdf-to-greyscale`, `pdf-to-json`, `pdf-to-docx`, `extract-images`, `pdf-to-markdown`, `prepare-pdf-for-ai`, `pdf-to-text`
**Организация и управление:** `ocr-pdf`, `merge-pdf`, `alternate-merge`, `organize-pdf`, `add-attachments`, `extract-attachments`, `edit-attachments`, `pdf-multi-tool`, `pdf-layers`, `extract-tables`, `split-pdf`, `divide-pages`, `extract-pages`, `delete-pages`, `add-blank-page`, `reverse-pages`, `rotate-pdf`, `rotate-custom`, `n-up-pdf`, `pdf-booklet`, `combine-single-page`, `view-metadata`, `edit-metadata`, `pdf-to-zip`, `compare-pdfs`, `posterize-pdf`, `page-dimensions`
**Оптимизация и восстановление:** `compress-pdf`, `pdf-to-pdfa`, `fix-page-size`, `linearize-pdf`, `remove-restrictions`, `repair-pdf`, `rasterize-pdf`, `deskew-pdf`, `font-to-outline`
**Безопасность:** `encrypt-pdf`, `sanitize-pdf`, `decrypt-pdf`, `flatten-pdf`, `remove-metadata`, `change-permissions`, `digital-sign-pdf`, `validate-signature-pdf`, `timestamp-pdf`
</details>
#### Вариант 1: при сборке (аргумент сборки Docker)
```bash
docker build \
--build-arg DISABLE_TOOLS="edit-pdf,sign-pdf,encrypt-pdf" \
-t bentopdf .
```
Список отключённых инструментов встраивается в JavaScript-бандл. Для изменения нужна пересборка.
#### Вариант 2: во время работы (config.json)
Смонтируйте файл `config.json` в раздаваемый каталог, пересборка не нужна:
```json
{
"disabledTools": ["edit-pdf", "sign-pdf", "encrypt-pdf"]
}
```
```bash
docker run -d \
-p 3000:8080 \
-v ./config.json:/usr/share/nginx/html/config.json:ro \
ghcr.io/alam00000/bentopdf-simple:latest
```
Или с Docker Compose:
```yaml
services:
bentopdf:
image: ghcr.io/alam00000/bentopdf-simple:latest
ports:
- '3000:8080'
volumes:
- ./config.json:/usr/share/nginx/html/config.json:ro
```
Оба способа можно сочетать: списки объединяются. Если инструмент указан в любом из них, он отключён.
#### Отключение функций редактора
Можно также отключить отдельные функции внутри PDF-редактора (например, скрытие данных, аннотации, формы), не отключая весь инструмент редактора. Добавьте `editorDisabledCategories` в свой `config.json`:
```json
{
"editorDisabledCategories": ["redaction", "annotation-stamp"]
}
```
<details>
<summary>Полный список категорий редактора</summary>
**Масштаб:** `zoom`, `zoom-in`, `zoom-out`, `zoom-fit-page`, `zoom-fit-width`, `zoom-marquee`, `zoom-level`
**Аннотации:** `annotation`, `annotation-markup`, `annotation-highlight`, `annotation-underline`, `annotation-strikeout`, `annotation-squiggly`, `annotation-ink`, `annotation-text`, `annotation-stamp`
**Фигуры:** `annotation-shape`, `annotation-rectangle`, `annotation-circle`, `annotation-line`, `annotation-arrow`, `annotation-polygon`, `annotation-polyline`
**Формы:** `form`, `form-textfield`, `form-checkbox`, `form-radio`, `form-select`, `form-listbox`, `form-fill-mode`
**Скрытие данных:** `redaction`, `redaction-area`, `redaction-text`, `redaction-apply`, `redaction-clear`
**Документ:** `document`, `document-open`, `document-close`, `document-print`, `document-capture`, `document-export`, `document-fullscreen`, `document-protect`
**Страница:** `page`, `spread`, `rotate`, `scroll`, `navigation`
**Панель:** `panel`, `panel-sidebar`, `panel-search`, `panel-comment`
**Инструменты:** `tools`, `pan`, `pointer`, `capture`
**Выделение:** `selection`, `selection-copy`
**История:** `history`, `history-undo`, `history-redo`
</details>
Категории иерархичны: отключение родительской (например, `annotation`) отключает все дочерние.
### Отключение счётчика звёзд GitHub
В навигационной панели показывается актуальное число звёзд GitHub, которое запрашивается с `api.github.com` при загрузке страницы. Это единственный внешний запрос, не связанный с PDF-функциями. Отключите его при сборке, чтобы убрать код запроса из бандла и исключить `api.github.com` из Content-Security-Policy:
```bash
docker build --build-arg DISABLE_GITHUB_STARS=true -t bentopdf .
```
Сборки Simple Mode (`bentopdf-simple`) всегда пропускают счётчик звёзд: этот флаг нужен только для сборки Commercial или собственных полных сборок.
### Собственные URL WASM (изолированная среда / свой хостинг)
> [!IMPORTANT]
> URL WASM встраиваются в JavaScript **при сборке**. Файлы WASM скачиваются **браузером пользователя** во время работы: Docker не загружает их при сборке. Для изолированных сетей нужно разместить файлы WASM на внутреннем сервере, доступном браузерам.
**Полный процесс для изолированной среды:**
```bash
# 1. On a machine WITH internet — download WASM packages
bash scripts/prepare-airgap.sh --list-ocr-languages
bash scripts/prepare-airgap.sh --search-ocr-language german
# 2. Download WASM/OCR packages
npm pack @bentopdf/pymupdf-wasm@0.11.14
npm pack @bentopdf/gs-wasm
npm pack coherentpdf
npm pack tesseract.js@7.0.0
npm pack tesseract.js-core@7.0.0
mkdir -p tesseract-langdata
curl -fsSL https://cdn.jsdelivr.net/npm/@tesseract.js-data/eng/4.0.0_best_int/eng.traineddata.gz -o tesseract-langdata/eng.traineddata.gz
mkdir -p ocr-fonts
curl -fsSL https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSans/NotoSans-Regular.ttf -o ocr-fonts/NotoSans-Regular.ttf
# 3. 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/ \
--build-arg VITE_TESSERACT_WORKER_URL=https://internal-server.example.com/wasm/ocr/worker.min.js \
--build-arg VITE_TESSERACT_CORE_URL=https://internal-server.example.com/wasm/ocr/core \
--build-arg VITE_TESSERACT_LANG_URL=https://internal-server.example.com/wasm/ocr/lang-data \
--build-arg VITE_TESSERACT_AVAILABLE_LANGUAGES=eng,deu \
--build-arg VITE_OCR_FONT_BASE_URL=https://internal-server.example.com/wasm/ocr/fonts \
-t bentopdf .
# 4. Export the image
docker save bentopdf -o bentopdf.tar
# 5. Transfer bentopdf.tar + the .tgz packages + tesseract-langdata/ + ocr-fonts/ into the air-gapped network
# 6. 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 /var/www/wasm/ocr/core /var/www/wasm/ocr/lang-data /var/www/wasm/ocr/fonts
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
TEMP_TESS=$(mktemp -d)
tar xzf tesseract.js-7.0.0.tgz -C "$TEMP_TESS"
cp "$TEMP_TESS/package/dist/worker.min.js" /var/www/wasm/ocr/worker.min.js
rm -rf "$TEMP_TESS"
tar xzf tesseract.js-core-7.0.0.tgz -C /var/www/wasm/ocr/core --strip-components=1
cp ./tesseract-langdata/*.traineddata.gz /var/www/wasm/ocr/lang-data/
cp ./ocr-fonts/* /var/www/wasm/ocr/fonts/
# Run BentoPDF
docker run -d -p 3000:8080 --restart unless-stopped bentopdf
```
Используйте коды, которые выводит `bash scripts/prepare-airgap.sh --list-ocr-languages`, или ищите по названию через `bash scripts/prepare-airgap.sh --search-ocr-language <term>`, для `--ocr-languages`. Когда вы собираете с ограниченным набором языков OCR, передайте те же коды в `VITE_TESSERACT_AVAILABLE_LANGUAGES`, чтобы приложение показывало только включённые языки. Для полностью офлайнового вывода OCR также разместите поставляемый каталог `ocr-fonts/` и укажите на него `VITE_OCR_FONT_BASE_URL`.
Задайте переменной пустую строку, чтобы отключить этот модуль (пользователям придётся настроить его вручную в расширенных настройках).
## Свой порт
По умолчанию BentoPDF слушает порт `8080` внутри контейнера. Чтобы изменить его, задайте переменную окружения `PORT` во время работы:
```bash
docker run -p 3000:9090 -e PORT=9090 ghcr.io/alam00000/bentopdf-simple:latest
```
| Переменная | Описание | По умолчанию |
| ---------- | ----------------------- | ------------ |
| `PORT` | Порт Nginx в контейнере | `8080` |
Это работает и со стандартным, и с nonroot-Dockerfile.
## Свой ID пользователя (PUID/PGID)
Для сред, где нужен запуск от конкретного непривилегированного пользователя (NAS-устройства, Kubernetes с контекстами безопасности, организационные политики), BentoPDF предоставляет отдельный Dockerfile с поддержкой PUID/PGID в стиле LSIO.
### Сборка и запуск
```bash
# 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
```
### Переменные окружения
| Переменная | Описание | По умолчанию |
| -------------- | --------------------------- | ------------ |
| `PUID` | ID пользователя для запуска | `1000` |
| `PGID` | ID группы для запуска | `1000` |
| `PORT` | Порт Nginx | `8080` |
| `DISABLE_IPV6` | Отключить слушатель IPv6 | `false` |
### Docker Compose
```yaml
services:
bentopdf:
build:
context: .
dockerfile: Dockerfile.nonroot
container_name: bentopdf
ports:
- '3000:8080'
environment:
- PUID=1000
- PGID=1000
restart: unless-stopped
```
### Как это работает
Контейнер стартует от root, создаёт пользователя с заданными PUID/PGID, меняет владельца всех каталогов с правом записи, затем сбрасывает привилегии через `su-exec`. Процесс nginx работает целиком от указанного вами пользователя.
> [!NOTE]
> Стандартный `Dockerfile` использует `nginx-unprivileged` (UID 101) и рекомендуется для большинства развёртываний. Используйте `Dockerfile.nonroot` только когда нужен конкретный UID/GID.
> [!WARNING]
> PUID/PGID не могут быть равны `0` (root). Точка входа проверяет значения и завершится с ошибкой при недопустимых.
## С Traefik (обратный прокси)
```yaml
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-simple: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
```
## С Caddy (обратный прокси)
```yaml
services:
caddy:
image: caddy:2
ports:
- '80:80'
- '443:443'
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
bentopdf:
image: ghcr.io/alam00000/bentopdf-simple: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"
}
```
## Ограничения ресурсов
```yaml
services:
bentopdf:
image: ghcr.io/alam00000/bentopdf-simple:latest
deploy:
resources:
limits:
cpus: '1'
memory: 512M
reservations:
cpus: '0.25'
memory: 128M
```
## Podman Quadlet (интеграция с systemd)
[Quadlet](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html) позволяет запускать контейнеры Podman как службы systemd. Это идеально подходит для рабочих развёртываний в Linux.
### Базовая настройка Quadlet
Создайте файл юнита контейнера `~/.config/containers/systemd/bentopdf.container` (пользовательский) или `/etc/containers/systemd/bentopdf.container` (системный):
```ini
[Unit]
Description=BentoPDF - Privacy-first PDF toolkit
After=network-online.target
Wants=network-online.target
[Container]
Image=ghcr.io/alam00000/bentopdf-simple:latest
ContainerName=bentopdf
PublishPort=3000:8080
AutoUpdate=registry
[Service]
Restart=always
TimeoutStartSec=300
[Install]
WantedBy=default.target
```
### Включение и запуск
```bash
# 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]
> Для общесистемного развёртывания используйте `systemctl` без флага `--user` и поместите файл в `/etc/containers/systemd/`.
### Quadlet с проверкой работоспособности
```ini
[Unit]
Description=BentoPDF with health monitoring
After=network-online.target
Wants=network-online.target
[Container]
Image=ghcr.io/alam00000/bentopdf-simple: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
```
### Автообновление с Quadlet
Podman может автоматически обновлять контейнеры, когда появляются новые образы:
```bash
# 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
Для собственной сетевой конфигурации создайте файл сети `bentopdf.network`:
```ini
[Network]
Subnet=10.89.0.0/24
Gateway=10.89.0.1
```
Затем сошлитесь на него в файле контейнера:
```ini
[Container]
Image=ghcr.io/alam00000/bentopdf-simple:latest
ContainerName=bentopdf
PublishPort=3000:8080
Network=bentopdf.network
```
## Обновление
```bash
# Pull latest image
docker compose pull
# Recreate container
docker compose up -d
```
+266
View File
@@ -0,0 +1,266 @@
# Развёртывание на Hostinger
[Hostinger](https://hostinger.com) популярный провайдер виртуального хостинга. Это руководство описывает развёртывание BentoPDF на виртуальном хостинге Hostinger.
## Требования
- Тариф хостинга Hostinger с доступом к файловому менеджеру
- Node.js, установленный локально для сборки
## Шаг 1: соберите проект
```bash
git clone https://github.com/alam00000/bentopdf.git
cd bentopdf
npm install
npm run build
```
Готовые файлы будут в папке `dist`.
## Шаг 2: загрузите на Hostinger
### Развёртывание в корне домена
1. Войдите в свой аккаунт Hostinger
2. Откройте **File Manager** → **public_html**
3. **Удалите** все существующие файлы (при необходимости сделайте резервную копию)
4. **Загрузите** всё содержимое локальной папки `dist` в `public_html`
### Развёртывание в подкаталоге
Если вы развёртываете в подкаталог (например, `yourdomain.com/pdf-tools/`):
1. Соберите проект с правильным базовым URL:
```bash
BASE_URL=/pdf-tools/ npm run build
```
2. Создайте папку на Hostinger:
- Откройте **File Manager** → **public_html**
- Создайте новую папку: `pdf-tools`
3. Загрузите всё содержимое `dist` в `public_html/pdf-tools/`
## Шаг 3: создайте файл .htaccess
Создайте файл `.htaccess` в корне папки развёртывания (`public_html` или `public_html/pdf-tools/`):
::: warning Важно
При развёртывании в подкаталог замените `RewriteBase /` на `RewriteBase /pdf-tools/` (или имя вашей папки).
:::
```apache
RewriteEngine On
RewriteBase /
# ============================================
# 1. SECURITY HEADERS (CRITICAL FOR WASM)
# ============================================
<IfModule mod_headers.c>
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
# REQUIRED for soffice.js (SharedArrayBuffer)
Header always set Cross-Origin-Opener-Policy "same-origin"
Header always set Cross-Origin-Embedder-Policy "require-corp"
</IfModule>
# ============================================
# 2. BROWSER CACHING
# ============================================
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/gif "access plus 1 year"
ExpiresByType image/webp "access plus 1 year"
ExpiresByType image/svg+xml "access plus 1 year"
ExpiresByType image/x-icon "access plus 1 year"
ExpiresByType font/woff2 "access plus 1 year"
ExpiresByType font/woff "access plus 1 year"
ExpiresByType font/ttf "access plus 1 year"
ExpiresByType text/css "access plus 1 month"
ExpiresByType application/javascript "access plus 1 month"
ExpiresByType application/wasm "access plus 1 year"
ExpiresByType application/gzip "access plus 1 year"
ExpiresByType text/html "access plus 0 seconds"
</IfModule>
# ============================================
# 3. COMPRESSION (STANDARD)
# ============================================
# Prevent server from double-compressing files
SetEnvIfNoCase Request_URI "\.gz$" no-gzip
SetEnvIfNoCase Request_URI "\.br$" no-gzip
SetEnvIfNoCase Request_URI "\.wasm$" no-gzip
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/plain
AddOutputFilterByType DEFLATE text/html
AddOutputFilterByType DEFLATE text/xml
AddOutputFilterByType DEFLATE text/css
AddOutputFilterByType DEFLATE application/xml
AddOutputFilterByType DEFLATE application/xhtml+xml
AddOutputFilterByType DEFLATE application/rss+xml
AddOutputFilterByType DEFLATE application/javascript
AddOutputFilterByType DEFLATE application/x-javascript
AddOutputFilterByType DEFLATE application/json
AddOutputFilterByType DEFLATE image/svg+xml
AddOutputFilterByType DEFLATE font/woff
AddOutputFilterByType DEFLATE font/ttf
</IfModule>
# ============================================
# 4. MIME TYPES & SPECIAL FILE HANDLING
# ============================================
AddType application/javascript .js .mjs
AddType application/wasm .wasm
AddType font/woff2 .woff2
AddType font/woff .woff
AddType image/webp .webp
# Handle soffice.wasm.gz correctly
<FilesMatch "soffice\.wasm\.gz$">
ForceType application/wasm
Header set Content-Encoding "gzip"
Header set Cross-Origin-Resource-Policy "cross-origin"
Header append Vary Accept-Encoding
</FilesMatch>
# Handle data.gz
<FilesMatch "soffice\.data\.gz$">
ForceType application/octet-stream
Header set Content-Encoding "gzip"
Header append Vary Accept-Encoding
</FilesMatch>
# ============================================
# 5. REDIRECTS & ROUTING
# ============================================
# Canonical WWW (update domain as needed)
RewriteCond %{HTTP_HOST} ^bentopdf\.com [NC]
RewriteRule ^(.*)$ https://www.bentopdf.com/$1 [L,R=301]
# Force HTTPS
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
# Remove trailing slash (except for language root directories)
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} !^/(de|es|zh|zh-TW|vi|it|id|tr|fr|pt)/$
RewriteCond %{REQUEST_URI} (.+)/$
RewriteRule ^ %1 [R=301,L]
# Existing files/dirs - serve directly
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
# ============================================
# 5.1. LANGUAGE ROUTES
# ============================================
# Supported languages: de, es, zh, zh-TW, vi, it, id, tr, fr, pt
# English has no prefix - served from root
# English prefix redirects to root (for SEO consistency)
RewriteRule ^en/?$ / [R=301,L]
RewriteRule ^en/(.+)$ /$1 [R=301,L]
# Language prefix root (e.g., /de/ -> /de/index.html)
RewriteCond %{DOCUMENT_ROOT}/$1/index.html -f
RewriteRule ^(de|es|zh|zh-TW|vi|it|id|tr|fr|pt)/?$ /$1/index.html [L]
# Language prefix with path (e.g., /de/merge-pdf -> /de/merge-pdf.html)
RewriteCond %{DOCUMENT_ROOT}/$1/$2.html -f
RewriteRule ^(de|es|zh|zh-TW|vi|it|id|tr|fr|pt)/([^/]+)/?$ /$1/$2.html [L]
# ============================================
# 5.5. DOCS ROUTING (VitePress)
# ============================================
RewriteCond %{REQUEST_URI} ^/docs
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME}\.html -f
RewriteRule ^(.*)$ $1.html [L]
# ============================================
# 6. ADD .HTML EXTENSION IF FILE EXISTS (ROOT LEVEL ONLY)
# ============================================
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME}.html -f
RewriteRule ^([^/]+)$ $1.html [L]
# ============================================
# 7. ERROR PAGES
# ============================================
ErrorDocument 404 /404.html
```
## Пример .htaccess для подкаталога
Для `yourdomain.com/pdf-tools/` измените такие строки:
```apache
RewriteBase /pdf-tools/
# ... (same content as above, but update paths) ...
# Language prefix root
RewriteCond %{DOCUMENT_ROOT}/pdf-tools/$1/index.html -f
RewriteRule ^(de|es|zh|zh-TW|vi|it|id|tr|fr|pt)/?$ /pdf-tools/$1/index.html [L]
# Language prefix with path
RewriteCond %{DOCUMENT_ROOT}/pdf-tools/$1/$2.html -f
RewriteRule ^(de|es|zh|zh-TW|vi|it|id|tr|fr|pt)/([^/]+)/?$ /pdf-tools/$1/$2.html [L]
# Root level .html extension
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME}.html -f
RewriteRule ^([^/]+)$ $1.html [L]
ErrorDocument 404 /pdf-tools/404.html
```
## Устранение неполадок
### Файлы WASM не загружаются
Убедитесь, что MIME-типы правильно заданы в `.htaccess`:
```apache
AddType application/wasm .wasm
```
### Инструменты LibreOffice не работают
Заголовки безопасности критически важны для SharedArrayBuffer:
```apache
Header always set Cross-Origin-Opener-Policy "same-origin"
Header always set Cross-Origin-Embedder-Policy "require-corp"
```
Если заголовки не применяются, обратитесь в поддержку Hostinger, чтобы включить `mod_headers`.
### Ошибки 404 при обновлении страницы
Убедитесь, что правило для расширения `.html` и языковые маршруты настроены правильно. BentoPDF использует статические HTML-файлы, а не маршрутизацию SPA:
```apache
# Language routes serve actual files from language directories
RewriteCond %{DOCUMENT_ROOT}/$1/$2.html -f
RewriteRule ^(de|es|zh|zh-TW|vi|it|id|tr|fr|pt)/([^/]+)/?$ /$1/$2.html [L]
# Root level pages
RewriteCond %{REQUEST_FILENAME}.html -f
RewriteRule ^([^/]+)$ $1.html [L]
```
### Ограничения на загрузку файлов
У Hostinger могут быть ограничения на размер файлов. Если массовая загрузка не удаётся, загружайте файлы WASM отдельно.
+538
View File
@@ -0,0 +1,538 @@
# Руководство по самостоятельному размещению
BentoPDF можно разместить на собственной инфраструктуре. В этом руководстве описаны разные варианты развертывания.
## Быстрый старт с Docker / Podman
Самый быстрый способ разместить BentoPDF у себя:
> [!TIP]
> BentoPDF поставляется в двух сборках:
>
> - **Сборка для самостоятельного размещения**: `ghcr.io/alam00000/bentopdf-simple:latest`. Все инструменты PDF, **без** маркетингового оформления (без главного экрана, FAQ, отзывов и подвала). Используйте ее для внутренних развертываний в команде или организации. Это **не** урезанная «облегченная» версия.
> - **Коммерческая сборка**: `ghcr.io/alam00000/bentopdf:latest`. Полный маркетинговый сайт, который использует сам bentopdf.com и держатели коммерческих лицензий для публичных развертываний.
>
> Если сомневаетесь, берите сборку для самостоятельного размещения.
> [!IMPORTANT]
> Для конвертации офисных файлов нужен `SharedArrayBuffer`, а значит, приложение должно быть изолировано между источниками (cross-origin isolated) и раздаваться из защищенного контекста. Официальный образ уже отправляет нужные заголовки COOP/COEP, но браузеры все равно отключают `SharedArrayBuffer` на обычных HTTP-адресах в локальной сети, например `http://192.168.x.x`.
>
> Используйте `http://localhost` только для проверки на одном устройстве. Если пользователи открывают BentoPDF по IP-адресу или имени хоста в локальной сети, настройте HTTPS.
```bash
# Docker
docker run -d -p 3000:8080 ghcr.io/alam00000/bentopdf-simple:latest
# Podman
podman run -d -p 3000:8080 ghcr.io/alam00000/bentopdf-simple:latest
```
Или с помощью Docker Compose / Podman Compose:
```yaml
# docker-compose.yml
services:
bentopdf:
image: ghcr.io/alam00000/bentopdf-simple:latest
ports:
- '3000:8080'
restart: unless-stopped
```
```bash
# Docker Compose
docker compose up -d
# Podman Compose
podman-compose up -d
```
## Podman Quadlet (Linux Systemd)
Запустите BentoPDF как службу systemd. Создайте `~/.config/containers/systemd/bentopdf.container`:
```ini
[Container]
Image=ghcr.io/alam00000/bentopdf-simple:latest
ContainerName=bentopdf
PublishPort=3000:8080
AutoUpdate=registry
[Service]
Restart=always
[Install]
WantedBy=default.target
```
```bash
systemctl --user daemon-reload
systemctl --user enable --now bentopdf
```
Полную документацию по Quadlet смотрите в [руководстве по развертыванию Docker](/ru/self-hosting/docker).
## Сборка из исходного кода
```bash
# Clone and build
git clone https://github.com/alam00000/bentopdf.git
cd bentopdf
npm install
npm run build
# The built files are in the `dist` folder
```
## Параметры конфигурации
### Сборка для самостоятельного размещения (Simple Mode)
Сборка для самостоятельного размещения (образ `bentopdf-simple`, также называемая Simple Mode) **функционально идентична** коммерческой сборке: все инструменты PDF на месте и работают одинаково. Она лишь скрывает маркетинговое оформление, которое имеет смысл только на самом bentopdf.com или в коммерческом публичном развертывании. **Это не урезанная и не «облегченная» версия.**
**Что скрывает сборка для самостоятельного размещения** (только оформление, функции PDF не удаляются):
- Панель навигации, главный экран, раздел возможностей, FAQ, отзывы, подвал
- Заголовок страницы меняется на «Инструменты PDF»
**Что сохраняет сборка для самостоятельного размещения** (все, что реально работает с PDF):
- Все инструменты PDF (объединение, разделение, редактирование, подпись, OCR, конвертация офисных файлов и т. д.)
- Поддержку собственного брендинга, всю конфигурацию на этапе сборки и во время работы
Коммерческая сборка (`ghcr.io/alam00000/bentopdf:latest`) лежит в основе самого bentopdf.com, и ее используют держатели коммерческих лицензий для публичных развертываний. Она добавляет главный экран, FAQ, отзывы и подвал, которые не нужны во внутреннем инструменте.
Если вы размещаете BentoPDF для своей команды, организации или как внутренний инструмент, берите сборку для самостоятельного размещения:
```bash
# Use the pre-built image (recommended)
docker run -p 3000:8080 ghcr.io/alam00000/bentopdf-simple:latest
# Or build it yourself
SIMPLE_MODE=true npm run build
```
Подробности смотрите в [SIMPLE_MODE.md](https://github.com/alam00000/bentopdf/blob/main/SIMPLE_MODE.md).
### Коммерческая сборка (публичные развертывания под вашим брендом)
Коммерческая сборка (образ `bentopdf`, без суффикса `-simple`) лежит в основе самого bentopdf.com. Она включает полный маркетинговый сайт (главный экран, возможности, FAQ, отзывы, подвал) в дополнение ко всем инструментам PDF. Используйте эту сборку, когда хотите **развернуть BentoPDF как публичный сервис PDF под собственным брендом**, например:
- Вы запускаете BentoPDF как размещаемый SaaS для конечных пользователей на своем домене
- Вам нужна посадочная страница (маркетинговые разделы плюс инструменты), а не только сами инструменты
- Вы держатель коммерческой лицензии и встраиваете BentoPDF в коммерческий продукт
**Запустите как есть** (с брендингом BentoPDF, удобно для предпросмотра сборки):
```bash
docker run -p 3000:8080 ghcr.io/alam00000/bentopdf:latest
```
**Соберите со своим брендом** (типичный коммерческий сценарий):
```bash
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. All rights reserved." \
-t acmepdf .
docker run -p 3000:8080 acmepdf
```
Все остальные параметры времени сборки (`BASE_URL`, `VITE_DEFAULT_LANGUAGE`, `DISABLE_TOOLS`, переопределения URL для WASM и т. д.) работают так же, как в сборке для самостоятельного размещения.
::: warning Лицензирование
Запуск коммерческой сборки разрешен по любому из двух вариантов лицензии BentoPDF:
- **AGPL-3.0** (бесплатно): допускается, если ваше развертывание публикует весь исходный код под AGPL, включая любые изменения брендинга и окружающую бизнес-логику.
- **Коммерческая лицензия** (79 $ навсегда): требуется для закрытых и проприетарных развертываний, где вы не публикуете исходный код своего форка с брендингом или бизнес-код.
Полное сравнение смотрите на [странице лицензирования](https://www.bentopdf.com/licensing). Модули WASM под лицензией AGPL (PyMuPDF, Ghostscript, CoherentPDF) загружаются из CDN во время работы, поэтому они не попадают в ваш образ и не меняют ваш лицензионный статус.
:::
### Базовый URL
Развертывание в подкаталоге:
```bash
BASE_URL=/pdf-tools/ npm run build
```
### Собственный брендинг
Замените стандартные логотип, название и текст подвала BentoPDF на свои при сборке:
| Переменная | Описание | По умолчанию |
| ------------------ | -------------------------------------- | --------------------------------------- |
| `VITE_BRAND_NAME` | Название бренда в заголовке и подвале | `BentoPDF` |
| `VITE_BRAND_LOGO` | Путь к логотипу относительно `public/` | `images/favicon-no-bg.svg` |
| `VITE_FOOTER_TEXT` | Свой текст подвала и авторских прав | `© 2026 BentoPDF. All rights reserved.` |
```bash
# Place your logo in public/, then build
VITE_BRAND_NAME="AcmePDF" \
VITE_BRAND_LOGO="images/acme-logo.svg" \
VITE_FOOTER_TEXT="© 2026 Acme Corp. Internal use only." \
npm run build
```
Или через Docker:
```bash
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 .
```
Брендинг работает как в полном режиме, так и в Simple Mode и сочетается со всеми остальными параметрами времени сборки (`BASE_URL`, `SIMPLE_MODE`, `VITE_DEFAULT_LANGUAGE`).
### Отключение отдельных инструментов
Скрывайте отдельные инструменты ради соответствия требованиям или безопасности. Отключенные инструменты удаляются с главной страницы, из поиска, быстрых клавиш, конструктора рабочих процессов и недоступны по прямому URL.
Идентификаторы инструментов совпадают с URL страницы без `.html`: откройте любой инструмент и посмотрите на адрес (например, `edit-pdf`, `sign-pdf`, `encrypt-pdf`).
**Во время сборки** (вшивается в бандл):
```bash
DISABLE_TOOLS="edit-pdf,sign-pdf" npm run build
```
**Во время работы** (пересборка не нужна, смонтируйте `config.json`):
```json
{
"disabledTools": ["edit-pdf", "sign-pdf"]
}
```
```bash
docker run -d -p 3000:8080 \
-v ./config.json:/usr/share/nginx/html/config.json:ro \
ghcr.io/alam00000/bentopdf-simple:latest
```
Оба способа можно сочетать: списки объединяются.
Вы также можете отключить отдельные функции внутри редактора PDF (например, редактирование с удалением данных), не отключая инструмент целиком, добавив `editorDisabledCategories` в `config.json`. Полный список категорий смотрите в [руководстве по Docker](/ru/self-hosting/docker#отключение-функции-редактора).
## Руководства по развертыванию
Выберите платформу:
- [Vercel](/ru/self-hosting/vercel)
- [Netlify](/ru/self-hosting/netlify)
- [Cloudflare Pages](/ru/self-hosting/cloudflare)
- [AWS S3 + CloudFront](/ru/self-hosting/aws)
- [Hostinger](/ru/self-hosting/hostinger)
- [Nginx](/ru/self-hosting/nginx)
- [Apache](/ru/self-hosting/apache)
- [Docker](/ru/self-hosting/docker)
- [Kubernetes](/ru/self-hosting/kubernetes)
- [CORS-прокси](/ru/self-hosting/cors-proxy) - необходим для цифровых подписей
## Частые проблемы
### Конвертация Word / ODT / Excel / PowerPoint в PDF зависает (SharedArrayBuffer недоступен)
**Симптом**: конвертация документов через LibreOffice (Word, ODT, Excel, PowerPoint в PDF) зависает примерно на 55% или не запускается. В консоли браузера может быть:
> `ReferenceError: SharedArrayBuffer is not defined`
…или `window.crossOriginIsolated` возвращает `false`, или компиляция WASM сообщает `expected magic word 00 61 73 6d, found 1f 8b 08 08`.
**Причина**: LibreOffice WASM требует `SharedArrayBuffer`, который браузер включает, только когда страница **изолирована между источниками (cross-origin isolated)** И раздается из **защищенного контекста**. Для этого должны выполняться два условия:
1. В каждом ответе есть оба заголовка:
- `Cross-Origin-Embedder-Policy: require-corp`
- `Cross-Origin-Opener-Policy: same-origin`
2. Страница раздается по `https://...` или `http://localhost`. Обычный HTTP на IP-адресе в локальной сети (например, `http://192.168.x.x`) защищенным НЕ считается: браузеры отключают там `SharedArrayBuffer`.
Несовпадение `00 61 73 6d / 1f 8b 08 08` является отдельным подсимптомом: у предварительно сжатых файлов `.wasm.gz` / `.data.gz` нет заголовка ответа `Content-Encoding: gzip`, поэтому браузер получает необработанные байты gzip вместо распакованного WASM.
**Решение**: точную конфигурацию смотрите в руководстве для вашей платформы:
- [Nginx →](/ru/self-hosting/nginx#конвертация-word-odt-excel-в-pdf-не-работает)
- [Apache →](/ru/self-hosting/apache#конвертация-word-odt-excel-в-pdf-не-работает)
- [AWS S3 + CloudFront →](/ru/self-hosting/aws#шаг-3b-политика-заголовков-ответа-обязательна-для-libreoffice-wasm)
- [Cloudflare Pages →](/ru/self-hosting/cloudflare#фаил-конфигурации) (файл `_headers`)
- [Netlify →](/ru/self-hosting/netlify#конвертация-word-odt-excel-в-pdf-зависает-на-55) (`netlify.toml`)
- [Vercel →](/ru/self-hosting/vercel#конвертация-word-odt-excel-в-pdf-не-работает) (`vercel.json`)
- [Hostinger →](/ru/self-hosting/hostinger#инструменты-libreoffice-не-работают) (`.htaccess`)
- [Kubernetes →](/ru/self-hosting/kubernetes#сохранение-заголовков-sharedarraybuffer-ingress-gateway)
- **Docker**: обрабатывается автоматически встроенной конфигурацией nginx, никаких действий не требуется.
**Проверка**: откройте консоль DevTools на любой странице BentoPDF и выполните:
```js
console.log(window.crossOriginIsolated); // should be true
console.log(typeof SharedArrayBuffer); // should be "function"
```
Если страница раздается по HTTPS или `http://localhost` И оба заголовка COEP/COOP присутствуют в каждом ответе, обе проверки пройдут. Если вы открываете `http://192.168.x.x` или другой HTTP-адрес не из loopback, настройте HTTPS: решения только через заголовки не существует.
### Файлы `.mjs` отдаются как `application/octet-stream`
**Симптом**: Sign PDF / Form Filler и некоторые другие инструменты показывают пустой просмотрщик или не загружаются. В консоли браузера:
> `Failed to load module script: The server responded with a non-JavaScript MIME type of "application/octet-stream". Strict MIME type checking is enforced for module scripts per HTML spec.`
**Причина**: в вашем веб-сервере или обратном прокси нет соответствия MIME-типа для файлов `.mjs` (встроенный просмотрщик PDF поставляется с ES-модулями с таким расширением). Во многих стандартных конфигурациях для неизвестных расширений по умолчанию используется `application/octet-stream`, и браузеры отказываются выполнять такие файлы как ES-модули.
**Решение**: нужный фрагмент смотрите в руководстве для вашей платформы:
- [Nginx →](/ru/self-hosting/nginx#sign-pdf-или-form-filler-показывает-пустои-просмотрщик-ошибка-mime-для-mjs)
- [Apache →](/ru/self-hosting/apache#sign-pdf-или-form-filler-показывают-пустои-просмотрщик-ошибка-mime-для-mjs)
- [AWS S3 + CloudFront →](/ru/self-hosting/aws#шаг-2-сборка-и-загрузка) (см. `aws s3 cp ... --include "*.mjs"`)
- [Kubernetes →](/ru/self-hosting/kubernetes#ошибки-mime-типа-mjs-пустои-iframe-в-sign-pdf-form-filler)
- **Docker, Vercel, Netlify, Cloudflare Pages, Hostinger**: обрабатывается автоматически, никаких действий не требуется.
**Проверка**: откройте DevTools → вкладка Network, найдите неудавшийся запрос `.mjs` и проверьте заголовок ответа `Content-Type`. Там должно быть `application/javascript`. Если по-прежнему `application/octet-stream`, возможно, внешний обратный прокси или CDN заново определяет тип: проверьте каждый уровень вашей цепочки.
## Конфигурация WASM (компоненты AGPL)
BentoPDF **не включает** библиотеки обработки под лицензией AGPL в свой исходный код, но **заранее настраивает URL CDN**, поэтому все функции работают сразу, без ручной настройки.
::: tip Работает без настройки
Начиная с v2.0.0, модули WASM заранее настроены на загрузку из CDN jsDelivr через переменные окружения. Все расширенные функции работают сразу, без какой-либо настройки пользователем.
:::
| Компонент | Лицензия | Возможности |
| --------------- | -------- | -------------------------------------------------------------- |
| **PyMuPDF** | AGPL-3.0 | Конвертация EPUB/MOBI/FB2/XPS, извлечение изображений и таблиц |
| **Ghostscript** | AGPL-3.0 | Конвертация в PDF/A, сжатие, выравнивание, растрирование |
| **CoherentPDF** | AGPL-3.0 | Оглавление, вложения, объединение PDF с закладками |
### Переменные окружения по умолчанию
Они заданы в `.env.production` и вшиваются в сборку:
```bash
VITE_WASM_PYMUPDF_URL=https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.16/
VITE_WASM_GS_URL=https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm@0.1.1/assets/
VITE_WASM_CPDF_URL=https://cdn.jsdelivr.net/npm/coherentpdf@2.5.5/dist/
VITE_TESSERACT_WORKER_URL=
VITE_TESSERACT_CORE_URL=
VITE_TESSERACT_LANG_URL=
VITE_TESSERACT_AVAILABLE_LANGUAGES=
VITE_OCR_FONT_BASE_URL=
```
### Переопределение URL для WASM
Значения по умолчанию можно переопределить при сборке для нестандартных развертываний:
```bash
# Via Docker build args
docker build \
--build-arg VITE_WASM_PYMUPDF_URL=https://your-server.com/pymupdf/ \
--build-arg VITE_WASM_GS_URL=https://your-server.com/gs/ \
--build-arg VITE_WASM_CPDF_URL=https://your-server.com/cpdf/ \
--build-arg VITE_TESSERACT_WORKER_URL=https://your-server.com/ocr/worker.min.js \
--build-arg VITE_TESSERACT_CORE_URL=https://your-server.com/ocr/core \
--build-arg VITE_TESSERACT_LANG_URL=https://your-server.com/ocr/lang-data \
--build-arg VITE_TESSERACT_AVAILABLE_LANGUAGES=eng,deu \
--build-arg VITE_OCR_FONT_BASE_URL=https://your-server.com/ocr/fonts \
-t bentopdf .
# Or via .env.production before building from source
VITE_WASM_PYMUPDF_URL=https://your-server.com/pymupdf/ npm run build
```
Чтобы полностью отключить модуль (пользователю придется настраивать его вручную в расширенных настройках), задайте его переменной пустую строку.
Для OCR либо оставьте все переменные `VITE_TESSERACT_*` пустыми и используйте стандартные онлайн-ресурсы, либо задайте URL воркера, ядра и языковых данных вместе для автономного OCR на своей инфраструктуре. Если вы включаете в сборку только некоторые языки OCR, задайте также `VITE_TESSERACT_AVAILABLE_LANGUAGES` с теми же кодами через запятую, чтобы интерфейс предлагал только установленные языки, а неподдерживаемый выбор завершался понятной ошибкой. Для полностью автономного создания PDF с возможностью поиска задайте также `VITE_OCR_FONT_BASE_URL` с адресом внутренней директории, где лежат встроенные шрифты OCR.
Пользователи также могут в любой момент переопределить эти значения через **расширенные настройки** в интерфейсе: пользовательские настройки, сохраненные в браузере, имеют приоритет над значениями окружения по умолчанию.
### Развертывание в изолированной сети / офлайн
Для сетей без доступа в интернет (госсектор, здравоохранение, финансы и т. д.). URL для WASM вшиваются в JavaScript **при сборке**, а сами файлы WASM скачивает **браузер пользователя** во время работы. Поэтому все нужно подготовить на машине с интернетом, а затем перенести во внутреннюю сеть.
#### Автоматический скрипт (рекомендуется)
Входящий в состав скрипт `prepare-airgap.sh` автоматизирует весь процесс: скачивает пакеты WASM, собирает образ Docker и создает самодостаточный пакет со скриптом установки.
```bash
git clone https://github.com/alam00000/bentopdf.git
cd bentopdf
# Show supported OCR language codes (for --ocr-languages)
bash scripts/prepare-airgap.sh --list-ocr-languages
# Search OCR language codes by name or abbreviation
bash scripts/prepare-airgap.sh --search-ocr-language german
# Interactive mode — prompts for all options
bash scripts/prepare-airgap.sh
# Or fully automated
bash scripts/prepare-airgap.sh --wasm-base-url https://internal.example.com/wasm
```
В результате создается директория с пакетом:
```
bentopdf-airgap-bundle/
bentopdf.tar # Docker image
*.tgz # WASM packages (PyMuPDF, Ghostscript, CoherentPDF, Tesseract)
tesseract-langdata/ # OCR traineddata files
ocr-fonts/ # OCR text-layer font files
setup.sh # Setup script for the air-gapped side
README.md # Instructions
```
Перенесите пакет в изолированную сеть через USB, внутренний репозиторий артефактов или другим разрешенным способом. Затем запустите входящий в него скрипт установки:
```bash
cd bentopdf-airgap-bundle
bash setup.sh
```
Скрипт установки загружает образ Docker, распаковывает файлы WASM и при желании запускает контейнер.
**Параметры скрипта:**
| Флаг | Описание | По умолчанию |
| ------------------------------ | -------------------------------------------------------------- | --------------------------------------------- |
| `--wasm-base-url <url>` | Где файлы WASM будут размещены внутри сети | _(обязателен, запрашивается, если не указан)_ |
| `--image-name <name>` | Тег образа Docker | `bentopdf` |
| `--output-dir <path>` | Директория для итогового пакета | `./bentopdf-airgap-bundle` |
| `--simple-mode` | Включить Simple Mode | выкл. |
| `--base-url <path>` | Базовый URL подкаталога (например, `/pdf/`) | `/` |
| `--language <code>` | Язык интерфейса по умолчанию (например, `fr`, `de`) | _(нет)_ |
| `--brand-name <name>` | Свое название бренда | _(нет)_ |
| `--brand-logo <path>` | Путь к логотипу относительно `public/` | _(нет)_ |
| `--footer-text <text>` | Свой текст подвала | _(нет)_ |
| `--ocr-languages <list>` | Языки OCR для включения в пакет через запятую | `eng` |
| `--list-ocr-languages` | Вывести поддерживаемые коды и названия OCR и выйти | выкл. |
| `--search-ocr-language <term>` | Поиск кодов OCR по названию или сокращению | выкл. |
| `--dockerfile <path>` | Используемый Dockerfile | `Dockerfile` |
| `--skip-docker` | Пропустить сборку и экспорт Docker | выкл. |
| `--skip-wasm` | Пропустить загрузку WASM (использовать имеющиеся файлы `.tgz`) | выкл. |
Интерактивный режим также принимает `list`, чтобы вывести полный список поддерживаемых кодов Tesseract, и `search <term>`, чтобы найти совпадения, например `search german` или `search chi`.
::: warning Требование одного источника
Файлы WASM должны раздаваться с **того же источника** (origin), что и приложение BentoPDF. Веб-воркеры используют `importScripts()`, который не может загружать скрипты с другого источника. Например, если BentoPDF работает на `https://internal.example.com`, базовый URL для WASM тоже должен быть `https://internal.example.com/wasm`.
:::
#### Ручные шаги
<details>
<summary>Если вы предпочитаете выполнить все вручную, без скрипта</summary>
**Шаг 1: Скачайте пакеты WASM и OCR** (на машине с интернетом)
```bash
npm pack @bentopdf/pymupdf-wasm@0.11.14
npm pack @bentopdf/gs-wasm
npm pack coherentpdf
npm pack tesseract.js@7.0.0
npm pack tesseract.js-core@7.0.0
mkdir -p tesseract-langdata
curl -fsSL https://cdn.jsdelivr.net/npm/@tesseract.js-data/eng/4.0.0_best_int/eng.traineddata.gz -o tesseract-langdata/eng.traineddata.gz
mkdir -p ocr-fonts
curl -fsSL https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSans/NotoSans-Regular.ttf -o ocr-fonts/NotoSans-Regular.ttf
```
**Шаг 2: Соберите образ Docker с внутренними URL**
```bash
git clone https://github.com/alam00000/bentopdf.git
cd bentopdf
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/ \
--build-arg VITE_TESSERACT_WORKER_URL=https://internal-server.example.com/wasm/ocr/worker.min.js \
--build-arg VITE_TESSERACT_CORE_URL=https://internal-server.example.com/wasm/ocr/core \
--build-arg VITE_TESSERACT_LANG_URL=https://internal-server.example.com/wasm/ocr/lang-data \
--build-arg VITE_OCR_FONT_BASE_URL=https://internal-server.example.com/wasm/ocr/fonts \
-t bentopdf .
```
**Шаг 3: Экспортируйте образ Docker**
```bash
docker save bentopdf -o bentopdf.tar
```
**Шаг 4: Перенесите во внутреннюю изолированную сеть**
Скопируйте через USB, внутренний репозиторий артефактов или другой разрешенный способ передачи:
- `bentopdf.tar`: образ Docker
- Пять пакетов `.tgz` WASM/OCR из шага 1
- Директорию `tesseract-langdata/` из шага 1
- Директорию `ocr-fonts/` из шага 1
**Шаг 5: Настройте внутри изолированной сети**
```bash
# Load the Docker image
docker load -i bentopdf.tar
# Extract WASM packages
mkdir -p ./wasm/pymupdf ./wasm/gs ./wasm/cpdf ./wasm/ocr/core ./wasm/ocr/lang-data ./wasm/ocr/fonts
tar xzf bentopdf-pymupdf-wasm-0.11.14.tgz -C ./wasm/pymupdf --strip-components=1
tar xzf bentopdf-gs-wasm-*.tgz -C ./wasm/gs --strip-components=1
tar xzf coherentpdf-*.tgz -C ./wasm/cpdf --strip-components=1
TEMP_TESS=$(mktemp -d)
tar xzf tesseract.js-7.0.0.tgz -C "$TEMP_TESS"
cp "$TEMP_TESS/package/dist/worker.min.js" ./wasm/ocr/worker.min.js
rm -rf "$TEMP_TESS"
tar xzf tesseract.js-core-7.0.0.tgz -C ./wasm/ocr/core --strip-components=1
cp ./tesseract-langdata/*.traineddata.gz ./wasm/ocr/lang-data/
cp ./ocr-fonts/* ./wasm/ocr/fonts/
# Run BentoPDF
docker run -d -p 3000:8080 --restart unless-stopped bentopdf
```
Убедитесь, что файлы доступны по URL, настроенным на шаге 2, включая `.../ocr/worker.min.js`, `.../ocr/core`, `.../ocr/lang-data` и `.../ocr/fonts`.
</details>
::: info Собираете из исходного кода, а не через Docker?
Задайте переменные в `.env.production` перед запуском `npm run build`:
```bash
VITE_WASM_PYMUPDF_URL=https://internal-server.example.com/wasm/pymupdf/
VITE_WASM_GS_URL=https://internal-server.example.com/wasm/gs/
VITE_WASM_CPDF_URL=https://internal-server.example.com/wasm/cpdf/
VITE_TESSERACT_WORKER_URL=https://internal-server.example.com/wasm/ocr/worker.min.js
VITE_TESSERACT_CORE_URL=https://internal-server.example.com/wasm/ocr/core
VITE_TESSERACT_LANG_URL=https://internal-server.example.com/wasm/ocr/lang-data
VITE_OCR_FONT_BASE_URL=https://internal-server.example.com/wasm/ocr/fonts
```
:::
### Собственный прокси для WASM
Если нужно раздавать файлы WASM под лицензией AGPL с правильными заголовками CORS, можно развернуть простой прокси. Пример реализации смотрите в [руководстве по Cloudflare WASM Proxy](https://github.com/alam00000/bentopdf/blob/main/cloudflare/WASM-PROXY.md).
::: tip Зачем разделять?
Такое разделение обеспечивает:
- Юридическую ясность для коммерческих пользователей
- Ядро BentoPDF остается под двойной лицензией (AGPL-3.0 / коммерческая)
- Файлы WASM загружаются во время работы и не входят в исходный код
:::
## Системные требования
| Требование | Минимум |
| ---------- | ------------------------------- |
| Диск | ~100 МБ (ядро без модулей AGPL) |
| ОЗУ | 512 МБ |
| Процессор | Любой современный |
::: tip
BentoPDF является статическим сайтом: база данных и серверная часть не нужны!
:::
+211
View File
@@ -0,0 +1,211 @@
# Развертывание в Kubernetes
Kubernetes может быть избыточен для статического сайта, но он хорошо подходит, если вы уже стандартизировали процессы на Helm и GitOps.
> [!IMPORTANT]
> **Обязательные заголовки для конвертации офисных файлов**
>
> Инструментам на основе LibreOffice (конвертация Word, Excel, PowerPoint) для поддержки `SharedArrayBuffer` нужны следующие HTTP-заголовки:
>
> - `Cross-Origin-Opener-Policy: same-origin`
> - `Cross-Origin-Embedder-Policy: require-corp`
>
> Официальные образы BentoPDF на nginx уже отправляют эти заголовки. В Kubernetes **контроллеры Ingress и Gateway тоже являются обратными прокси**, поэтому убедитесь, что заголовки сохраняются (или добавьте их на границе).
## Требования
- Кластер Kubernetes
- Helm v3
- Образ BentoPDF на nginx (например, `ghcr.io/alam00000/bentopdf:<tag>`), который работает на **порту 8080**
## Развертывание с помощью Helm
### Установка из этого репозитория (локальный чарт)
```bash
kubectl create namespace bentopdf
helm upgrade --install bentopdf /path/to/bentopdf/chart \
--namespace bentopdf \
--set image.repository=ghcr.io/alam00000/bentopdf \
--set image.tag=latest
```
### Установка из GHCR (чарт OCI)
Если чарт опубликован в GHCR как артефакт OCI:
```bash
export GHCR_USERNAME="<github-org-or-user>"
helm upgrade --install bentopdf oci://ghcr.io/$GHCR_USERNAME/charts/bentopdf \
--namespace bentopdf \
--create-namespace \
--version 0.1.0 \
--set image.repository=ghcr.io/alam00000/bentopdf \
--set image.tag=latest
```
## Как открыть доступ
### Port-forward (быстрая проверка)
```bash
kubectl -n bentopdf port-forward deploy/bentopdf 8080:8080
```
### Ingress (необязательно)
Включите Ingress (пример для nginx-ingress):
```yaml
ingress:
enabled: true
className: nginx
hosts:
- host: pdf.example.com
paths:
- path: /
pathType: Prefix
```
### Gateway API (необязательно)
Чарт поддерживает `Gateway` и `HTTPRoute` из Gateway API.
Пример (оператор Cloudflare Gateway API):
```yaml
gateway:
enabled: true
name: bento-tunnel
namespace: bentopdf
gatewayClassName: cloudflare
httpRoute:
enabled: true
parentRefs:
- name: bento-tunnel
namespace: bentopdf
sectionName: http
hostnames:
- pdfs.example.com
```
## Сохранение заголовков SharedArrayBuffer (Ingress/Gateway)
### Что должно происходить
Конфигурация nginx в BentoPDF задает нужные заголовки ответа. Большинство контроллеров Ingress и Gateway **передают заголовки ответа апстрима без изменений**.
### Что может сломаться
- Политика контроллера или границы, которая **переопределяет** или **удаляет** заголовки ответа
- Промежуточный слой «заголовков безопасности», который задает другие значения COOP/COEP
### Как проверить
Выполните команду для вашего публичного адреса:
```bash
curl -I https://pdf.example.com/ | egrep -i 'cross-origin-opener-policy|cross-origin-embedder-policy'
```
Вы должны увидеть:
- `Cross-Origin-Opener-Policy: same-origin`
- `Cross-Origin-Embedder-Policy: require-corp`
### Если ваш контроллер Ingress не сохраняет заголовки
Добавьте заголовки на границе (способ зависит от контроллера). Пример для **nginx-ingress**:
```yaml
ingress:
enabled: true
className: nginx
annotations:
nginx.ingress.kubernetes.io/configuration-snippet: |
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
```
## Ошибки MIME-типа `.mjs` (пустой iframe в Sign PDF / Form Filler)
Если в консоли браузера появляется `Failed to load module script: ... non-JavaScript MIME type "application/octet-stream"` для запроса `.mjs`, значит контроллер Ingress или Gateway перед nginx из BentoPDF подменяет заголовок `Content-Type` апстрима на `application/octet-stream`.
Nginx в образе BentoPDF уже отдает `.mjs` как `application/javascript`. Нужно запретить контроллеру удалять или заменять этот заголовок. Для nginx-ingress:
```yaml
ingress:
annotations:
nginx.ingress.kubernetes.io/configuration-snippet: |
location ~* \.mjs$ {
types {} default_type application/javascript;
}
```
Либо используйте фильтр `ResponseHeaderModifier` в Gateway API, чтобы задать `Content-Type: application/javascript` для путей `.mjs`. Проверьте на вкладке Network в DevTools: у неудавшегося запроса `.mjs` теперь должно быть `content-type: application/javascript`.
### Если вы используете Gateway API и хотите принудительно добавить заголовки
Gateway API поддерживает фильтр `ResponseHeaderModifier`. Его можно подключить в `httpRoute.rules[*].filters`:
```yaml
httpRoute:
enabled: true
hostnames: [pdf.example.com]
parentRefs:
- name: bento-tunnel
namespace: misc
sectionName: http
rules:
- matches:
- path: { type: PathPrefix, value: / }
filters:
- type: ResponseHeaderModifier
responseHeaderModifier:
set:
- name: Cross-Origin-Opener-Policy
value: same-origin
- name: Cross-Origin-Embedder-Policy
value: require-corp
```
Поддержка конкретных фильтров зависит от вашего контроллера Gateway. Если фильтр игнорируется, добавьте заголовки на уровне границы или контроллера.
## Отключение отдельных инструментов
Используйте ConfigMap, чтобы отключать инструменты во время работы без пересборки образа:
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: bentopdf-config
namespace: bentopdf
data:
config.json: |
{
"disabledTools": ["edit-pdf", "sign-pdf", "encrypt-pdf"]
}
```
Смонтируйте его в раздаваемую директорию:
```yaml
spec:
containers:
- name: bentopdf
volumeMounts:
- name: config
mountPath: /usr/share/nginx/html/config.json
subPath: config.json
readOnly: true
volumes:
- name: config
configMap:
name: bentopdf-config
```
Идентификаторы инструментов совпадают с URL страницы без `.html`: откройте любой инструмент и посмотрите на адрес (например, `edit-pdf`, `merge-pdf`, `compress-pdf`). Отключенные инструменты скрыты с главной страницы, из поиска, быстрых клавиш, конструктора рабочих процессов и недоступны по прямому URL. Полный список параметров смотрите в [руководстве по Docker](/ru/self-hosting/docker#отключение-отдельных-инструментов).
+139
View File
@@ -0,0 +1,139 @@
# Развертывание в Netlify
[Netlify](https://netlify.com) предлагает отличный хостинг статических сайтов с щедрым бесплатным тарифом.
## Развертывание в один клик
[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/alam00000/bentopdf)
## Ручное развертывание
### Шаг 1: Подключите репозиторий
1. Войдите в [Netlify](https://app.netlify.com)
2. Нажмите «Add new site» → «Import an existing project»
3. Подключите свой аккаунт GitHub
4. Выберите свой форк BentoPDF
### Шаг 2: Настройте параметры сборки
| Параметр | Значение |
| ----------------- | --------------- |
| Build command | `npm run build` |
| Publish directory | `dist` |
| Node version | 18+ |
### Шаг 3: Разверните
Нажмите «Deploy site» и дождитесь завершения сборки.
## Файл конфигурации
Создайте `netlify.toml` в корне проекта:
```toml
[build]
command = "npm run build"
publish = "dist"
[build.environment]
NODE_VERSION = "18"
# Required security headers for SharedArrayBuffer (used by LibreOffice WASM)
[[headers]]
for = "/*"
[headers.values]
Cross-Origin-Embedder-Policy = "require-corp"
Cross-Origin-Opener-Policy = "same-origin"
Cross-Origin-Resource-Policy = "cross-origin"
# Pre-compressed LibreOffice WASM binary - must be served with Content-Encoding
[[headers]]
for = "/libreoffice-wasm/soffice.wasm.gz"
[headers.values]
Content-Type = "application/wasm"
Content-Encoding = "gzip"
Cache-Control = "public, max-age=31536000, immutable"
# Pre-compressed LibreOffice WASM data - must be served with Content-Encoding
[[headers]]
for = "/libreoffice-wasm/soffice.data.gz"
[headers.values]
Content-Type = "application/octet-stream"
Content-Encoding = "gzip"
Cache-Control = "public, max-age=31536000, immutable"
# Cache other WASM files
[[headers]]
for = "*.wasm"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
Content-Type = "application/wasm"
# SPA routing
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
```
::: warning Важно
Заголовки `Cross-Origin-Embedder-Policy` и `Cross-Origin-Opener-Policy` необходимы для конвертации Word/ODT/Excel/PowerPoint в PDF. Без них `SharedArrayBuffer` недоступен, и движок LibreOffice WASM не сможет запуститься.
Заголовки `Content-Encoding: gzip` для файлов `.wasm.gz` и `.data.gz` указывают браузеру распаковывать их автоматически. Без них браузер получает необработанные байты gzip, и компиляция WASM завершается ошибкой.
:::
## Переменные окружения
Задайте их в Site settings → Environment variables:
| Переменная | Описание |
| ----------------------- | ----------------------------------------------------------------------- |
| `SIMPLE_MODE` | Укажите `true` для минимальной сборки |
| `VITE_BRAND_NAME` | Свое название бренда (заменяет «BentoPDF») |
| `VITE_BRAND_LOGO` | Путь к логотипу относительно `public/` (например, `images/my-logo.svg`) |
| `VITE_FOOTER_TEXT` | Свой текст подвала и авторских прав |
| `VITE_DEFAULT_LANGUAGE` | Язык интерфейса по умолчанию (например, `fr`, `de`, `es`) |
## Собственный домен
1. Перейдите в Site settings → Domain management
2. Нажмите «Add custom domain»
3. Следуйте инструкциям по настройке DNS
## Большие медиафайлы
Для больших файлов WASM рассмотрите включение [Netlify Large Media](https://docs.netlify.com/large-media/overview/):
```bash
netlify lm:setup
git lfs track "*.wasm"
```
## Устранение неполадок
### Конвертация Word/ODT/Excel в PDF зависает на 55%
Если конвертация документов зависает на 55%, откройте консоль DevTools и проверьте:
```js
console.log(window.crossOriginIsolated); // should be true
console.log(typeof SharedArrayBuffer); // should be "function"
```
Если `crossOriginIsolated` равно `false`, заголовки COEP/COOP из вашего `netlify.toml` не применяются. Убедитесь, что файл лежит в корне проекта, и разверните сайт заново.
Если в консоли появляется `expected magic word 00 61 73 6d, found 1f 8b 08 08`, у файлов `.wasm.gz` нет заголовков `Content-Encoding: gzip`. Убедитесь, что блоки `[[headers]]` для `soffice.wasm.gz` и `soffice.data.gz` есть в вашем `netlify.toml`.
### Сборка завершается ошибкой
Проверьте совместимость версии Node:
```toml
[build.environment]
NODE_VERSION = "20"
```
### Медленная первая загрузка
Включите оптимизацию ресурсов в Site settings → Build & deploy → Asset optimization.
+232
View File
@@ -0,0 +1,232 @@
# Развертывание с Nginx
Разместите BentoPDF на собственном сервере с помощью Nginx.
## Требования
- Сервер Ubuntu/Debian
- Установленный Nginx
- SSL-сертификат (рекомендуется Let's Encrypt)
## Шаг 1: Соберите проект
```bash
git clone https://github.com/alam00000/bentopdf.git
cd bentopdf
npm install
npm run build
```
Чтобы настроить брендинг, задайте переменные окружения перед сборкой:
```bash
VITE_BRAND_NAME="AcmePDF" VITE_BRAND_LOGO="images/acme-logo.svg" npm run build
```
## Шаг 2: Скопируйте файлы
```bash
sudo mkdir -p /var/www/bentopdf
sudo cp -r dist/* /var/www/bentopdf/
sudo chown -R www-data:www-data /var/www/bentopdf
```
## Шаг 3: Конфигурация Nginx
Создайте `/etc/nginx/sites-available/bentopdf`:
```nginx
server {
listen 80;
server_name your-domain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name your-domain.com;
# SSL Configuration
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
root /var/www/bentopdf;
index index.html;
# Gzip compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/wasm;
gzip_min_length 1000;
# WASM MIME type
types {
application/wasm wasm;
}
# Required headers for SharedArrayBuffer (LibreOffice WASM)
# These must be on every response - especially HTML pages
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Resource-Policy "cross-origin" always;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Pre-compressed LibreOffice WASM binary
location ~* /libreoffice-wasm/soffice\.wasm\.gz$ {
gzip off;
types {} default_type application/wasm;
add_header Content-Encoding gzip;
add_header Cache-Control "public, immutable";
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
}
# Pre-compressed LibreOffice WASM data
location ~* /libreoffice-wasm/soffice\.data\.gz$ {
gzip off;
types {} default_type application/octet-stream;
add_header Content-Encoding gzip;
add_header Cache-Control "public, immutable";
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
}
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|wasm)$ {
expires 1y;
add_header Cache-Control "public, immutable";
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
}
# SPA routing - serve index.html for all routes
location / {
try_files $uri $uri/ /index.html;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
}
}
```
## Шаг 4: Включите сайт
```bash
sudo ln -s /etc/nginx/sites-available/bentopdf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
```
## Шаг 5: SSL с Let's Encrypt
```bash
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d your-domain.com
```
## Развертывание в подкаталоге
Чтобы разместить сайт по адресу `/pdf/`:
```nginx
location /pdf/ {
alias /var/www/bentopdf/;
try_files $uri $uri/ /pdf/index.html;
}
```
Соберите проект так:
```bash
BASE_URL=/pdf/ npm run build
```
## Настройка производительности
Добавьте в `nginx.conf`:
```nginx
http {
# Enable sendfile
sendfile on;
tcp_nopush on;
tcp_nodelay on;
# Increase buffer sizes
client_max_body_size 100M;
# Worker connections
worker_connections 2048;
}
```
## Устранение неполадок
### WASM не загружается
Убедитесь, что задан MIME-тип:
```nginx
types {
application/wasm wasm;
}
```
### Sign PDF или Form Filler показывает пустой просмотрщик (ошибка MIME для `.mjs`)
Если в консоли браузера появляется:
```
Failed to load module script: The server responded with a non-JavaScript
MIME type of "application/octet-stream". Strict MIME type checking is
enforced for module scripts per HTML spec.
```
…и неудавшийся запрос оканчивается на `.mjs`, значит в вашем nginx нет соответствия `.mjs → application/javascript`. Встроенный просмотрщик PDF (его используют Sign PDF, Form Filler и несколько других инструментов) содержит файлы ES-модулей с расширением `.mjs`, а стандартный `mime.types` в nginx их по умолчанию не включает.
Исправьте это, добавив `mjs` в существующую строку `application/javascript` в `/etc/nginx/mime.types`:
```nginx
application/javascript js mjs;
```
> НЕ добавляйте _вторую_ строку `application/javascript`: nginx разбирает `mime.types` построчно, и в некоторых версиях вторая строка для того же типа молча игнорируется. Отредактируйте существующую строку, добавив `mjs` рядом с `js`.
Или переопределите это локально в блоке server (без правки глобального файла):
```nginx
types {
application/javascript js mjs;
application/wasm wasm;
}
```
После изменения перезагрузите nginx:
```bash
sudo nginx -t && sudo systemctl reload nginx
```
Для проверки найдите в DevTools на вкладке Network неудавшийся запрос `.mjs` и посмотрите заголовок ответа `Content-Type`. Теперь там должно быть `application/javascript`, а не `application/octet-stream`.
### Конвертация Word/ODT/Excel в PDF не работает
LibreOffice WASM требует `SharedArrayBuffer`, а для него нужны заголовки `Cross-Origin-Embedder-Policy` и `Cross-Origin-Opener-Policy`. Также нужен защищенный контекст: `http://localhost` подходит для локальной проверки, но для `http://192.168.x.x` и других адресов в локальной сети обычно требуется HTTPS. Учтите, что директивы `add_header` в блоке `location` **переопределяют** директивы `add_header` уровня server, а не объединяются с ними. Каждый блок `location` со своими `add_header` должен включать заголовки COEP/COOP.
Проверьте так:
```bash
curl -I https://your-domain.com/ | grep -i cross-origin
```
Если перед nginx стоит обратный прокси, убедитесь, что он сохраняет эти заголовки.
### 502 Bad Gateway
Проверьте журнал ошибок Nginx:
```bash
sudo tail -f /var/log/nginx/error.log
```
+122
View File
@@ -0,0 +1,122 @@
# Развертывание в Vercel
[Vercel](https://vercel.com) предлагает самый простой способ развертывания статических сайтов.
## Развертывание в один клик
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/alam00000/bentopdf)
## Ручное развертывание
### Шаг 1: Сделайте форк репозитория
Сделайте форк [bentopdf/bentopdf](https://github.com/alam00000/bentopdf) в свой аккаунт GitHub.
### Шаг 2: Импортируйте в Vercel
1. Перейдите на [vercel.com/new](https://vercel.com/new)
2. Выберите свой форк репозитория
3. Настройте проект:
| Параметр | Значение |
| ---------------- | --------------- |
| Framework Preset | Vite |
| Build Command | `npm run build` |
| Output Directory | `dist` |
| Install Command | `npm install` |
### Шаг 3: Переменные окружения (необязательно)
Добавьте их при необходимости:
| Переменная | Описание |
| ----------------------- | ----------------------------------------------------------------------- |
| `SIMPLE_MODE` | Укажите `true` для минимального интерфейса |
| `VITE_BRAND_NAME` | Свое название бренда (заменяет «BentoPDF») |
| `VITE_BRAND_LOGO` | Путь к логотипу относительно `public/` (например, `images/my-logo.svg`) |
| `VITE_FOOTER_TEXT` | Свой текст подвала и авторских прав |
| `VITE_DEFAULT_LANGUAGE` | Язык интерфейса по умолчанию (например, `fr`, `de`, `es`) |
### Шаг 4: Разверните
Нажмите «Deploy» и дождитесь завершения сборки.
## Собственный домен
1. Откройте настройки проекта
2. Перейдите в раздел «Domains»
3. Добавьте свой домен
4. Настройте DNS по инструкции
## Ограничения
::: warning Большие файлы
У serverless-функций Vercel есть ограничение в 50 МБ. Поскольку BentoPDF является статическим сайтом, это не должно вас затронуть, но модули WASM велики (около 100 МБ в сумме). Убедитесь, что они раздаются из папки `/public`.
:::
## Устранение неполадок
### Тайм-аут сборки
Если сборка завершается по тайм-ауту, попробуйте:
```json
// vercel.json
{
"buildCommand": "npm run build",
"outputDirectory": "dist"
}
```
### Ошибка 404 при обновлении страницы
Добавьте `vercel.json` для маршрутизации SPA:
```json
{
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
}
```
### Конвертация Word/ODT/Excel в PDF не работает
Для конвертации через LibreOffice WASM нужен `SharedArrayBuffer`, а для него на всех страницах должны быть следующие заголовки ответа:
- `Cross-Origin-Embedder-Policy: require-corp`
- `Cross-Origin-Opener-Policy: same-origin`
Предварительно сжатым файлам `.wasm.gz` и `.data.gz` также нужен заголовок `Content-Encoding: gzip`, чтобы браузер их распаковывал.
Добавьте это в свой `vercel.json`:
```json
{
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }],
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "Cross-Origin-Embedder-Policy", "value": "require-corp" },
{ "key": "Cross-Origin-Opener-Policy", "value": "same-origin" },
{ "key": "Cross-Origin-Resource-Policy", "value": "cross-origin" }
]
},
{
"source": "/libreoffice-wasm/soffice.wasm.gz",
"headers": [
{ "key": "Content-Type", "value": "application/wasm" },
{ "key": "Content-Encoding", "value": "gzip" }
]
},
{
"source": "/libreoffice-wasm/soffice.data.gz",
"headers": [
{ "key": "Content-Type", "value": "application/octet-stream" },
{ "key": "Content-Encoding", "value": "gzip" }
]
}
]
}
```
Для проверки откройте консоль DevTools и выполните `console.log(window.crossOriginIsolated)`: должно вернуться `true`.