Skip to main content

Как мы строили «Vercel для России»: четыре переписывания билда и другие приключения

· 7 min read

Знакомый прислал ссылку на лендинг своего стартапа. Сайт не открывался без VPN.

Я спросил, где деплоил. Vercel, говорит. Ну да, говорю, понятно.

За последние пару лет это стало паттерном. Разработчик делает сайт, деплоит на Vercel — потому что это удобно и быстро — а потом обнаруживает, что половина его российской аудитории видит либо тайм-аут, либо страницу браузера с предложением проверить соединение. AWS-регионы, на которых работает Vercel, работают для России нестабильно — зависит от провайдера, региона и фазы луны.

Окей, есть же Yandex Cloud. Российская инфраструктура, рублёвый биллинг, стабильные IP. Но поднять там статический сайт с HTTPS и доменом — это несколько часов в консоли, три разных сервиса и неизбежный ритуал с DNS-чейленджем, который с первого раза не работает. Я проходил этот квест несколько раз и каждый раз тихо матерился в районе Certificate Manager.

В какой-то момент стало понятно, что это системная дыра. Современный вайбкодер — человек, который написал сайт в Cursor или собрал лендинг через Bolt — не должен знать ничего про DNS-записи для подтверждения сертификатов. Он должен нажать кнопку и получить живой сайт. Именно это мы и строим.

Что такое Layero

Одна строчка: платформа деплоя фронтенда на российской инфраструктуре.

Даёшь репозиторий — получаешь живой сайт с доменом, HTTPS и превью-средой для каждой ветки. Сертификаты, CDN, сборка — наша забота. Рублёвый биллинг, никакого Stripe.

Ключевая вещь — превью-среды. Каждый push в ветку получает свой URL, доступный через 30 секунд после сборки. Пул-реквест можно показать дизайнеру прямо из гитхаба, не объясняя, как запустить проект локально.

Как устроен деплой под капотом

Когда приходит webhook от GitHub, API сразу возвращает ответ и уходит — сборка запускается асинхронно в отдельном воркере. Воркер клонирует репозиторий, определяет фреймворк, ставит зависимости, собирает проект, загружает результат в S3 и активирует деплой. После активации все узлы платформы узнают о новом контенте в течение примерно 10 секунд — через механизм уведомлений в базе данных.

Превью за 30 секунд при 15-минутном CDN

YC CDN распространяется по регионам 5–15 минут. Это объективная реальность, которую не обойти. При этом хочется, чтобы превью была доступна сразу после сборки — иначе какой смысл.

Решение: два отдельных пути доставки для каждого деплоя.

Первый путь — превью-ссылка, которая идёт напрямую с сервера, минуя CDN. Сайт живёт через 30 секунд после завершения сборки.

Второй путь — основной адрес проекта через CDN: нормальный TLS, кеширование на краевых узлах, глобальная доступность. Готовность CDN определяем инструментально — опрашиваем несколько публичных DNS-резолверов и ждём, пока регион не прогреется. Как только это произошло, превью-ссылка автоматически начинает перенаправлять на основной адрес. Через сутки превью перестаёт работать.

SPA fallback: то, чего YC CDN не умеет

Классическая проблема SPA: пользователь открывает https://example.com/dashboard напрямую — CDN смотрит в S3, файла /dashboard нет, возвращает 404. Нужен fallback: на 404 отдавать index.html с кодом 200.

Vercel делает это из коробки. YC CDN — нет, просто не поддерживает. Поэтому между CDN и хранилищем у нас стоит дополнительный слой, который перехватывает 404 и отдаёт index.html. Это стандартная схема для SPA, но её пришлось явно организовать самостоятельно.

Изоляция сборок: gVisor

Сборки пользователей — это чужой код, которому доверять нельзя. Запускать его напрямую на сервере — плохая идея.

Для изоляции используем gVisor. Это не полноценная виртуальная машина, но гораздо лучше обычного Docker: gVisor перехватывает системные вызовы и исполняет их в изолированной среде, не давая коду добраться до хостовой системы. Дешевле полноценной виртуализации, но достаточно для наших целей.

Каждый воркер работает с ограниченной памятью и процессором, без доступа к файловой системе хоста и с выходом в сеть только на нужные адреса — npm-registry, GitHub, наше хранилище. Раз в несколько минут фоновый процесс убирает завершившиеся контейнеры и временные файлы.


Четыре переписывания архитектуры билда

Это та часть истории, которую стоит рассказать подробно.

Попытка первая: Serverless Container

Очевидная первая мысль — запускать сборки в YC Serverless Container. Serverless масштабируется сам, платишь за использование, не надо думать об инфраструктуре. Звучит хорошо.

