ERPNext в Docker Compose: готовый файл
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-контейнер: создаёт первый сайт и админ-пользователя |
backend | Gunicorn, обслуживает Frappe/ERPNext API и веб-интерфейс |
frontend | Nginx внутри контейнера — отдаёт статику и проксирует на backend |
websocket | Node.js socket.io-сервер для real-time уведомлений в интерфейсе |
queue-short / queue-long | Воркеры фоновых задач (короткие и длинные джобы) |
scheduler | Крон внутри Frappe — плановые задачи, напоминания, отчёты |
db | MariaDB — основное хранилище данных |
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 ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →