Expasys BI
Руководство пользователя (версия 2026.1)
×

Pro

 
Установка с использованием docker на Debian 12.
Полная документация к скрипту pro.sh  — Установка и настройка Questionnaire Studio Pro (QSPRO)
 
Общее описание системы
Данный Скрипт — это автоматизированный установщик для комплексного решения Questionnaire Studio Pro (QSPRO), в Debian-подобных системах, предназначенного для создания, запуска и анализа опросов и анкет.
 
Система включает:
Веб-приложение QSPRO (на .NET) — основной фронтенд и бэкенд
PostgreSQL — основная СУБД
PgBouncer — пуллер соединений к PostgreSQL
Caddy — обратный прокси-сервер
JupyterHub — среда для аналитики и Python/R/SQL
Cronicle — планировщик задач (аналог cron)
Noсodb — Low-code платформа для визуального редактирования и загрузки данных в postgres
Memcached — общее  key-value хранилище
Все компоненты запускаются через Docker и Docker Compose, что обеспечивает изоляцию, масштабируемость и переносимость.
 
 
Обновите систему и Установите базовые утилиты:
apt update && sudo apt upgrade -y
apt install -y curl wget gnupg2 software-properties-common apt-transport-https
 
Установите:
docker и docker-compose
apt install jq && apt install docker.io && apt install docker-compose
 
Минимальные требования к inotify:
fs.inotify.max_user_instances
fs.inotify.max_user_watches
fs.inotify.max_queued_events
 
вот так
nano /etc/sysctl.d/99-inotify.conf
 
вставить туда
fs.inotify.max_user_instances = 524288
fs.inotify.max_user_watches = 1048576
fs.inotify.max_queued_events = 163840
 
сохраняем настройку
sysctl --system
 
перезагрузка
reboot
 
проверка
cat /proc/sys/fs/inotify/max_user_instances
cat /proc/sys/fs/inotify/max_user_watches
cat /proc/sys/fs/inotify/max_queued_events
 
locales:
настройки locales должны соответствовать  русской локали ru_RU.UTF-8
nano /etc/default/locale
вставить туда
LANG=ru_RU.UTF-8
LC_ALL=ru_RU.UTF-8
перезагрузить
reboot
 
Создайте рабочую директорию:
Это может быть как просто /opt так и любая другая специально выделенная директория , в которой скрипт будет создавать все необходимые файлы
 
Открытие портов (Опционально):
Если используется фаервол (ufw), разрешите порты:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 8080/tcp
sudo ufw allow 81/tcp
sudo ufw allow 82/tcp
 
Запуск скрипта (пошаговая инструкция)
chmod +x pro.sh
./pro.sh
 
Скрипт должен запускаться от пользователя с sudo-доступом, так как он устанавливает пакеты и изменяет системные настройки.
 
Режимы установки: 1 и 2
После запуска скрипт предложит выбрать режим:
Режим 1 - использовать существующие volumes (по умолчанию)
Режим 2 - удалить всё и поставить заново
 
Введите 1 или 2 [1]:
*при вводе пустого значения - будет считаться 1 режим
 
Режим 1: Использовать существующие данные (предпочтительно)
Сохраняются:
Базы данных
Конфигурации
Пользовательские файлы
Подходит для обновления или перезапуска
Скрипт прочитает .env, appsettings.json, пароли и т.д.
 
Режим 2: Полная переустановка
Удаляются:
Все Контейнеры
Все образы
Все волюмы
Папка questionnaire-studio/
Пользователь etl
Создаются:
Новые пароли
Новые SSH-ключи
Новые конфигурационные файлы
Подходит для чистого развертывания или тестирования
В режиме 2 все данные будут утеряны. Рекомендуется делать бэкап (описаны ниже).
 
Этапы работы скрипта
 
Этап 1: Проверка и установка зависимостей
Устанавливает curl, openssh-client, locales, jq (если отсутствуют)
Устанавливает локаль ru_RU.UTF-8 (важно для корректного отображения русского языка)
Если локаль не применяется, потребуется перезагрузка сервера.
 
Этап 2: Проверка системных требований
Проверяет наличие docker-compose
Проверяет доступ к docker.expasys.ru
 
