Генерация PDF из DOCX в Telegram-боте на Bothost: LibreOffice headless

На платформе Bothost боты работают в Docker-контейнерах — это удобно для деплоя Telegram-, Discord- и MAX-ботов из Git, но стандартный образ не включает системные пакеты вроде LibreOffice. Если вашему боту нужно конвертировать файлы .docx в .pdf (договоры, справки, отчёты, счета по шаблону), установите LibreOffice headless через кастомный Dockerfile. LibreOffice headless — это полноценный офисный пакет в фоновом режиме без графического интерфейса. Такой подход подходит для Python-ботов на aiogram, Node.js-ботов и других проектов, развёрнутых на хостинге ботов Bothost: пользователь отправляет .docx в чат, бот возвращает готовый PDF.

Способ 1: Кастомный Dockerfile (рекомендуется)

Добавьте в корень репозитория файл Dockerfile и поставьте галочку «Использовать собственный Dockerfile» при создании бота. Галочка «Использовать собственный Dockerfile» в форме создания бота

Dockerfile для Python-бота

FROM python:3.11-slim

WORKDIR /app

# Устанавливаем LibreOffice headless + шрифты
RUN apt-get update && apt-get install -y --no-install-recommends \
    libreoffice-core \
    libreoffice-writer \
    fonts-dejavu \
    fonts-liberation \
    && rm -rf /var/lib/apt/lists/*

# LibreOffice требует домашнюю директорию
ENV HOME=/root

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["python", "main.py"]
Важно: fonts-dejavu и fonts-liberation — обязательны. Без них кириллица и стандартные шрифты (Arial, Times New Roman) в PDF будут отображаться некорректно.

Конвертация DOCX → PDF в коде

После добавления LibreOffice в образ используйте его через subprocess:
import subprocess
import os

def docx_to_pdf(docx_path: str, out_dir: str) -> str:
    """Конвертирует .docx в .pdf через LibreOffice headless."""
    subprocess.run(
        [
            "libreoffice",
            "--headless",
            "--norestore",
            "--convert-to", "pdf",
            "--outdir", out_dir,
            docx_path,
        ],
        check=True,
        timeout=60,
    )
    basename = os.path.splitext(os.path.basename(docx_path))[0]
    return os.path.join(out_dir, basename + ".pdf")

Пример использования в aiogram 3

import os
import tempfile
from aiogram import Bot, Dispatcher, F
from aiogram.types import Message, FSInputFile

bot = Bot(token=os.getenv("BOT_TOKEN"))
dp = Dispatcher()

@dp.message(F.document)
async def handle_document(message: Message):
    doc = message.document
    if not doc.file_name.endswith(".docx"):
        await message.answer("Пришлите файл .docx")
        return

    with tempfile.TemporaryDirectory() as tmpdir:
        # Скачиваем файл
        docx_path = os.path.join(tmpdir, doc.file_name)
        await bot.download(doc, destination=docx_path)

        # Конвертируем
        pdf_path = docx_to_pdf(docx_path, tmpdir)

        # Отправляем обратно
        await message.answer_document(
            FSInputFile(pdf_path),
            caption="Готово! Вот ваш PDF."
        )

Способ 2: Node.js-бот

Для Node.js-проектов Dockerfile аналогичный:
FROM node:20-slim

WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends \
    libreoffice-core \
    libreoffice-writer \
    fonts-dejavu \
    fonts-liberation \
    && rm -rf /var/lib/apt/lists/*

ENV HOME=/root

COPY package*.json ./
RUN npm ci --omit=dev

COPY . .

CMD ["node", "index.js"]
Конвертация через child_process:
const { execFile } = require('child_process');
const path = require('path');

function docxToPdf(docxPath, outDir) {
    return new Promise((resolve, reject) => {
        execFile(
            'libreoffice',
            ['--headless', '--norestore', '--convert-to', 'pdf', '--outdir', outDir, docxPath],
            { timeout: 60000 },
            (err) => {
                if (err) return reject(err);
                const base = path.basename(docxPath, '.docx');
                resolve(path.join(outDir, base + '.pdf'));
            }
        );
    });
}

Замечания

ТемаПодробности
Размер образаLibreOffice добавляет ~400–500 МБ. Первая сборка займёт 3–5 минут, повторные — быстрее за счёт кеша
КириллицаБез пакетов fonts-dejavu / fonts-liberation кириллические символы могут отображаться как квадраты
Параллельные запросыLibreOffice запускается как отдельный процесс при каждой конвертации. Для высокой нагрузки рассмотрите очередь задач
Поддерживаемые форматыПомимо .docx, LibreOffice конвертирует .odt, .doc, .xlsx, .pptx и другие офисные форматы
---

Типичные ошибки

LibreOffice не запускается / ошибка профиля Убедитесь, что в Dockerfile есть ENV HOME=/root. LibreOffice создаёт временный профиль в домашней директории. Шрифты отображаются некорректно Установите fonts-dejavu и fonts-liberation. Для специфических шрифтов скопируйте .ttf-файлы в /usr/local/share/fonts/ и выполните fc-cache -f. Процесс завис Добавьте timeout=60 в subprocess.run(). LibreOffice иногда зависает на повреждённых файлах.

Когда это нужно боту

Типичные сценарии на Bothost: бот принимает Word-шаблон и отдаёт PDF клиенту; автоматическая выдача договоров и актов в Telegram; конвертация загруженных документов без отдельного сервера. Всё выполняется внутри контейнера вашего бота — достаточно добавить Dockerfile в репозиторий и включить опцию «Использовать собственный Dockerfile» при создании бота.

См. также