惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

U
Unit 42
T
Threatpost
C
CERT Recently Published Vulnerability Notes
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Security Archives - TechRepublic
Security Archives - TechRepublic
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
K
Kaspersky official blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Attack and Defense Labs
Attack and Defense Labs
N
News and Events Feed by Topic
Project Zero
Project Zero
H
Heimdal Security Blog
C
Cybersecurity and Infrastructure Security Agency CISA
Know Your Adversary
Know Your Adversary
Google Online Security Blog
Google Online Security Blog
W
WeLiveSecurity
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Schneier on Security
Schneier on Security
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
N
News | PayPal Newsroom
Hacker News - Newest:
Hacker News - Newest: "LLM"
H
Hacker News: Front Page
L
LINUX DO - 热门话题
Spread Privacy
Spread Privacy
T
Threat Research - Cisco Blogs
Cloudbric
Cloudbric
V
Vulnerabilities – Threatpost
Hacker News: Ask HN
Hacker News: Ask HN
S
Securelist
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
TaoSecurity Blog
TaoSecurity Blog
NISL@THU
NISL@THU
N
News and Events Feed by Topic
S
Security Affairs
The Last Watchdog
The Last Watchdog
T
Tor Project blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
T
The Exploit Database - CXSecurity.com
Simon Willison's Weblog
Simon Willison's Weblog
P
Palo Alto Networks Blog
AWS News Blog
AWS News Blog
P
Proofpoint News Feed
C
Cisco Blogs
C
Cyber Attacks, Cyber Crime and Cyber Security
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
L
LINUX DO - 最新话题
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
T
Tenable Blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
S
Schneier on Security

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Как я без навыков fullstack-разработчика сделал свой SaaS
xcillx · 2026-05-22 · via Все публикации подряд на Хабре

Уровень сложностиПростой

Время на прочтение10 мин

Охват и читатели51

Кейс

Всем привет. Хочу поделиться тем, как я, совершенно без навыков fullstack-разработчика, сделал, как мне кажется, прикольный SaaS. Весь текст - мой поток мыслей и воспоминаний.

Моё устройство: MacBook Air M2, 16 ГБ. У меня нет опыта работы в бигтехе, в принципе в каком либо техе, хехе 🙂


В декабре 2025 года, когда поехал в гости к родителям праздновать Новый год, столкнулся с небывалой для меня критической проблемой — отсутствием доступа к интернету. ChatGPT, Grok, Claude, YouTube, Telegram — всё. Мой родной регион был одним из первых, на ком РКН тестировал белые списки.

До этого я пользовался для обхода блокировок OpenVPN — энтузиасты выкладывали ключи в открытый доступ. Да, с моей стороны это было пренебрежением безопасностью. Но меня устраивало, пока в конце 2025 года OpenVPN не забанили. Мой университет подключал студентов к своей сети через него, а теперь сосет лапу.

Я не готов мириться с принудительными блокировками. Поэтому, находясь всё ещё в регионе, в поисках решения наткнулся на него — протокол Hysteria2. Быстренько развернул VPS, настроил и был рад возвращению в современный мир.

Но во время чтения доки меня зацепили возможности этого протокола. По сути — протокол, удобно упакованный в Docker-контейнер, который давал отличный и удобный API.


В конце февраля 2026 года мне попалось видео на YouTube — «Как устроена оплата картой». Это видео показало мне, как работает REST API, и дало осознание: из маленьких блоков можно построить что-то большее. Мне стало настолько интересно, что я развернул контейнер на своём MacBook и теперь был готов подёргать его за ручки.

Сначала курлами, но потом пришла мысль автоматизировать это через Python. Первое, с чем столкнулся — библиотека requests.

С иишкой быстренько накатали код, в котором мне предстояло очень подробно разобраться. Вот кусочек:

API_HOST   = "127.0.0.1"
API_PORT   = 8080
API_SECRET = "sixseven"

USERS = {
    "s"   : "ss",
    "user": "password",
    "test": "12345",
}

ALLOWED = {
    "s"   : False,
    "user": True,
    "test": False,
}