Этап 3: Выбор версии QSPRO
Скрипт:
Запрашивает список тегов с https://docker.expasys.ru/v2/questionnaire-studio-pro/tags/list
Фильтрует теги по формату ГГГГ.ММ.ДД.НННН
Выводит список доступных версий
Предлагает выбрать:
Номер версии
L — для latest
Ввести вручную (например, 2024.10.01.1234)
 
Этап 4: Выбор локали и часового пояса
Язык: фиксированно ru_RU.UTF-8
Часовой пояс: выбор из списка (по умолчанию Europe/Moscow)
 
Этап 5: Генерация паролей
Автоматически генерируются пароли:
POSTGRES_PASSWORD — для пользователя postgres
QSPRO_PASSWORD — для пользователя qspro (приложение)
DWH_PASSWORD — для пользователя dwh (аналитика)
ETL_PASSWORD — для пользователя etl (JupyterHub)
 Пароли генерируются как UUID без дефисов.
 
Этап 6: Генерация SSH-ключей
Ключи создаются в questionnaire-studio/expasys_keys/
Имя: expasys_ssh_rsa (приватный) и expasys_ssh_rsa.pub (публичный)
Используются для SSH-доступа от Cronicle к JupyterHub
 
Этап 7: Создание конфигурационных файлов
Скрипт генерирует:
.env — переменные окружения
docker-compose.yml — оркестрация сервисов
appsettings.json — настройки .NET-приложения
pgbouncer.ini, userlist.txt, postgresql.conf — настройки БД
Caddyfile — прокси-конфигурация
jupyterhub_config.py — конфигурация JupyterHub
requirements.txt — Python-пакеты для аналитики
 
Этап 8: Настройка inotify
Скрипт проверяет и устанавливает параметры ядра Linux:
fs.inotify.max_user_instances=524288
fs.inotify.max_user_watches=1048576
fs.inotify.max_queued_events=163840
 
Этап 9: Запуск контейнеров
Происходит поэтапно:
Скачивание образов (docker-compose pull)
Запуск db, pgbouncer, app-master — для миграций
Создание пользователей и БД
Настройка pg_hba.conf (scram-sha-256)
Запуск остальных сервисов: caddy, jupyterhub, cronicle, nocodb, воркеры
 
Этап 10: Настройка SSH-интеграции
Устанавливается openssh-server в jupyterhub
Настройка sshd_config
Копирование ключей
Тест SSH-подключения от cronicle к jupyterhub
Создание тестовой задачи в cronicle
 
Структура проекта после установки
 
./questionnaire-studio/
├── .env
├── appsettings.json
├── Caddyfile
├── cronicle_task.json (временный файл)
├── DirectoryPath/ (volume)
├── docker-compose.yml
├── docker.logs
├── Dockerfile.jupyterhub
├── Dockerfile.pgbouncer
├── Dockerfile.postgres
├── expasys_keys/
│   ├── expasys_ssh_rsa
│   └── expasys_ssh_rsa.pub
├── expasysbi_install.log
├── jupyterhub_config.py
├── pgbouncer.ini
├── postgresql.conf
├── requirements.txt
├── test_script.py (временный файл)
├── userlist.txt
├── version.app.txt
├── views/ (volume)
└── wwwroot/ (volume)
 
Доступ к сервисам
 
QSPRO (мастер)       8080    http://localhost:8080
Caddy (прокси)        80        http://localhost
JupyterHub               81        http://localhost:81
Cronicle                    82         http://localhost:82
NoCode                    6433     http://localhost:6433
 
 Caddy перенаправляет / → мастер-нода, /bi → мастер и т.д.
 
Логи и диагностика:
Скрипт создает:
docker.logs — логи всех контейнеров
expasysbi_install.log — конфигурационные файлы и настройки
 
Команды для проверки:
docker-compose ps                # статус контейнеров
docker-compose logs app-master    # логи мастера
docker-compose logs pgbouncer     # логи пула
 
 
Дополнительные возможности: SSH, Cronicle, JupyterHub
 
JupyterHub
Пользователь: etl
Пароль: смотри в файле /questionnaire-studio/.env в переменной ETL_PASSWORD
Доступ: http://localhost:81
Поддержка: Python, R, .NET, SQL (через jupysql)
 
Cronicle
Планировщик задач
Пользователь: admin
Пароль: admin
Может запускать Python-скрипты на JupyterHub через SSH
Интерфейс: http://localhost:82
 
SSH-интеграция
cronicle → jupyterhub через SSH-ключ
Позволяет автоматизировать ETL-процессы
 
