MAATRIX / Блог / ERPNext в Docker Compose: готовый файл

ERPNext в Docker Compose: готовый файл

MAATRIX

ERPNext — открытая ERP-система, которую малый и средний бизнес выбирает не за красоту, а за то, что в ней из коробки есть склад, бухгалтерия, CRM, HR и производство без месяца интеграций. Проблема начинается на этапе установки: bench и ручная настройка Frappe-фреймворка требуют точных версий Python, Node и MariaDB, и любое расхождение ломает сборку. Docker Compose снимает этот вопрос — ниже рабочий файл, который поднимает ERPNext со всеми зависимостями одной командой.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Зачем ERPNext в Docker, а не «руками» на сервере

Frappe (фреймворк, на котором построен ERPNext) исторически ставится через bench init — скрипт тянет конкретные версии Python, Node.js, wkhtmltopdf, MariaDB и Redis, и любое расхождение версий ломает сборку. На чистом Ubuntu это пляски с nvm, pyenv и системными пакетами, которые конфликтуют с остальным сервером — особенно если на нём уже крутится что-то на других версиях MariaDB или Redis.

Docker Compose решает это иначе: каждый сервис (бэкенд, фронтенд-nginx, очереди, планировщик, база, кэш) — свой контейнер с фиксированными версиями внутри официального образа frappe/erpnext. Обновление — это смена тега образа, а не пересборка окружения; откат — docker compose down и возврат к прежнему тегу. Для продакшена это надёжнее, чем bench, установленный вручную и обрастающий недокументированными локальными правками.

Есть и оговорка: официальный способ разворачивания — скрипт easy-install.py из репозитория frappe_docker, который сам генерирует compose-файл. Файл ниже — самостоятельный, читаемый вариант для тех, кто хочет понимать происходящее и держать конфиг у себя в git, а не полагаться на генератор.

Архитектура контейнеров ERPNext

КонтейнерРоль
configuratorРазовый init-контейнер: пишет common_site_config.json с адресами БД и Redis
create-siteРазовый init-контейнер: создаёт первый сайт и админ-пользователя
backendGunicorn, обслуживает Frappe/ERPNext API и веб-интерфейс
frontendNginx внутри контейнера — отдаёт статику и проксирует на backend
websocketNode.js socket.io-сервер для real-time уведомлений в интерфейсе
queue-short / queue-longВоркеры фоновых задач (короткие и длинные джобы)
schedulerКрон внутри Frappe — плановые задачи, напоминания, отчёты
dbMariaDB — основное хранилище данных
redis-cacheКэш Frappe (сессии, кэш-документы)
redis-queueОчередь задач для воркеров

Все сервисы, кроме db и redis-*, используют один и тот же образ frappe/erpnext — разница только в команде запуска, так что тянуть при обновлении нужно один образ, а не десяток разных.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Готовый docker-compose.yml

Ниже рабочий файл для одного сайта на одном сервере. Замените erp.example.com, пароли и токен сайта на свои.

# docker-compose.yml
version: "3.8"

x-erpnext-image: &erpnext-image frappe/erpnext:v15

