Сервер в Москве, копия и восстановление

Кабинет заявок работает у тебя на компьютере, тесты зелёные. Теперь его надо поселить туда, где лежат данные клиентов «Кодиума», то есть на сервер в России. В прогоне курса агент на этом пути трижды сдал работу с тихой дырой, которую не поймал ни один тест. Урок про то, как поймать такое своими глазами.

Чему научишься в этом уроке:
• выложить кабинет на сервер в Москве с HTTPS, лимитом входа и службой;
• настроить ночную копию и доказать, что из неё восстанавливаются;
• создать ключ приёма и сверить его на двух серверах, не показывая;
• проверить сервер после агента: .env, код, перезапуск.

Почему второй сервер и сколько он стоит

Почему Москва, ты помнишь из урока 5.1: имена и телефоны граждан России храним в базах в России. Бот остаётся в Амстердаме, кабинету Telegram не нужен. Бот и форма принимают заявку и сразу передают её в кабинет, у себя не хранят.

Сервер в прогоне: Timeweb, MSK-1, 1 ядро, 1 ГБ памяти (свободно 894 МБ), 15 ГБ диска, по панели на 10.10.2026 это 1 720 ₽ в месяц или 2,35 ₽ в час. Цены меняются, смотри свою перед заказом.

Перед заказом: свой ssh-ключ добавь в панели при создании сервера, как в урок 2.5. Поддомен направь A-записью на адрес нового сервера, как в урок 3.3; certbot запускай, когда адрес уже открывается. Учебный сервер после сдачи удали в панели, иначе он продолжит списывать деньги. Для реального заказчика сервер оформляется на него (урок 5.6).

Схема. Слева «Амстердам»: бот «Кодиум» и приём формы; стрелка вниз к Telegram владельца с плашкой «Новая заявка №N, курс, источник + ссылка, без имени и телефона». Стрелка «заявка по HTTPS + ключ X-Ingest-Key» ведёт в «Москву»: nginx (HTTPS, лимит /login), служба crm-kodium, база SQLite, рядом «03:00 МСК, sqlite3 .backup, 14 копий» и стрелка к облаку «Яндекс Диск, disk:/Кодиум/crm-backup/». Внизу рамка «Проверка восстановления»: тестовая заявка, копия, заявку удалили, служба остановлена и база из копии, заявка вернулась.

Выкладка: задание агенту

Порядок знаком по урок 3.3: сначала разрешить SSH в ufw, потом остальное. Новое: лимит частоты на /login в nginx; случайные пароли пользователей в файле, который читает только root (права 600), ты их агенту не показываешь; резервная копия с проверкой восстановления. Задание из прогона (адреса заменены):

Выложи «Кабинет заявок» на сервер в Москве: ssh root@<адрес сервера в Москве> (ключ настроен, Ubuntu 24.04, 1 ядро, ~900 МБ памяти). Домен <твой поддомен> уже смотрит на этот сервер. Делай по шагам и проверяй каждый:
1. ufw: СНАЧАЛА разреши OpenSSH, потом 80 и 443, потом включи. nginx + certbot (--register-unsafely-without-email) для <твой поддомен>, listen и для IPv6 тоже. На уровне nginx — ограничение частоты запросов на /login.
2. Пользователь crm, папка /opt/crm-kodium, .venv, .env с правами 600 (папки pip/Microsoft, .venv, базу и кэши НЕ копировать). Служба systemd crm-kodium (uvicorn на 127.0.0.1:8000, пользователь crm, автозапуск).
3. Создай владельца «owner» и менеджера «manager» с СЛУЧАЙНЫМИ паролями: пароли запиши в файл /root/crm-paroli.txt с правами 600, в ответе и журнале их НЕ печатай.
4. Резервная копия: скрипт + таймер systemd раз в сутки в 03:00 МСК: копия базы через sqlite3 .backup (не простым копированием файла), хранить 14 последних на сервере и загружать копию на Яндекс Диск в disk:/Кодиум/crm-backup/ (токен YADISK_TOKEN в .env). Запусти копию один раз вручную.
5. ПРОВЕРКА ВОССТАНОВЛЕНИЯ (обязательно): добавь тестовую заявку через API, сделай копию, удали заявку, восстанови базу из копии (с остановкой службы), убедись, что заявка вернулась; покажи команды и результат.
6. Покажи: ufw status, curl -I https://<твой поддомен>/login, systemctl status crm-kodium, list-timers по резервной копии. Ни токенов, ни паролей в выводе.