YC Serverless Container даёт 256 МБ временного места на диске — и всё. Кэш пакетного менеджера писать некуда. Крупный проект с 300+ зависимостями переполняет этот лимит ещё на этапе установки. Первое время мы говорили пользователям, что у них «слишком большой проект» — что, конечно, было неправдой.

С сетью отдельная история. Serverless NAT сбрасывает неактивные соединения примерно через две минуты. Установка пакетов — это сотни последовательных запросов к registry, каждый из которых может попасть в это окно. Результат: установка зависала на середине без каких-либо ошибок в логах, просто переставала что-либо делать. Целые сборки уходили в тайм-аут по 16 минут.

Итого: 15+ минут на CRA-проект, нестабильность, и отсутствие кэша означало, что каждый деплой скачивал все зависимости заново.

Попытки вторая и третья: борьба с ограничениями

Следующие два захода — вариации на тему «обойти ограничения не меняя архитектуру». Указывали пакетным менеджерам альтернативные пути для кэша, скачивали исходники через архивный API GitHub вместо полноценного git-клонирования — это быстрее и не страдает от NAT. Помогало, но не кардинально. 15 минут превратились в 8–10, но системные проблемы никуда не делись.

Параллельно несколько недель теряли время на охоту за случайными зависаниями при установке пакетов. Симптомы выглядели как «что-то с сетью YC». Оказалось — собственный npm-прокси, который мы подняли для кэширования. При одновременных запросах к внешнему registry он уходил в offline-режим, и менеджер пакетов вместо ошибки просто висел в ожидании ответа, который никогда не придёт. Фиксится одним параметром конфигурации — но чтобы это найти, нужно было несколько итераций с включёнными подробными логами.

Попытка четвёртая: VM с NVMe

Решение, которое реально сработало — перестать бороться с ограничениями SC и просто взять нормальную VM с NVMe-диском.

На VM работает диспетчер, который принимает задачи сборки и поднимает под каждую изолированный контейнер. Кэш зависимостей хранится на хосте и доступен контейнеру — так пакеты не скачиваются заново при каждом деплое.

Время сборки — 2–3 минуты на типичном проекте. Сеть стабильная. Диск не заканчивается.

Сейчас работаем над пятым заходом: параллельные сборки и умная очистка кэша зависимостей. Но это уже оптимизация поверх работающей системы, а не попытка реанимировать то, что работать не хочет.


Что такое проект, окружение и деплой

Этот вопрос кажется тривиальным ровно до того момента, как начинаешь реализовывать.

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

В первой версии слоя «окружений» не существовало — проект напрямую ссылался на деплои. Это работало ровно до появления превью-сред, когда стало непонятно, что считать продакшном, а что — временным превью.


Автодетект фреймворка

Загружаешь репозиторий — платформа сама понимает, что перед ней: Next.js, Vite, Astro, Nuxt, SvelteKit, Gatsby, CRA, Docusaurus или просто статические файлы. Определяет пакетный менеджер по lock-файлу, подбирает версию Node.js из конфига проекта. Поддерживаются Node 18, 20 и 22 — бинарники предустановлены в образе, ничего не скачивается в процессе сборки.

Любое из этих решений можно переопределить вручную в настройках проекта или при деплое через CLI.


Сертификаты

Именно с этим начинался весь проект.

В Layero SSL работает так: добавляешь домен — платформа сама запрашивает сертификат через Let's Encrypt и прикрепляет к CDN. Если что-то пошло не так — показываем причину. Предупреждаем за две недели до истечения.

Пользователь видит «Сертификат выдаётся», потом «Активен». Слова «DNS-01» нигде нет.


Стек

Бэкенд — Python, FastAPI, PostgreSQL. SQL пишем руками без ORM — это сознательное решение: понимаешь, что именно происходит и почему тормозит. Билдер на том же стеке. CLI — TypeScript, 4 зависимости, никакого bloat. Фронтенд контрол-плейна — React + Vite + Tailwind — деплоится через Layero.


Где мы сейчас

Платформа работает. Деплои идут, сертификаты выдаются, превью появляются за 30 секунд. CLI умеет одной командой задеплоить локальную папку и проходит логин по device-flow — токен забирается из браузера через бэкенд, без локального HTTP-сервера, поэтому работает даже из песочниц AI-агентов (Cursor, Claude Code). В работе: параллельные сборки и поддержка серверного рендеринга (Next.js, SvelteKit с серверной частью).

Если есть проект, который нужно задеплоить в России, — попробуйте: app.layero.ru