Бот/ИИ-ассистент was priced at 20 000 ₽ against a real market range of 150k-1.5m ₽ for comparable AI-bot products - raised 8x to 160 000 ₽. Интеграция с сервисами repriced to ~20% below the market rate for CRM/messenger integrations (~115k ₽ market -> 90 000 ₽). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013otXCiTZsxdZ4SJKZ9wUky
TopSysOps - сайт + админ-панель
Продакшн-версия сайта top-sysops.ru: публичная часть с серверным рендерингом на Next.js (Главная, Акции, Услуги, Кейсы, FAQ, Блог с рич-текст редактором, Отзывы, Контакты), личный кабинет для клиентов (переписка с командой, ИИ-чат, профиль) и админ-панель (дашборд посещаемости, модерация отзывов, редактор прайс-листа, редактор акций, редактор кейсов и FAQ, редактор блога, переписка с клиентами, ИИ-ассистент для сотрудников, виджет чата поддержки на сайте). SEO: sitemap.xml, robots.txt и разметка schema.org генерируются самим Next.js.
Из чего состоит
├── docker-compose.yml # четыре сервиса: db, backend, nextjs, web
├── .env.example # шаблон настроек, скопировать в .env
├── backend/ # Node.js/Express API + PostgreSQL
├── public-site/ # Next.js (SSR) - публичный сайт и блог
└── admin-app/ # React (Vite) - админка и личный кабинет + Caddy
- db - PostgreSQL. Хранит услуги/цены, акции, отзывы, статьи блога, статистику визитов, клиентов и учётные записи администраторов.
- backend - REST API. При первом запуске сам накатывает миграции и создаёт администратора и стартовый прайс-лист/акции (те же позиции, что были на старом сайте).
- nextjs - публичный сайт и блог, отдаются с сервера (SSR) - это и есть ответ на "нужен SEO": краулер получает готовый HTML с разметкой schema.org, а не пустую страницу, которую дорисовывает JavaScript. Не имеет отдельного порта наружу - доступен только через Caddy.
- web - Caddy: собирает и раздаёт статику
admin-app(админка и личный кабинет - им SEO не нужен, поэтому остались обычным SPA), проксирует/api/*на backend, всё остальное - наnextjs. Caddy же сам получает и продлевает HTTPS-сертификат Let's Encrypt - вручную ничего настраивать не нужно, только домен в.env.
Почему не переписали админку и кабинет на Next.js тоже: SEO им не нужно
вообще (они закрыты в robots.txt и требуют авторизации), а полный переезд
уже готовой и работающей админки ради нулевой выгоды - это только риск что-то
сломать. SSR применён именно там, где от него есть польза - на страницах,
которые должны индексироваться.
Требования
- Сервер на Ubuntu 24.04 (2 ГБ RAM хватит с запасом).
- Домен, у которого A-запись указывает на IP сервера - нужен для автоматического HTTPS.
Если домена пока нет, можно временно развернуть по IP без HTTPS (см.
.env.example). - Открытые порты 80 и 443.
1. Установка Docker на Ubuntu 24
sudo apt update
sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# чтобы не писать sudo перед каждой командой docker (перелогиньтесь после этого)
sudo usermod -aG docker $USER
Проверка: docker compose version.
2. Загрузка проекта на сервер
Скопируйте всю папку проекта на сервер, например через scp или git:
scp -r topsysops-app your-user@your-server-ip:~/topsysops-app
# или, если проект в git-репозитории:
git clone <ваш-репозиторий> topsysops-app
3. Настройка .env
cd topsysops-app
cp .env.example .env
nano .env
Обязательно замените:
POSTGRES_PASSWORD- пароль базы данных;JWT_SECRET- длинная случайная строка (openssl rand -hex 32);ADMIN_USERNAME/ADMIN_PASSWORD- логин и пароль первого администратора;DOMAIN- ваш домен (напримерtop-sysops.ru) или:80, если тестируете по IP без HTTPS.
Файл .env содержит секреты - он уже добавлен в .gitignore, не публикуйте его.
4. Запуск
docker compose up -d --build
Первый запуск соберёт образы (нужен интернет на сервере), поднимет базу, применит миграции, создаст администратора и стартовый прайс-лист/акции, затем запустит сайт. Проверить статус и логи:
docker compose ps
docker compose logs -f backend
Дождитесь строки [start] backend listening on port 4000, затем откройте
сайт в браузере по вашему домену (или http://IP-сервера, если DOMAIN=:80).
Если используете реальный домен, Caddy автоматически выпустит сертификат при первом обращении по HTTPS - на это может уйти несколько секунд.
Если TLS у вас на Nginx Proxy Manager
Именно этот сценарий - в .env.example он уже настроен по умолчанию:
DOMAIN=http://top-sysops.ru, HTTP_PORT=7778, порт 443 у контейнера
закомментирован в docker-compose.yml. Со стороны NPM:
- Hosts -> Proxy Hosts -> Add Proxy Host.
- Domain Names: ваш домен (
top-sysops.ru). - Scheme:
http, Forward Hostname/IP: IP или hostname сервера, где запущен docker compose (127.0.0.1, если NPM на той же машине; иначе реальный IP), Forward Port:7778(или то, что указали вHTTP_PORT). - Вкладка SSL: выберите/запросите сертификат Let's Encrypt, включите Force SSL - весь HTTPS полностью на стороне NPM, контейнеру об этом вообще думать не нужно.
- Save. Проверьте, что
docker compose psпоказываетwebсо проброшенным портом7778->80- именно на него должен смотреть Forward Port в NPM.
Если после этого видите "too many redirects" - смотрите раздел
"Устранение неполадок" ниже, обычно это означает, что DOMAIN в .env
задан без схемы http://.
5. Первый вход в админку
Откройте https://ваш-домен/admin, войдите под ADMIN_USERNAME / ADMIN_PASSWORD
из .env, затем сразу зайдите в Настройки -> Сменить пароль и задайте
собственный пароль - значение из .env дальше не используется системой, но
лучше не оставлять его действующим.
Из админки доступно:
- Дашборд - визиты (всего/сегодня/график за 7 дней), количество отзывов;
- Отзывы - одобрить / отклонить / удалить;
- Клиенты - список зарегистрированных клиентов и переписка с ними (ответ на сообщения из личного кабинета, метка «ждёт ответа» для непрочитанных);
- Прайс-лист - добавление и редактирование категорий и позиций услуг, скрытие позиции с сайта без удаления (иконка глаза);
- Акции - то же самое для акций;
- Кейсы - карточки портфолио (название, тег, статус с цветным индикатором, описание), с возможностью скрыть без удаления;
- FAQ - вопросы и ответы для сайта;
- Блог - статьи (заголовок, слаг, краткое описание, текст в WYSIWYG-редакторе
с форматированием - заголовки, списки, ссылки, цитаты - публикация) -
попадают на по-настоящему серверно отрендеренную страницу
/blog/<slug>и в sitemap.xml сразу после сохранения; - Контакты - адрес, телефон, email, режим работы, ссылка на карту;
- ИИ-ассистент - внутренний чат для сотрудников на базе OpenRouter (см. ниже);
- Настройки - смена пароля и управление администраторами (добавить нового сотрудника с логином/паролем, удалить лишнего - кроме себя и последнего оставшегося администратора).
Изменения в прайсе, акциях, кейсах и FAQ появляются на сайте в течение минуты:
Next.js кэширует данные с публичного сайта на 60 секунд (revalidate: 60), это
плата за серверный рендеринг - страницы отдаются мгновенно, а не после похода в
базу на каждый заход. В самой админке изменения видны сразу же, без задержки.
Обновление прайс-листа на уже развёрнутом сайте
Стартовый прайс-лист (в seed.js) применяется только к пустой базе - то есть
только при самом первом запуске. Для уже развёрнутого сайта обновление новой
линейки услуг оформлено как обычные миграции
(backend/migrations/006_update_services_2026_07.sql и
007_landing_tier.sql) - они применяются автоматически при следующем
запуске бэкенда, ничего вручную запускать не нужно:
docker compose up -d --build
Система миграций (backend/src/migrate.js) гарантирует, что каждый файл
выполнится ровно один раз - если уже разворачивали сайт раньше и обновляете
код сейчас, эти миграции просто окажутся среди тех, что применятся при
следующем перезапуске backend, наравне с остальными.
Что делают эти миграции: убирают старую категорию "Разработка сайтов" (от 5 000 ₽) и добавляют:
- Разработка под ключ - Лендинг (от 20 000 ₽, простая посадочная без бота/админки), MVP «Быстрый старт» (от 120 000 ₽, с формой-ботом и простой админкой), MVP Pro (от 450 000 ₽, с личным кабинетом и ИИ-ассистентом - как этот сайт), продакшн-разработка (от 1 200 000 ₽, индивидуально), Discovery-воркшоп (от 30 000 ₽), сопровождение проекта (от 40 000 ₽/мес);
- White-label и партнёрство - white-label «Кабинет + ИИ-ассистент» (от 500 000 ₽), IT-мониторинг сайта (от 10 000 ₽/мес), реферальная и партнёрская программы.
Цифры сверены с рынком Санкт-Петербурга (лендинги студийного уровня - 19 000-65 000 ₽, MVP с личным кабинетом у студий - от 260 000 ₽ и выше).
Дальше все эти позиции редактируются как обычно - через админку («Прайс-лист»).
Если после docker compose up -d --build старая категория всё равно видна -
проверьте docker compose logs backend | grep migrate: там должны быть строки
applying 006_update_services_2026_07.sql и applying 007_landing_tier.sql.
Если их нет - значит контейнер backend не пересобрался/не перезапустился с
новым кодом (проверьте docker compose ps, при необходимости
docker compose up -d --build backend отдельно).
SEO
- robots.txt и sitemap.xml - генерируются самим Next.js (
public-site/app/robots.jsиapp/sitemap.js), закрывают/adminи/accountот индексации, доступны по адресамhttps://ваш-домен/robots.txtи/sitemap.xml. Sitemap включает главную,/blogи все опубликованные статьи - обновляется автоматически. - schema.org (JSON-LD) - рендерится прямо на сервере (это заслуга SSR: разметка
оказывается в HTML, который получает краулер, а не дорисовывается JS уже в
браузере). На сайте автоматически публикуется
LocalBusiness(адрес, телефон, email из раздела «Контакты», рейтинг из одобренных отзывов),FAQPage(из раздела FAQ) и каталог услуг (hasOfferCatalog, из прайс-листа) - помогает Google/Яндексу показывать расширенные сниппеты (рейтинг со звёздами, раскрывающиеся вопросы в выдаче). На страницах статей блога - разметкаArticle. - Блог с рич-текст редактором - основной канал органического трафика для
B2B IT-услуг. Пишите статьи через админку («Блог», обычный WYSIWYG-редактор:
заголовки, списки, ссылки, цитаты) - каждая становится отдельной,
по-настоящему серверно отрендеренной страницей на
/blog/<slug>и сразу появляется в sitemap.xml. - Серверный рендеринг (SSR) - публичный сайт и блог теперь на Next.js:
краулер и превью в соцсетях получают готовый HTML сразу, без ожидания
JavaScript. Админка и личный кабинет остались SPA на Vite - им это не нужно,
они и так закрыты в
robots.txtи требуют входа. - Что дальше сделать руками (не автоматизируется из кода): зарегистрировать сайт в Яндекс.Вебмастере и Google Search Console (там же отправить sitemap.xml вручную первый раз), завести профиль в Яндекс.Картах/Google Business с тем же адресом и телефоном, что в разделе «Контакты» (важно для локального SEO по запросам вида «IT-аутсорсинг Санкт-Петербург»).
Чат поддержки на сайте (TopTicket / Chatwoot)
Виджет чата для посетителей сайта настраивается в .env и встраивается на
этапе сборки Next.js-приложения (значения переменных NEXT_PUBLIC_* попадают
в клиентский бандл, поэтому смена провайдера требует пересборки контейнера
nextjs, а не web).
По умолчанию уже включён TopTicket (ваша собственная разработка, help.top-sysops.ru)
с реальными данными. Выберите вариант в .env:
# TopTicket (по умолчанию) - embed-сниппет вашей разработки:
# <script src="https://help.top-sysops.ru/widget.js" data-key="..." async>
CHAT_PROVIDER=topticket
TOPTICKET_WIDGET_URL=https://help.top-sysops.ru/widget.js
TOPTICKET_WIDGET_KEY=EBpDS4JqT7a-_MzZ
# либо Chatwoot
CHAT_PROVIDER=chatwoot
CHATWOOT_BASE_URL=https://chat.lamlaba.ru
CHATWOOT_WEBSITE_TOKEN=ваш-website-token # Inboxes -> ваш инбокс -> Configuration
# либо совсем без чата
CHAT_PROVIDER=none
После изменения .env:
docker compose up -d --build nextjs
Виджет подключается только на публичных страницах сайта, в админке не показывается.
ИИ-ассистент в админке (OpenRouter)
Это отдельный, второй чат - не для посетителей сайта, а внутренний инструмент для сотрудников: помогает набросать ответ на отзыв, посчитать что-то по прайсу и т.д. Доступен только тем, кто вошёл в админку.
Работает через OpenRouter - агрегатор моделей разных провайдеров под одним API. Ключ используется только на бэкенде и никогда не попадает в браузер:
OPENROUTER_API_KEY=sk-or-... # https://openrouter.ai/keys
OPENROUTER_MODEL=openai/gpt-4o-mini # любая модель, доступная в вашем аккаунте
После заполнения:
docker compose up -d backend
Если OPENROUTER_API_KEY не задан, раздел «ИИ-ассистент» в админке просто
покажет предупреждение и не будет пытаться обращаться к API - остальной сайт
при этом продолжает работать как обычно.
Контакты
Адрес, телефон, email, режим работы и ссылка на карту редактируются в админке
(раздел «Контакты», /admin/contact) — как и прайс-лист, акции, кейсы и FAQ,
без пересборки. Пустое поле просто не показывается на сайте (например, если
не хотите публиковать телефон).
Личный кабинет клиентов
Отдельная система входа для посетителей сайта - не путать с админкой. Регистрация
и вход доступны по адресам /account/register и /account, на главной странице
есть заметная плашка-приглашение с этими же ссылками.
После входа клиенту доступны:
- Обзор - приветствие и текущие акции;
- Написать нам - личная переписка с командой TopSysOps. Сообщения клиента видны в админке (раздел «Клиенты»), сотрудник отвечает прямо оттуда, ответ сразу появляется в кабинете клиента;
- ИИ-ассистент - второй, отдельный чат, отвечает на вопросы об услугах и ценах через тот же OpenRouter, что и ассистент в админке (см. выше), но с собственным системным промптом и более строгим лимитом запросов;
- Профиль - изменение имени/компании/телефона, смена пароля.
Регистрация клиентов и вход в админку - две независимые системы: у них разные
таблицы в базе (customers и admin_users) и разные сессионные cookie
(ts_customer_session и ts_admin_session), так что сотрудник может быть
одновременно залогинен и в кабинет, и в админку в одном браузере без конфликтов.
Дополнительная защита от спама/перебора: на регистрацию, вход, отправку
сообщений и обращения к ИИ-ассистенту в кабинете клиента тоже действует
express-rate-limit, как и в остальном API.
Резервное копирование базы данных
docker compose exec db pg_dump -U topsysops topsysops > backup-$(date +%F).sql
Восстановление из бэкапа:
cat backup-2026-07-25.sql | docker compose exec -T db psql -U topsysops topsysops
Рекомендуется добавить это в cron, например раз в сутки.
Обновление после изменений в коде
git pull # если используете git
docker compose up -d --build
Данные в PostgreSQL хранятся в именованном томе db_data и не теряются при
пересборке контейнеров.
Устранение неполадок
docker compose logs -f backend- если сайт не открывается, чаще всего проблема видна тут (например, не заполненJWT_SECRET).docker compose logs -f nextjs- если не открывается главная страница или блог (например, не может достучаться до бэкенда - тогда секции будут просто пустыми, а в логе будет[serverApi] failed to fetch ...).docker compose logs -f web- если не выдаётся HTTPS-сертификат, здесь будут сообщения Caddy (обычно это неверный DNS: домен ещё не указывает на сервер).- Если меняли
DOMAINв.env, перезапуститеweb:docker compose up -d web. - "Too many redirects" / бесконечный редирект - значит перед контейнером
стоит внешний реверс-прокси со своим TLS-сертификатом (например, Nginx Proxy
Manager - см. раздел выше), а Caddy внутри контейнера всё равно пытается сам
сделать редирект http->https. Исправляется
так: в
.envпоставьтеDOMAIN=http://ваш-домен(со схемойhttp://) и перезапуститеweb(docker compose up -d web) - это отключит автоматический HTTPS/редирект в Caddy, он будет просто отдавать http, а шифрование останется на внешнем nginx. Также проверьте, что внешний nginx передаёт заголовкиHost,X-Forwarded-ProtoиX-Real-IP/X-Forwarded-For. - Полный сброс базы (например, во время тестов):
docker compose down -vудалит и данные - используйте только осознанно.
Безопасность на будущее
- Не открывайте порт 5432 (PostgreSQL) наружу - в текущей конфигурации он и так не проброшен на хост, доступен только другим контейнерам.
- Ограничьте доступ по SSH и настройте
ufw(ufw allow 80,443,OpenSSH, затемufw enable). Если используете сценарий с внешним nginx и внутренним портом вроде 7778 (HTTP_PORT=7778), этот порт наружу открывать не нужно - трафик между nginx и контейнером идёт локально, через него наружу открыты только 80/443 самого nginx. express-rate-limitуже ограничивает количество попыток входа в админку и количество отзывов с одного IP - это защита от перебора паролей и спама, но не полноценный WAF; при росте нагрузки рассмотрите Cloudflare или аналог.