Переход с Node.js на Bun в продакшене: как мы реально мигрировали сервис без остановки API
Когда Bun только появился, он выглядел как очередной “быстрый Node.js”. Обычно такие проекты быстро исчезают из поля зрения. Но в какой-то момент становится понятно, что Bun - это уже не эксперимент, а реальный runtime, который можно поставить рядом с Node.js в продакшене.
И вот тут возникает нормальный инженерный вопрос: можно ли вообще заменить Node.js, который уже обслуживает трафик, на Bun так, чтобы ничего не упало.
Я разберу именно такой сценарий - не “hello world”, а живой backend под нагрузкой.
Как выглядел исходный сервис
Допустим, у нас обычный backend на Node.js. Ничего экзотического: Express, несколько API-роутов, база данных и nginx перед ним.
import express from "express";
const app = express();
app.get("/health", (req, res) => {
res.json({ status: "ok" });
});
app.get("/api/data", async (req, res) => {
res.json({ source: "node", data: "hello" });
});
app.listen(3000, () => {
console.log("Node.js service running on :3000");
});Он уже в продакшене, и у него есть трафик. То есть это не лаборатория, где можно “перезапустить и посмотреть”.
Первое, что ломает иллюзию “простой замены”
Инстинктивно кажется, что можно просто взять и запустить это через Bun:
bun run server.jsИ иногда это действительно работает.
Но в реальном проекте почти сразу всплывает другая картина: часть зависимостей ведёт себя по-другому, какие-то npm пакеты начинают ругаться, а где-то поведение stream или файловой системы отличается настолько, что приложение вроде бы запускается, но работает не так, как ожидалось.
И вот в этот момент становится понятно, что это не “замена Node.js”, а отдельный runtime со своими особенностями.
Как нормальные миграции выглядят в реальности
Самая большая ошибка - пытаться заменить runtime сразу.
В продакшене никто так не делает, потому что это гарантированный риск даунтайма.
Правильная стратегия всегда одинаковая: Node.js остаётся работать, а Bun поднимается рядом, как второй сервис.
Поднимаем Bun рядом с Node.js
Мы просто переносим тот же API, но уже на Bun runtime.
import { serve } from "bun";
serve({
port: 3001,
fetch(req) {
const url = new URL(req.url);
if (url.pathname === "/health") {
return Response.json({ status: "ok", runtime: "bun" });
}
if (url.pathname === "/api/data") {
return Response.json({ source: "bun", data: "hello" });
}
return new Response("Not found", { status: 404 });
},
});И запускаем:
bun run server.tsНа этом этапе у тебя уже две системы:
Node.js живёт на 3000
Bun живёт на 3001
И самое важное - старый сервис всё ещё обслуживает прод.
Переключение происходит не в коде, а в инфраструктуре
Вся магия миграции на самом деле происходит в nginx.
Сначала он вообще ничего не знает про Bun и продолжает отправлять весь трафик в Node.js:
location / {
proxy_pass http://127.0.0.1:3000;
}А потом появляется первый аккуратный шаг - выделяем часть маршрутов под Bun:
location /api/bun/ {
proxy_pass http://127.0.0.1:3001/;
}И вот это момент, который обычно недооценивают. Ты не “переписываешь систему”, ты начинаешь делить трафик между двумя runtime-ами.
Что начинает ломаться (и это нормально)
Самое интересное начинается не в момент запуска, а в момент реальной нагрузки.
Первое, что обычно всплывает - зависимости. Некоторые npm пакеты просто не дружат с Bun. Особенно те, что завязаны на низкоуровневые Node.js API или native модули.
Вторая вещь - различия в файловой системе и stream API. Где-то код работает одинаково, а где-то появляются неожиданные расхождения в поведении.
И третье - это неочевидные edge cases, которые в Node.js “как-то работали”, а в Bun начинают вести себя честнее, но неожиданнее.
Как выглядит продакшен-запуск
Когда всё более-менее стабильно, Bun обычно заводят как systemd-сервис.
[Unit]
Description=Bun API
After=network.target
[Service]
WorkingDirectory=/var/www/app
ExecStart=/root/.bun/bin/bun run server.ts
Restart=always
[Install]
WantedBy=multi-user.targetИ дальше система уже живёт как два параллельных сервиса.
Самый важный момент миграции
Если отбросить все технические детали, суть миграции очень простая:
Ты не “переписываешь Node.js на Bun”.
Ты постепенно проверяешь, может ли Bun взять часть нагрузки без деградации системы.
И только после этого принимается решение - оставлять ли его полностью или нет.
Когда Bun действительно выигрывает
В реальности Bun чаще всего выигрывает не “скоростью API”, а тем, что упрощает инфраструктуру:
быстрее стартует приложение
быстрее ставятся зависимости
проще dev-цикл
меньше инструментов вокруг
Но это не магическая замена Node.js. Это просто более новый runtime, который хорошо работает в определённых сценариях.
Финальный вывод
Самая здоровая стратегия миграции выглядит так:
Node.js остаётся стабильной основой
Bun подключается рядом
трафик переводится постепенно
и только потом принимается решение
И это, пожалуй, главный инсайт, который редко пишут в статьях про Bun.
Комментарии
Чтобы оставить комментарий, войдите в аккаунт.