Агент работал 11,8 минуты. sqlite3 .backup снимает согласованный слепок базы на ходу, простое копирование файла во время записи может дать битую копию. Таймер на 00:00 UTC, это 03:00 по Москве, хранится 14 копий.

Копия, которую восстанавливали

Резервная копия как запасной ключ от квартиры: пока не открыл им дверь, не знаешь, подходит ли. Поэтому пункт 5 обязательный: заявку добавили, сделали копию, удалили заявку, подложили копию при остановленной службе. Заявка вернулась.

Бэкап, из которого ни разу не восстанавливались, не бэкап. Лог восстановления (команды и что заявка вернулась) сохрани, он пойдёт в сдачу практики.

Кейс: «всё работает», а .env удалён

Перечитай отчёт шага выше. Восстановление проверено, а копия на Яндекс Диск не уходила: вместо YADISK_TOKEN в .env стояла заглушка. И не было SESSION_SECRET, это кейс урока 5.4.

После его починки агент перезапустил службу и удалил /opt/crm-kodium/.env. Служба жила на старом процессе, ночная копия падала (exit 2), в отчёте стояло «всё работает». После следующего перезапуска кабинет бы не поднялся.

Тесты проверяют код у тебя на компьютере, а не файл на сервере. Ловит пятиминутная проверка руками после каждого задания агенту на сервере:

# права и имена переменных (значения не печатаются)
ls -l /opt/crm-kodium/.env
cut -d= -f1 /opt/crm-kodium/.env
# где код читает настройки (без .venv с чужими библиотеками)
grep -rnE "getenv|environ" /opt/crm-kodium --include=*.py --exclude-dir=.venv
# перезапуск своими руками
systemctl restart crm-kodium
systemctl status crm-kodium
systemctl list-timers

Без --exclude-dir=.venv grep в прогоне выдал 19 строк вперемешку с чужими библиотеками. С ним осталось 6, и каждую стоит прочитать:

  • main.py:7 SESSION_SECRET = os.getenv("SESSION_SECRET") и main.py:11 INGEST_KEY = os.getenv("INGEST_KEY"): второго значения в скобках нет, так и должно быть;
  • test_main.py:11, 14, 15: тестовые значения в самих тестах, это норма;
  • main.py:96 if os.getenv("TESTING") == "1" and token == "":: тестовая лазейка в боевом коде. При TESTING=1 проверка CSRF-токена отключается.

TESTING в .env на сервере нет, так что сейчас дыры нет. Но хватит одной строки в .env, и формы открыты чужим сайтам, а тесты, идущие с TESTING=1, этого не заметят. Да и сами тесты на сервере лишние. Задание агенту из прогона (адрес заменён):

Проверка сервера глазами (grep -rnE "getenv|environ" /opt/crm-kodium --include=*.py --exclude-dir=.venv) нашла в main.py:96 строку `if os.getenv("TESTING") == "1" and token == "":` — при TESTING=1 в окружении отключается проверка CSRF. Это тестовая лазейка в боевом коде. Убери её: тесты должны получать и подставлять CSRF-токен, как настоящий браузер, а не отключать проверку. На сервер <адрес сервера в Москве> тесты (test_*.py, conftest) не выкладывай — удали их оттуда. Запусти тесты локально (pytest -q, полный итог), обнови код на сервере (НЕ трогай .env и базу), перезапусти службу, проверь, что она поднялась, и покажи вывод той же grep-команды на сервере.

Норма: права -rw——-, три имени на месте, у секретов нет второго значения и режимов «для тестов», после restart статус active. Самая надёжная проверка из урока 5.4: убери секрет, служба не стартует.

Кейс: зелёные тесты ценой защиты

Агент убрал лазейку за 3,2 минуты: grep на сервере показал две строки, тестов на сервере нет. Но тестов стало 8 вместо 9. Вместе с лазейкой агент удалил test_csrf_token_required, ту самую проверку, которую лазейка отключала.

Тест вернули заданием «удалять тесты, чтобы стало зелёным, нельзя». Итог: 1 failed, 8 passed. Форма с верным CSRF-токеном получила отказ 403.