def get_online_users():
    url = f"http://{API_HOST}:{API_PORT}/online"
    headers = {"Authorization": API_SECRET}

    try:
        r = requests.get(url, headers=headers, timeout=5)
        r.raise_for_status()           # выбросит ошибку, если не 200
        return r.json()                # {"username": connection_count, ...}
    except requests.exceptions.HTTPError as e:
        print(f"API вернул ошибку {r.status_code}: {r.text}")
        return {}
    except requests.exceptions.RequestException as e:
        print(f"Ошибка подключения к API: {e}")
        return {}

Не буду врать, на новичка такое производит большое впечатление. Поэтому, восхищённый этими возможностями, я решил сделать простой VPN-сервис. Исключительно чтобы разобраться и удовлетворить любопытство.


Началась разработка. Разделил проект на три части: backend, frontend, Hysteria2 — три независимые сущности, зоны ответственности которых не пересекаются. Так я познакомился с Docker Compose. Контейнер для Hysteria2 уже тянулся с Docker Hub, а через Compose весь стек поднимался за секунды.

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


Дальше я попросил ИИшку выплюнуть мне README проекта со всеми эндпоинтами, чтобы другая ИИшка сделала чистый фронт. Я давно работал с Manus — это агент, который может запилить веб-приложение с нуля. Однако я три раза с нуля просил его переписать фронт. На тот момент у меня не было никакого понимания, как работает фронт, поэтому даже не мог толком объяснить, что не так.

Отбросив весь говнокод от Manus (много мусора, потому что ИИ-агенты щедро наваливают рядом кучу... отладочных скриптов и вспомогательных файлов для себя), я начал писать фронт с Claude с нуля — причём с полным пониманием каждого шага. Так познакомился с Vite — бандлером, который собирает проект из TS, TSX и прочего и в процессе разработки выступает сервером: по запросу браузера отдаёт собранную HTML-страничку.

Тут так же стоит упомянуть одну делаль - ИИшки тупые. В плане они не могут удерживать весь контекст в памяти. Поэтому мною было принято решение сделать собственную дизайн систему, адаптированную для ИИ.

Когда я прошу ИИ накатать мне новую страницу или компонент - она может расставить рандомные классы для стилизации текста и компонентов. Поэтому у меня есть правило-промпт к которому обращаются ИИ, чтобы ВЕСЬ текст - шрифты, отступы, цвета, а так же компоненты - имели ОДИН И ТОТ ЖЕ стиль.

 тут можно увидеть как устроена типографика

тут можно увидеть как устроена типографика

По сути - это дизайн система всего проекта. В папке cStyles лежат *Stls.ts файлы, которые описывают стили для каждого раздела компонентов в проекте:

src/styles/
├── tokens.ts        — атомарные токены (typography, surface, radius, …)
├── animations.ts    — transition, hover, press, enter, loading
├── variants.ts      — colorScheme + тип ColorScheme
├── index.ts         — единая точка входа, реэкспортирует всё
└── cStyles/         — стили компонентов, разбитые по папкам src/components/
    ├── uiStls.ts        компоненты из components/ui/
    ├── commonStls.ts    компоненты из components/common/
    ├── layoutStls.ts    компоненты из components/layout/
    ├── dashboardStls.ts → компоненты из components/dashboard/
    ├── usersStls.ts     → компоненты из components/users/
    ├── serversStls.ts   → компоненты из components/servers/
    └── pagesStls.ts     → компоненты из pages/

Сначала никакой дизайн системы не было. Страницы выглядели хаотичными. Где то отличался размер шрифтов, где-то цвета. Только потом появился промпт для перевода компонентов и страниц на дизайн систему — это md файл на 250+ строк.


Что касается ИИ — использовал много разных: Claude, Grok, Gemini, ChatGPT, Codex. Не заплатил ни цента. Создал примерно 15 Google-аккаунтов, и лимитов на весь рабочий день хватало с запасом.

Штука в том, что ИИ важен контекст — как устроены эндпоинты, что лежит в .env, как связаны части проекта. Без этого каждый раз объяснять всё с нуля — боль. Поэтому я начал архивировать проект через tar и скидывать архив в чат. Если фича не требовала бОльшего контекста, только нужную часть, чтобы не сжигать токены впустую.

Но каждый раз руками прописывать исключения — .gitvenvnode_modulesdist и прочий мусор — такое себе. Поэтому написал bash-скрипт pack.sh. Вот как примерно он выглядел:

#!/usr/bin/env bash
#
# Скрипт pack — упаковка частей проекта htrBox в .tar архивы
# Все архивы сохраняются на уровень выше проекта (/Users/stas/projects/)
#
# Использование:
#   pack          -> весь проект (кроме исключений)
#   pack back     -> backend + docker-compose.yaml + .env
#   pack front    -> frontend + docker-compose.yaml + .env
#   pack --dry-run [all|back|front]  -> только показать команду tar, не выполнять
#

set -euo pipefail

# ------------------------------------------------
#          ЖЁСТКО ЗАХАРДКОЖЕННЫЕ ПУТИ
# ------------------------------------------------

readonly PROJECT_ROOT="/Users/stas/projects/htrBox"
readonly OUTPUT_DIR="$(dirname "$PROJECT_ROOT")"   # -> /Users/stas/projects
readonly PROJECT_NAME="htrBox"

# ------------------------------------------------
#     Список исключений (при pack без аргументов)
# ------------------------------------------------

declare -a EXCLUDES=(
    ".git"
    ".env"
    "backend/venv"
    "backend/.DS_Store"
    "frontend/node_modules"
    "frontend/.DS_Store"
    "frontend/package-lock.json"
    ".DS_Store"
    "other"
    "exclude.txt"
)

и тд ...

Ещё один трюк, который реально изменил процесс. Раньше я сразу говорил ИИ: «давай реализуем фичу». Теперь — нет. Сначала говорю: не трогай код. Опиши план внедрения фичи с TODO-маркерами и подробным описанием каждого шага — так, чтобы человек, который вообще не в теме проекта, понял что делать. Мы обкашливали план, и только потом я давал команду: вперёд, с первого пункта.

Важный момент: просил ИИ отдавать только изменённые файлы — включая обновлённый TODO с текущим прогрессом после выполнения каждого шага. Копировал файлы, смотрел через git dif  в VSCode, что поменялось, если все ок — двигались дальше.

Кстати, GitKraken — топ. Раньше сидел на терминальном tig, но GitKraken удобнее, особенно для визуализации веток. Для одного разработчика — бесплатный 🙂

Так в чём кайф всего этого подхода? Когда ИИ начинает галлюцинировать или заканчиваются токены на аккаунте — не страшно. Можно сделать ./pack.sh, перекинуть архив с актульным контекстом на новый аккаунт вместе с актуальной историей TODO, и продолжить работу с того места где остановился.

Я считаю, что такой поход к разработке относительно замедляет работу. Но это не критично, потому что дает полный контроль и понимание над процессом. Данный подход использовался исключительно для Claude, потому что он умеет работать с tar архивами (как и Manus).


Написав какую-то часть фронта и запустив всю эту шарманку, возникла куча других проблем. Бэкенд есть, данные там обновляются — но как их доставить до фронта? Причём не один раз, а постоянно. Как сделать так, чтобы после перезагрузки страницы не требовался повторный вход? Как не долбить сервер лишними запросами и кэшировать данные? Вопросов было много.

Насоветовавшись с ИИ, познакомился с концепцией DOM-дерева и отобрал для себя стек:

  • TanStack Query — для автоматического обновления данных на странице

  • Zustand + localStorage — для кэширования состояния между сессиями

  • wouter — лёгкий роутер для навигации, значительно проще React Router

  • httpOnly cookie — для хранения refresh token; проставляется через заголовок в ответе бэкенда

Чтобы не тонуть в сложности большого проекта, сделал отдельный мини-проект — максимально упрощённую версию, чтобы понять концепцию поллинга через TanStack Query и кэширование через Zustand + cookie. Ну и заодно разобрался, как работает JWT-авторизация.

Ещё открыл для себя Postman. Через него тестировать API — кайф. Особенно классная фича — наследование авторизации всей коллекции сверху вниз: не нужно прописывать pre- и post-scripts для каждого запроса (правда бился 3 часа почему postman не видит cookie - надо выставить тумблер Cookie Jar для каждого эндпоинта в OFF).

postman moooood

postman moooood


Разобравшись в мини-проекте, как работают эти два брата — frontend и backend, — пришло время следующей главы: базы данных.

На тот момент у меня не было вообще никакого опыта. Что такое foreign key или primary key — загадка. Поэтому скачал DataGrip, сделал дамп базы с прода и начал разбираться. PostgreSQL уже крутился на проде, но что происходило внутри — было непонятно. Для понимания баз данных сделал локальную среду для лабораторных работ по SQL на базе PostgreSQL 17 в Docker-контейнере.