узнать ip JupyterHub
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' jupyterhub
 
Секреты и безопасность
Где хранятся пароли:
.env — содержит все пароли
userlist.txt — пароли для pgbouncer
expasysbi_install.log — содержит пароли в открытом виде!
Не передавайте expasysbi_install.log по небезопасным каналам!
 
Рекомендации:
Удалите expasysbi_install.log после установки
Ограничьте доступ к папке questionnaire-studio/
Используйте .env в .gitignore, если в Git
 
 
Бэкапы
Создаем бэкап:
 
заходим в контейнер:
docker exec -it db bash 
 
создаем бэкап:
pg_dump -U qspro -Fc expasys > expasys_docker_backup.dump 
* он создастся  в текущей директории докера
 
выходим из контейнера:
exit
 
копируем файл бэкапа из докера на хост-машину в "укромное место" (в моем случае это выглядит так):
docker cp db:/expasys_docker_backup.dump /opt/questionnaire-studio/expasys_docker_backup.dump
 
Восстановление базы данных из бэкапов:
 
копируем из хостовой машины в контейнер db:
docker cp /opt/questionnaire-studio/expasys_docker_backup.dump db:/expasys_docker_backup.dump
 
 
заходим в контейнер:
docker exec -it db bash 
 
восстанавливаем бд expasys из бэкап файла
pg_restore -U qspro -d expasys -v /expasys_docker_backup.dump
 
заходим и проверяем:
psql -U qspro -d expasys
\c expasys
\dt
 
Вольюмы
Все Волюмы находяться по стандартному пути пути:
 
/var/lib/docker/volumes/
 
у меня выглядит так:
brw-------  1 root root  8, 1 авг 21 10:36 backingFsBlockDev
-rw-------  1 root root 65536 авг 21 11:40 metadata.db
drwx-----x  3 root root  4096 авг 21 11:40 questionnaire-studio_caddy_data
drwx-----x  3 root root  4096 авг 21 11:39 questionnaire-studio_data
drwx-----x  3 root root  4096 авг 21 11:40 questionnaire-studio_jupyterhub_config
drwx-----x  3 root root  4096 авг 21 11:40 questionnaire-studio_jupyterhub_data
drwx-----x  3 root root  4096 авг 21 11:40 questionnaire-studio_nocodb_data
drwx-----x  3 root root  4096 авг 21 11:39 questionnaire-studio_views
drwx-----x  3 root root  4096 авг 21 11:39 questionnaire-studio_wwwroot
 
 
Типичные ошибки и решения:
 
docker-compose not found       Не установлен                    Установите docker-compose
curl: command not found          Не установлен                     sudo apt install curl
pg_isready timeout                    Проблемы с PostgreSQL     Проверьте docker logs db
SSH connection failed                Ключи не совпадают          Перезапустите в режиме 2
inotify limits too low                  Системные ограничения     Увеличьте в /etc/sysctl.conf
appsettings.json not found        Файл удален                         Скопируйте из контейнера или режим 2
user etl not found                      Пользователь удален           Режим 2 или вручную создать
 
Caddyfile
 