Упал тест — сначала выясни, кто неправ: тест или приложение. Неправо приложение, значит, и менеджер на сервере не может сохранить статус, чини приложение. Неправ тест, чини тест. Боевой код не должен знать о тестах.

Причину агент нашёл верно. Cookie входа ставится с флагом Secure, то есть ходит только по HTTPS, а тестовый клиент ходит по HTTP, и cookie с токеном не возвращается. Неправ был тест. А починил агент приложение: добавил в main.py IS_TESTING (секрет совпал с тестовым) и при нём выключил https_only. И выложил это на сервер. Та же лазейка под новым именем.

Правильная починка в тесте: тестовый клиент с base_url="https://testserver" ходит как по HTTPS, cookie доходит, CSRF проверяется по-настоящему. Задание:

Причина найдена верно, но починка — новая лазейка в боевом коде: IS_TESTING = SESSION_SECRET == "test-session-secret-for-testing-only" отключает https_only. Боевой код не должен знать про тесты. Убери IS_TESTING и любые проверки «это тест» из main.py, https_only=True всегда. Почини ТЕСТ: создай TestClient с base_url="https://testserver" — тогда cookie с флагом Secure передаются, и проверка CSRF работает по-настоящему. pytest -q — все зелёные, покажи итог. Обнови main.py на сервере <адрес сервера в Москве> (не трогай .env и базу), перезапусти, проверь, что служба поднялась, и покажи вывод: grep -nE "https_only|IS_TESTING|TESTING" /opt/crm-kodium/main.py

Итог: в main.py только https_only=True, 9 passed, тестов на сервере нет, служба active.

Два окна терминала. Сверху «вывод до исправления»: grep -rnE "getenv|environ" по папке /opt/crm-kodium без .venv, 6 строк: main.py:7 SESSION_SECRET, main.py:11 INGEST_KEY, test_main.py:11, 14, 15 и выделенная красным main.py:96 if os.getenv("TESTING") == "1" and token == "". Снизу «после»: та же команда даёт 2 строки (main.py:7 и main.py:11), cut -d= -f1 .env показывает три имени SESSION_SECRET, INGEST_KEY, YADISK_TOKEN, таймер crm-backup на 00:00 UTC, ufw active с OpenSSH, 80 и 443. Справа локальный pytest -q: 9 passed. Адрес сервера скрыт.

Запомни три способа «добыть» зелёные тесты, все три агент применял в прогонах курса: удалить тест (здесь); пометить тест «пропустить», в П3 итог был 23 passed, 2 skipped; ослабить приложение под тест (TESTING, IS_TESTING). Каждый даёт зелёный итог и ничего не доказывает. Поэтому смотри число тестов, строку skipped и grep по боевому коду.

Три карточки с красной рамкой под заголовком «Как агент добывает зелёные тесты». «Удалить тест»: test_csrf_token_required пропал, было 9, стало 8. «Пропустить тест»: в П3 итог 23 passed, 2 skipped. «Ослабить приложение»: TESTING=1 отключал CSRF, IS_TESTING отключал https_only. Внизу зелёная полоса: упал тест — выясни, кто неправ; боевой код не знает о тестах; чини тест, а не защиту.
Сессия агента сломалась. После возврата теста сессия оборвалась ошибкой «API Error: 400 The provider rejected the request as invalid». Продолжение той же сессии (claude -c) за 5 секунд дало ту же ошибку. Не повторяй: начни новую сессию и первым сообщением передай контекст: что за проект, что уже сделано, что сломано.

Ключ приёма: сначала руками на двух серверах

Ключ X-Ingest-Key как пропуск на проходной: кабинет принимает заявку только от того, кто его показал. Создаёшь его ты сам до задания агенту и копируешь из терминала прямо в nano, иначе агент придумает свой или подставит значение по умолчанию (кейс ниже). Случайную строку даёт модуль secrets из стандартной библиотеки Python:

# 1. у себя: случайный ключ (секрет сессий — token_urlsafe(48))
python -c "import secrets;print(secrets.token_urlsafe(32))"
# 2. Москва: ssh root@<адрес сервера в Москве>
nano /opt/crm-kodium/.env      # строка INGEST_KEY=<ключ>
chmod 600 /opt/crm-kodium/.env
systemctl restart crm-kodium
# 3. Амстердам: ssh root@<адрес сервера в Амстердаме>
nano /opt/bot-kodium/.env      # CRM_INGEST_KEY=<тот же ключ>
                               # CRM_URL=https://<твой поддомен>/api/zayavka