Нашёл классный курс от Postgres Pro и их же книгу по SQL, где объясняются ключевые концепции: как проектировать схему, что такое нормализация, как писать запросы, как делать DDL. После этого база данных стала для меня интересным объектом исследования. К тому же пришло понимание, что ИИшка не все связи между таблицами выстроила, поэтому могли быть висячие данные (которые должны удаляться на ON DELETE CASCADE).


Разработка идет полным ходом. Конечно же ВСТАЛ вопрос об автоматизации деплоя на сервера. Такое тут тоже есть. Bash скрипт. Скрипт накатывает docker container Hysteria2 на ноды в других странах (которые принимают соединение от пользователей с последующим проксированием) и на backend в Yandex Cloud. Вот пример функции деплоя на YC с автоматическим продлением сертификатов через certbot:

скрин admin dashboard

скрин admin dashboard

# ----------------------------------------------------
# ДЕПЛОЙ Yandex Cloud (frontend + backend + postgres)
# ----------------------------------------------------

deploy_yc() {
  echo ""
  echo "-------------------------------------------"
  echo "  🚀 Деплой -> Yandex Cloud ($YC_HOST)"
  echo "-------------------------------------------"

  [ ! -f "$SCRIPT_DIR/certificates/yandex-cloud.ini" ] && fail "certificates/yandex-cloud.ini не найден"
  [ ! -f "$SCRIPT_DIR/infra/.env" ]                    && fail "infra/.env не найден"
  [ ! -f "$SCRIPT_DIR/infra/nginx.conf" ]              && fail "infra/nginx.conf не найден"

  log "Создаём директории на YC..."
  ssh -i "$YC_KEY" "$YC_USER@$YC_HOST" "mkdir -p $YC_DIR $YC_DATA_DIR/pgdata"

  log "Копируем backend/ на YC..."
  tar -czf /tmp/backend.tar.gz \
    --exclude="venv" \
    --exclude="__pycache__" \
    --exclude="*.pyc" \
    --exclude="pytest.ini" \
    --exclude=".pytest_cache" \
    --exclude="requirements-dev.txt" \
    --exclude="TODO" \
    -C "$PROJECT_ROOT" backend
  scp -i "$YC_KEY" /tmp/backend.tar.gz "$YC_USER@$YC_HOST:/tmp/"
  ssh -i "$YC_KEY" "$YC_USER@$YC_HOST" "tar --warning=no-unknown-keyword -xzf /tmp/backend.tar.gz -C $YC_DIR && rm /tmp/backend.tar.gz"
  rm /tmp/backend.tar.gz

  log "Копируем frontend/ на YC..."
  tar -czf /tmp/frontend.tar.gz \
    --exclude="node_modules" \
    --exclude="dist" \
    --exclude="TODO" \
    --exclude="nginx.conf" \
    -C "$PROJECT_ROOT" frontend
  scp -i "$YC_KEY" /tmp/frontend.tar.gz "$YC_USER@$YC_HOST:/tmp/"
  ssh -i "$YC_KEY" "$YC_USER@$YC_HOST" "tar --warning=no-unknown-keyword -xzf /tmp/frontend.tar.gz -C $YC_DIR && rm /tmp/frontend.tar.gz"
  rm /tmp/frontend.tar.gz

  # nginx.conf для prod передаётся отдельно, поверх распакованного frontend/
  # dev-файл (frontend/nginx.conf в репозитории) не затрагивается
  log "Копируем prod nginx.conf на YC (infra/nginx.conf -> frontend/nginx.conf)..."
  scp -i "$YC_KEY" "$SCRIPT_DIR/infra/nginx.conf" "$YC_USER@$YC_HOST:$YC_DIR/frontend/nginx.conf"

  log "Копируем docker-compose и .env..."
  scp -i "$YC_KEY" "$SCRIPT_DIR/infra/docker-compose.yaml" "$YC_USER@$YC_HOST:$YC_DIR/docker-compose.yaml"
  scp -i "$YC_KEY" "$SCRIPT_DIR/infra/.env"                "$YC_USER@$YC_HOST:$YC_DIR/.env"

  log "Копируем Cloudflare токен..."
  ssh -i "$YC_KEY" "$YC_USER@$YC_HOST" "mkdir -p ~/.secrets && chmod 700 ~/.secrets"
  scp -i "$YC_KEY" "$SCRIPT_DIR/certificates/yandex-cloud.ini" "$YC_USER@$YC_HOST:~/.secrets/cloudflare-yc.ini"
  ssh -i "$YC_KEY" "$YC_USER@$YC_HOST" "chmod 600 ~/.secrets/cloudflare-yc.ini"

  log "Проверяем сертификат для stdoq.ru..."
  ssh -i "$YC_KEY" "$YC_USER@$YC_HOST" '
    CERT_PATH="/etc/letsencrypt/live/stdoq.ru/fullchain.pem"
    RENEW_NEEDED=false

    if [ ! -f "$CERT_PATH" ]; then
      echo "  -> Сертификат не найден, получаем новый..."
      RENEW_NEEDED=true
    else
      EXPIRY=$(openssl x509 -enddate -noout -in "$CERT_PATH" | cut -d= -f2)
      EXPIRY_TS=$(date -d "$EXPIRY" +%s 2>/dev/null || date -j -f "%b %d %T %Y %Z" "$EXPIRY" +%s 2>/dev/null)
      NOW_TS=$(date +%s)
      DAYS_LEFT=$(( (EXPIRY_TS - NOW_TS) / 86400 ))
      echo "  -> Сертификат найден, осталось дней: $DAYS_LEFT"
      [ "$DAYS_LEFT" -lt 30 ] && RENEW_NEEDED=true
    fi

    if [ "$RENEW_NEEDED" = true ]; then
      if ! command -v certbot &>/dev/null; then
        echo "  -> Устанавливаем certbot..."
        sudo apt-get update -qq && sudo apt-get install -y -qq certbot python3-certbot-dns-cloudflare
      fi
      sudo certbot certonly \
        --dns-cloudflare \
        --dns-cloudflare-credentials ~/.secrets/cloudflare-yc.ini \
        -d stdoq.ru -d www.stdoq.ru \
        --email ТУТ БЫЛ МОЙ EMAIL@yandex.ru \
        --agree-tos --no-eff-email \
        --non-interactive
      echo "  -> Сертификат получен ✓"
    else
      echo "  -> Сертификат актуален, пропускаем ✓"
    fi
  '

  log "Запускаем контейнеры (--build)..."
  ssh -i "$YC_KEY" "$YC_USER@$YC_HOST" "
    cd $YC_DIR
    docker compose -f docker-compose.yaml up -d --build --force-recreate frontend backend postgres
    docker image prune -f
  "

  echo ""
  echo "---- Статус контейнеров ----"
  ssh -i "$YC_KEY" "$YC_USER@$YC_HOST" "docker ps --filter name=frontend --filter name=backend --filter name=postgres --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'"

  echo ""
  echo "---- Логи backend (последние 20 строк) ----"
  ssh -i "$YC_KEY" "$YC_USER@$YC_HOST" "docker logs backend --tail=20"

  log "Yandex Cloud задеплоен ✓"
}