services:
  configurator:
    image: *erpnext-image
    deploy:
      restart_policy:
        condition: none
    entrypoint:
      - bash
      - -c
    command:
      - >
        ls -1 apps > sites/apps.txt;
        bench set-config -g db_host $$DB_HOST;
        bench set-config -gp db_port $$DB_PORT;
        bench set-config -g redis_cache "redis://$$REDIS_CACHE";
        bench set-config -g redis_queue "redis://$$REDIS_QUEUE";
        bench set-config -g redis_socketio "redis://$$REDIS_QUEUE";
        bench set-config -gp socketio_port $$SOCKETIO_PORT;
    environment:
      DB_HOST: db
      DB_PORT: "3306"
      REDIS_CACHE: redis-cache:6379
      REDIS_QUEUE: redis-queue:6379
      SOCKETIO_PORT: "9000"
    volumes:
      - sites:/home/frappe/frappe-bench/sites
    depends_on:
      - db
      - redis-cache
      - redis-queue

  create-site:
    image: *erpnext-image
    deploy:
      restart_policy:
        condition: none
    entrypoint:
      - bash
      - -c
    command:
      - >
        wait-for-it -t 120 db:3306;
        wait-for-it -t 120 redis-cache:6379;
        wait-for-it -t 120 redis-queue:6379;
        [ -f sites/${SITE_NAME}/site_config.json ] || bench new-site
        --mariadb-user-host-login-scope='%'
        --admin-password=${ADMIN_PASSWORD}
        --db-root-username=root
        --db-root-password=${MYSQL_ROOT_PASSWORD}
        --install-app erpnext
        --set-default ${SITE_NAME};
    environment:
      SITE_NAME: erp.example.com
      ADMIN_PASSWORD: change_me_admin_pass
      MYSQL_ROOT_PASSWORD: change_me_root_pass
    volumes:
      - sites:/home/frappe/frappe-bench/sites
    depends_on:
      - configurator

  backend:
    image: *erpnext-image
    restart: unless-stopped
    volumes:
      - sites:/home/frappe/frappe-bench/sites
      - logs:/home/frappe/frappe-bench/logs
    depends_on:
      - create-site

  frontend:
    image: *erpnext-image
    restart: unless-stopped
    command: ["nginx-entrypoint.sh"]
    environment:
      BACKEND: backend:8000
      SOCKETIO: websocket:9000
      FRAPPE_SITE_NAME_HEADER: ${SITE_NAME:-erp.example.com}
      UPSTREAM_REAL_IP_ADDRESS: 127.0.0.1
      UPSTREAM_REAL_IP_HEADER: X-Forwarded-For
      UPSTREAM_REAL_IP_RECURSIVE: "off"
    volumes:
      - sites:/home/frappe/frappe-bench/sites
      - logs:/home/frappe/frappe-bench/logs
    ports:
      - "127.0.0.1:8080:8080"
    depends_on:
      - backend
      - websocket

  websocket:
    image: *erpnext-image
    restart: unless-stopped
    command: ["node", "/home/frappe/frappe-bench/apps/frappe/socketio.js"]
    volumes:
      - sites:/home/frappe/frappe-bench/sites
      - logs:/home/frappe/frappe-bench/logs
    depends_on:
      - create-site

  queue-short:
    image: *erpnext-image
    restart: unless-stopped
    command: ["bench", "worker", "--queue", "short,default"]
    volumes:
      - sites:/home/frappe/frappe-bench/sites
      - logs:/home/frappe/frappe-bench/logs
    depends_on:
      - create-site

  queue-long:
    image: *erpnext-image
    restart: unless-stopped
    command: ["bench", "worker", "--queue", "long,default,short"]
    volumes:
      - sites:/home/frappe/frappe-bench/sites
      - logs:/home/frappe/frappe-bench/logs
    depends_on:
      - create-site

  scheduler:
    image: *erpnext-image
    restart: unless-stopped
    command: ["bench", "schedule"]
    volumes:
      - sites:/home/frappe/frappe-bench/sites
      - logs:/home/frappe/frappe-bench/logs
    depends_on:
      - create-site

  db:
    image: mariadb:10.6
    restart: unless-stopped
    command:
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_unicode_ci
      - --skip-character-set-client-handshake
      - --skip-innodb-read-only-compressed
    environment:
      MYSQL_ROOT_PASSWORD: change_me_root_pass
    volumes:
      - db-data:/var/lib/mysql

  redis-cache:
    image: redis:6.2-alpine
    restart: unless-stopped
    volumes:
      - redis-cache-data:/data

  redis-queue:
    image: redis:6.2-alpine
    restart: unless-stopped
    volumes:
      - redis-queue-data:/data

volumes:
  sites:
  logs:
  db-data:
  redis-cache-data:
  redis-queue-data:

Обратите внимание на x-erpnext-image — YAML-якорь, чтобы версия образа менялась в одном месте, а не в семи. На момент написания актуальна ветка v15; перед запуском проверьте на Docker Hub, какой тег последний стабильный — в проекте выходят минорные релизы регулярно, и точный номер быстро устаревает.

Первый запуск и создание сайта

Порядок важен: init-контейнеры должны отработать до старта воркеров, поэтому не запускайте всё разом через docker compose up -d с первого раза — дайте create-site завершиться.

# создать сеть и volumes, поднять только базу и Redis
docker compose up -d db redis-cache redis-queue

# прогнать конфигуратор и создание сайта
docker compose up configurator
docker compose up create-site

# убедиться, что сайт создан (в логах — "Site erp.example.com already exists" при повторном запуске это нормально)
docker compose logs create-site | tail -30

# поднять всё остальное
docker compose up -d