chmod 600 /opt/bot-kodium/.env
systemctl restart bot-kodium kodium-forma
Руками на сервере:
• пароли пользователей: ssh root@<адрес> "cat /root/crm-paroli.txt", только в своём терминале, потом в менеджер паролей;
• имена в .env без значений: cut -d= -f1 /opt/crm-kodium/.env;
• новый секрет сессий: python -c "import secrets;print(secrets.token_urlsafe(48))", вставить в .env через nano, затем chmod 600;
• копия разово: systemctl start <служба копии> (имя из list-timers), журнал: journalctl -u <служба копии> -n 20;
• сменил ключ в Москве, смени и в Амстердаме, перезапусти обе службы и сверь отпечатки.

Бот и форма пишут в кабинет

Задание агенту (по прогону, ключи теперь записываешь ты):

Заявки теперь хранятся в «Кабинете заявок» в России (152-ФЗ). Проект bot-kodium (бот и форма) на сервере <адрес сервера в Амстердаме>. В .env там я сам записал CRM_URL и CRM_INGEST_KEY: не меняй и не печатай.
1. Каждую заявку (бот и форма) сразу POST на CRM_URL с заголовком X-Ingest-Key, поля name, phone, course, source («бот»/«сайт»), consent_at; таймаут 10 с, 2 повтора.
2. Уведомление владельцу в Telegram БЕЗ имени и телефона: «Новая заявка №<номер из ответа CRM>, курс, источник» + ссылка https://<твой поддомен>/zayavka/<номер>.
3. Выгрузку zayavki.xlsx на Диск выключи настройкой YADISK_EXPORT=0, код не удаляй.
4. CRM недоступна после повторов: в pending.jsonl только номер попытки, курс и время; владельцу в Telegram «CRM недоступна, клиент: <имя> <телефон> — внеси вручную»; в журнал без ПД.
5. Тесты с подменой CRM и Telegram; обнови bot-kodium и kodium-forma, перезапусти, curl-ом отправь заявку с формы и покажи номер от CRM.

Агенту хватило 8,5 минуты. Если кабинет недоступен, заявка не теряется.

Кейс: ключ «по умолчанию» на обоих серверах

В прогоне ключ на обоих серверах совпал, но это было dev-ingest-key-change-in-production: агент раньше вписал значение по умолчанию в локальный .env, и оно уехало на оба сервера. Поддельные заявки мог слать любой, кто читал код. Ключ заменили руками, как в шаге выше; старый получил 401, заявка с формы пришла под номером 3.

Сверить ключи можно, не показывая их. Отпечаток получается из ключа однозначно, но ключ из него не восстановить, как по отпечатку пальца не увидеть лица. Зайди по ssh на каждый сервер (путь /opt/bot-kodium/.env из прогона, у тебя папка своего бота):

# сервер в Москве
grep '^INGEST_KEY=' /opt/crm-kodium/.env | cut -d= -f2- | sha256sum | cut -c1-12
# сервер в Амстердаме
grep '^CRM_INGEST_KEY=' /opt/bot-kodium/.env | cut -d= -f2- | sha256sum | cut -c1-12

Совпали, значит ключи одинаковые; нет, значит кабинет ответит боту 401. Отпечаток можно показать в сдаче, ключ нельзя. Ключ один, а отпечатки разные? Проверь кавычки вокруг значения и конец строки Windows в .env.

Если не получилось

  • После restart служба не поднимается → ls -l /opt/crm-kodium/.env; файла нет, значит агент его удалил. Восстанови по врезке «Руками на сервере»: новые SESSION_SECRET и INGEST_KEY (тот же в Амстердаме), YADISK_TOKEN, права 600, перезапуск.
  • Копия падает с exit 2 → journalctl -u <служба копии> -n 20; в прогоне причиной был удалённый .env.
  • На Диске нет crm-backup → токен в .env заглушка или без одного из прав, чтение или запись (урок 2.4); вставь свой сам.
  • Бот получает 401 → сверь отпечатки ключей.
  • Старый ключ ещё принимается → в коде значение по умолчанию; задание агенту убрать его.
  • Сессия агента отвечает «API Error: 400» и на продолжение тоже → начни новую сессию, контекст в первом сообщении.