Где Ansible? Где Terraform? Для трех или пяти серверов не критично купить и настроить их ручками. Пытался переписать bash скрипты для deploy и cleanup (он тоже есть) на Ansible PlayBooks, но повяз в этом. К тому же не хотелось лишать пользователей (в случае неудачно написанного playbook'a) их законного доступа к интернету. На данный момент bash отлично справляется - решил я.


Немного скринов, как это выглядит:

Сейчас backend стреляет всем пулом пользователей. Если пользователей станет больше 100 (что я не планирую), придется добавлять пагинацию. Тогда данные будут лететь, например, по 50 человек.

как выглядит список юзеров для админа

как выглядит список юзеров для админа

У каждого пользователя в профиле отображается зверушка, которая назначается хешированием юзернейма по алгоритму djb2. Их 12 штук, все SVG.

лк пользователей

лк пользователей


И вот я тут 🙂— 21 мая 26 года — хочу прикрутить платёжный шлюз. API уже написано, но никто пока не одобрил подключение. Кто знает какие-нибудь платежные шлюзы для high risk без оформления сз/ип и прочих ненужностей? Желательно без конских комиссий, докой API и наличием Sandbox.

В будущем планирую добавить Prometheus с Grafana, Node Exporter для отслеживания метрик нод и отображения их профиле пользователей, как бизнес фича.