Caddyfile находится в /questionnaire-studio/
классический выглядит так
 
 
http://redosdocker.bi.expasys.online {
        handle /bi {
                reverse_proxy http://app-master:5000
        }
        handle /Design/* {
                reverse_proxy http://app-master:5000
        }
        handle /View/* {
                reverse_proxy http://app-master:5000
        }
        handle /admin/bi {
                reverse_proxy http://app-master:5000
        }
        handle /admin/event-journal {
                reverse_proxy http://app-master:5000
        }
        handle /* {
                reverse_proxy http://app-master:5000 http://app-worker1:5000 http://app-worker2:5000 http://app-worker3:5000 http://app-worker4:5000 http://app-worker5:5000 http://app-worker6:5000 http://app-worker7:5000 http://app-worker8:5000 http://app-worker9:5000 http://app-worker10:5000 {
                        lb_policy cookie custom-caddy-cookie b17f21741fca637d2f4ee4c441abc72639067a09d3320a7045055da13724292c688ebc83d3da38da6b6c9900e3bfcb739a470f9bff49e87eee13078a18452ba4
                        health_uri /about
                        health_interval 3s
                        health_timeout 5s
                        health_status 200
                }
        }
}
 
http://redosdocker.dwh.expasys.online {
        reverse_proxy http://nocodb:8080
}
 
http://redosdocker.etl.expasys.online {
        redir / /studio
        reverse_proxy http://jupyterhub:8000 {
                header_up Host {http.request.host}
                header_up X-Real-IP {http.request.remote.host}
        }
}
 
http://redosdocker.cron.expasys.online {
        reverse_proxy http://cronicle:3012
}
 
 
https://redosdocker.bi.expasys.online {
#        tls /etc/caddy/certs/fullchain.pem /etc/caddy/certs/privkey.pem
 
        handle /bi {
                reverse_proxy http://app-master:5000
        }
        handle /Design/* {
                reverse_proxy http://app-master:5000
        }
        handle /View/* {
                reverse_proxy http://app-master:5000
        }
        handle /admin/bi {
                reverse_proxy http://app-master:5000
        }
        handle /admin/event-journal {
                reverse_proxy http://app-master:5000
        }
        handle /* {
                reverse_proxy http://app-master:5000 http://app-worker1:5000 http://app-worker2:5000 http://app-worker3:5000 http://app-worker4:5000 http://app-worker5:5000 http://app-worker6:5000 http://app-worker7:5000 http://app-worker8:5000 http://app-worker9:5000 http://app-worker10:5000 {
                        lb_policy cookie custom-caddy-cookie b17f21741fca637d2f4ee4c441abc72639067a09d3320a7045055da13724292c688ebc83d3da38da6b6c9900e3bfcb739a470f9bff49e87eee13078a18452ba4
                        health_uri /about
                        health_interval 3s
                        health_timeout 5s
                        health_status 200
                }
        }
}
 
https://redosdocker.dwh.expasys.online {
        reverse_proxy http://nocodb:8080
}
 
https://redosdocker.etl.expasys.online {
        redir / /studio
        reverse_proxy http://jupyterhub:8000 {
                header_up Host {http.request.host}
                header_up X-Real-IP {http.request.remote.host}
        }
}
 
https://redosdocker.cron.expasys.online {
        reverse_proxy http://cronicle:3012
}
 
учтите что при готовых сертах (вдруг их принесли на флешке ) разкоментите строку, и положите их в нужный файл
 
#  tls /etc/caddy/certs/fullchain.pem /etc/caddy/certs/privkey.pem
 
и DockerFile.yaml
- /opt/questionnaire-studio/certs/:/etc/caddy/selfsigned.crt:ro
- /opt/questionnaire-studio/certs/:/etc/caddy/selfsigned.key:ro
 
кладите  серты  сюда:
/questionnaire-studio/certs/
/questionnaire-studio/certs/
 
или сюда в зависимости от системы (иногда кадди не понимает что это папки а не файлы поэтому первый вариант предпочтительней)
/questionnaire-studio/certs/selfsigned.key/
/questionnaire-studio/certs/selfsigned.crt/
 
Если не хотите тратить сертификаты , и запускать все на локалке
{
    auto_https off
}
:80 {
    handle /* {
        reverse_proxy http://app-master:5000 http://app-worker1:5000 http://app-worker2:5000 http://app-worker3:5000 http://app-worker4:5000 http://app-worker5:5000 {
            lb_policy cookie custom-caddy-cookie b17f21741fca637d2f4ee4c441abc72639067a09d3320a7045055da13724292c688ebc83d3da38da6b6c9900e3bfcb739a470f9bff49e87eee13078a18452ba4 {
                fallback round_robin
            }
            health_uri /about
            health_interval 3s
            health_timeout 5s
            health_status 200
        }
    }
    handle /bi {
        reverse_proxy http://app-master:5000
    }
    handle /Design/* {
        reverse_proxy http://app-master:5000
    }
    handle /View/* {
        reverse_proxy http://app-master:5000
    }
    handle /admin/bi {
        reverse_proxy http://app-master:5000
    }
    handle /admin/event-journal {
        reverse_proxy http://app-master:5000
    }
}
:6433 {
    reverse_proxy http://nocodb:8080
}
:81 {
    redir / /studio
    reverse_proxy http://jupyterhub:8000 {
        header_up Host {http.request.host}
        header_up X-Real-IP {http.request.remote.host}
    }
}
:82 {
    reverse_proxy http://cronicle:3012
}
 
в данном варианте redosdocker , в кадди меняется на :