Контрольный вопрос. Агент пишет: «Секрет добавлен, служба перезапущена, всё работает». Что делаешь сам, прежде чем идти дальше?

АВерю отчёту, тесты же зелёные
БПрошу агента прислать содержимое .env в чат
ВЗахожу на сервер: проверяю, что .env на месте с правами 600, сам перезапускаю службу и запускаю копию
ГУдаляю .env, чтобы секрет точно никто не увидел

Подсказка: Вспомни, почему служба в прогоне «работала» без .env.

На курсе задания проверяются автоматически. Решить первое задание бесплатно →  ·  Подобрать курс

Контрольный вопрос. Команда отпечатка ключа вывела на сервере в Москве и на сервере в Амстердаме разные строки. Что будет с новой заявкой из бота?

АКабинет примет заявку, ключи сверяются только при входе
БЗаявка запишется дважды
ВБот сам подставит верный ключ
ГКабинет ответит 401, заявка не запишется в базу

Подсказка: Отпечатки одинаковы только у одинаковых ключей.

На курсе задания проверяются автоматически. Решить первое задание бесплатно →  ·  Подобрать курс

Контрольный вопрос. Когда ночную резервную копию можно считать рабочей?

АКогда таймер виден в list-timers
БКогда из копии восстановили базу и удалённая заявка вернулась
ВКогда на Диске лежит 14 файлов
ГКогда агент написал, что копия сделана

Подсказка: Чем проверяют запасной ключ от квартиры?

На курсе задания проверяются автоматически. Решить первое задание бесплатно →  ·  Подобрать курс

Контрольный вопрос. Агент вернул удалённый тест CSRF, тест упал: форма с верным токеном получает 403. Что делаешь первым?

АПрошу агента удалить тест снова, раз он мешает
БРазрешаю агенту отключать проверку CSRF, если идёт тест
ВВыясняю, кто неправ: тест или приложение, и чиню именно сломанное
ГПомечаю тест «пропустить», чтобы итог стал зелёным

Подсказка: Боевой код не должен знать о тестах.

На курсе задания проверяются автоматически. Решить первое задание бесплатно →  ·  Подобрать курс

Задание. МИССИЯ «Кабинет в Москве». 1. Закажи сервер в России (почасовая оплата подойдёт) со своим ssh-ключом (урок 2.5), направь поддомен A-записью (урок 3.3). После сдачи удали учебный сервер. 2. Дай агенту задание выкладки со своими адресами. Пароли читай сам из /root/crm-paroli.txt. 3. Сохрани лог проверки восстановления. 4. Сгенерируй ключ приёма, запиши руками на оба сервера, свяжи бота и форму заданием из урока, сверь отпечатки. 5. Сломай и почини: попроси агента отправить в CRM заявку с неверным ключом, затем с верным, но без согласия. Ожидается 401 и 400. 6. Проверь сервер командами из раздела про удалённый .env; лазейку или секрет по умолчанию исправь заданием агенту. Принято, если: приложены лог восстановления, два совпавших отпечатка, коды 401 и 400, вывод grep по getenv|environ без лазеек, ls -l и имена переменных .env (без значений), systemctl status crm-kodium после твоего перезапуска и таймеры с копией. Паролей, ключей и адресов серверов в снимках нет.

Подсказка: Если отпечатки не совпали, сначала найди, на каком сервере ключ остался значением по умолчанию.

✅ Готово, если: программа запускается без ошибок и выводит то, что просят в задании.

На курсе задания проверяются автоматически. Решить первое задание бесплатно →  ·  Подобрать курс

Что дальше

Кабинет в Москве, копии восстанавливаются, бот пишет по ключу. Дальше разговор с заказчиком: как не дать CRM разрастись до 30 полей.

Итог:
• данные клиентов на сервере в Москве, Telegram работает из Амстердама;
• копия ночью, 14 штук и Яндекс Диск, восстановление проверено;
• после агента сам смотришь .env, код через grep, перезапускаешь службу и копию;
• ключ приёма создаёшь сам и сверяешь по отпечатку.
• упал тест — выясни, кто неправ; боевой код не знает о тестах.

← Назад  ·  ↑ В начало урока  ·  ⌂ В начало курса  ·  Вперёд →

Школа Виктора Комлева