Первый прогон create-site занимает несколько минут — ставится сама ERPNext поверх Frappe, накатываются миграции схемы. Если процесс падает с ошибкой подключения к базе — почти всегда db ещё не приняла соединения; wait-for-it для этого и добавлен, но на медленном диске полезно поднять таймаут с 120 до 300 секунд.

После запуска ERPNext слушает на 127.0.0.1:8080 внутри frontend — наружу порт нарочно не пробрасывается, это отдаётся внешнему nginx на хосте.

Nginx, HTTPS и домен

Внутренний nginx в контейнере frontend не занимается TLS — это задача внешнего reverse-proxy на хосте. Если у вас уже есть nginx как reverse-proxy для других сервисов на сервере, добавьте ещё один server-блок:

server {
    listen 443 ssl http2;
    server_name erp.example.com;

    ssl_certificate     /etc/letsencrypt/live/erp.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/erp.example.com/privkey.pem;

    client_max_body_size 50m;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

server {
    listen 80;
    server_name erp.example.com;
    return 301 https://$host$request_uri;
}

client_max_body_size стоит поднять заранее — в ERPNext часто загружают вложения (сканы счетов, прайс-листы), и лимит nginx по умолчанию в 1 МБ отсекает их с ошибкой 413. Сертификат получите через certbot — этот шаг не отличается от настройки Let's Encrypt на VPS для любого другого сайта. WebSocket отдельного проксирования не требует — frontend сам маршрутизирует /socket.io/ на сервис websocket, вам достаточно проксировать корень / на порт 8080.

Бэкапы, обновления и продакшен-нюансы

Бэкап. ERPNext умеет бэкапиться штатным механизмом bench прямо из контейнера backend:

docker compose exec backend bench --site erp.example.com backup --with-files

Файлы бэкапа окажутся в volume sites, в подпапке private/backups сайта. Их нужно регулярно копировать наружу — сам volume не защищает от потери сервера. Если сервер уже настроен под docker-compose в продакшене, логично добавить cron-задачу с этой командой и синком результата в S3-совместимое хранилище или на другой сервер.

Обновление. Меняете тег в x-erpnext-image на новый релиз, затем:

docker compose pull
docker compose up -d
docker compose exec backend bench --site erp.example.com migrate

Миграцию нельзя пропускать — новые версии ERPNext часто меняют схему базы, и без migrate интерфейс либо не откроется, либо начнёт падать на отдельных модулях. Перед обновлением обязательно снимите свежий бэкап — откатить контейнер назад просто, а вот откатить уже применённую миграцию схемы бывает не тривиально.

Ресурсы. MariaDB, несколько Python-воркеров и Node.js одновременно ощутимо едят память. Тестовому инстансу с парой пользователей хватит 2 vCPU / 4 ГБ RAM, для боевой эксплуатации с десятком сотрудников комфортнее закладывать от 4 vCPU / 8 ГБ и SSD — на медленном диске MariaDB и так узкое место при отчётах с джойнами по нескольким таблицам.

Изоляция. Если на сервере уже крутятся сервисы со своей MariaDB или Redis, не переиспользуйте их для ERPNext — в контейнерах зафиксированы версии и конфигурация (utf8mb4, конкретные флаги), от которых Frappe зависит при старте. Держите db и redis-* из этого файла отдельно от остального стека.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Можно ли обойтись без внешнего nginx и открыть порт 8080 напрямую?

Технически да, но тогда вы останетесь без TLS и без единой точки для сертификатов и логов. Для чего-то серьёзнее локального теста внешний reverse-proxy с HTTPS обязателен.

Как добавить второй сайт (мультитенантность) в тот же стек?

Frappe поддерживает несколько сайтов на одном bench — запустите create-site ещё раз с новым SITE_NAME, а во frontend настройте отдельные server-блоки nginx с одним и тем же proxy_pass.

Почему после первого docker compose up -d сайт не открывается?

Скорее всего create-site не успел завершиться до того, как поднялись остальные контейнеры. Проверьте docker compose logs create-site — если сайт не создан, backend и frontend работают, но выдают «site not found».

Нужен ли отдельный volume для загруженных файлов?

Нет, они хранятся в том же volume sites, в public/files и private/files сайта — отдельно выносить не нужно, если только вы не переходите на S3-совместимое хранилище через сторонний app.

Что делать, если bench new-site падает с ошибкой доступа к MariaDB?

Чаще всего это несовпадение MYSQL_ROOT_PASSWORD между сервисами db и create-site — переменная должна совпадать в обоих местах compose-файла.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →