Workerd и celld: self-hosting Workers на своём сервере

Самое частое, что люди слышат про Cloudflare Workers, — «зато привязывает». Приложение написано под Workers, Durable Objects устроены не как у всех. Эта статья — про то, почему именно сделка Cloudflare+Deno эту претензию ломает, а не укрепляет. На выходе: рантайм Workers открыт (это было и раньше), а то, что делало модель реально мощной — распределённые Durable Objects — теперь можно запустить на своём железе, без облака, без чужого аптайма и без чужих тарифов. Инструмент называется celld, и куплен он, собственно, ради этого.

Разберём по-честному: что такое workerd и в чём была его дыра, чем celld её закрывает и как устроены «ячейки» (cells), флот и бакет; как это физически запустить и почему тут нужны два порта и приватная сеть; что из этого действительно переносимо между Cloudflare, workerd и celld (включая SSR-фреймворки), а что — нет; и главное для нашего читателя с PHP+VPS — когда вам это надо, а когда вы правильно сделаете, что не тронете. В финале — что конкретно обещает сделка и чего она пока не даёт (статус «ещё строится»).

Терминологию держим как в первой статье серии: «воркер» здесь — серверный обработчик Workers, а не браузерный Service Worker из офлайн-статьи. Код и команды абстрактные (домены условные example.test, бакет my-cells); имена API, флаги и формат конфига — сверяйте по документации workerd и celld в день внедрения, это живые поверхности.


Часть I. workerd и дыра, из-за которой всё затеяли

1. Открытый рантайм был всегда — и это не маркетинг

Cloudflare выпустила рантайм Workers в open source под именем workerd (Apache-2.0). Важная деталь, которую Варда подчёркивает в анонсе сделки: это не параллельная реализация «для картинки», а тот же код, на котором работает их прод. То есть «я могу поднять Workers-совместимый сервер у себя» — не новая фраза 2026 года; она была верна и раньше.

workerd конфигурируется не wrangler.toml (это обёртка поверх него), а декларативным файлом на Cap’n Proto. Учебный скелет — чтобы видеть форму, а не чтобы копировать в прод (точные поля и имена берите из workerd.capnp в репозитории Cloudflare):

# config.capnp — учебный скелет, НЕ копия из доки; сверяйте поля по src/workerd/server/workerd.capnp
using Workerd = import "/workerd/workerd.capnp";

const config :Workerd.Config = (
  services = [
    ( name = "main",
      worker = (
        handlers = ["fetched"],                 # экспорт fetch воркера
        compatibilityDate = "2026-11-06",
        esModule = embed "./dist/index.js"      # собранный бандл воркера
      )
    )
  ],
  sockets = [
    ( name = "http", address = "*:8080", http = (), service = "main" )
  ]
);

Здесь видно главное отличие от wrangler.toml из статьи про переезд: ресурсы (env-биндинги) в workerd описываются внутри самого конфига как сервисы — бакеты, неймспейсы, Durable Objects, — а не живут на стороне платформы. Это и есть «сам себе платформа».

2. Одна строка, которую вендор сам признал

Если бы workerd закрывал задачу целиком, никакой покупки не нужно было бы. Но Варда пишет прямым текстом (совместный анонс, 09.10.2026):

«workerd has always had one big gap in its production-readiness: Durable Objects. Workerd implements them, but only in a single-instance way, good enough for local testing, but unable to scale.»

Раскрутим, что тут болит. Durable Object — это stateful-сущность: у неё своя SQLite, живые WebSocket, однопоточное исполнение. Чтобы модель масштабировалась, сотни тысяч DO должны быть разбросаны по разным машинам, а платформа — знать, на какой машине живёт конкретный объект, дорутить туда запрос и не дать двум машинам одновременно писать в одну ячейку. В прод-Cloudflare это делает их собственная (тяжёлая, «зверь») инфраструктура распределения. В открытом workerd её не было: один экземпляр рантайма — один набор DO локально. Хорош для wrangler dev и тестов — бесполезен как продакшн-кластер. Именно поэтому, по словам самого Варды, «not a lot of people seem to have any idea that this is even possible» и «few have [used it in production]».

Ключевая мысль серии в этой точке: дыра была не в языке и не в модели — дыра была в распределении состояния. И закрыл её не Cloudflare, а посторонний проект, который они купили.


Часть II. celld: распределённый DO у вас на серверах

1. Что это вообще такое

celld — open-source демон от команды Deno (Apache-2.0), выпущенный в августе 2026 и представленный Далом 5 августа; 9 октября он стал «семейным» проектом Cloudflare. Официальное определение из README репозитория:

«celld is an open-source daemon that runs a Cloudflare Workers application on your own machines: Workers, Durable Objects, KV, Queues, D1, R2, Workflows, Cron Triggers, and static assets, deployed from the wrangler.json that you already have.»

Две вещи в этом предложении важнее всего для миграции:

  • всё то же приложение, что для Cloudflare — включая весь набор примитивов, а не только «воркеры»;
  • из того же wrangler.json — тот конфиг, что вы пишете для деплоя на облако, здесь и есть источник. Промежуточный слой Cap’n Proto из Части I celld сам генерит; руками .capnp писать не надо.

2. Ячейка, флот и бакет — три вещи, из которых собрано распределение

celld переводит абстракцию DO («маленький адресуемый сервер со своей базой», см. первую статью) на язык операций на своём железе. По README:

  • Ячейка (cell) = именованный сервер со своей SQLite-базой. Это ваш Durable Object. (Аналогия из README: «Each object is a cell: a named server with its own SQLite database.»)
  • Узел (node) = один процесс celld на одной машине. Узел встраивает V8 и исполняет бандлы wrangler.
  • Флот (fleet) = узлы, разделяющие один бакет. В бакете лежат деплой, состояние ячеек и маленькие «ownership-записи» (кто владеет).

Кольцевой вопрос распределённого stateful-выполнения — «кто гарантирует, что в один объект пишет только один исполнитель». Классика отвечает: consensus (Raft/Paxos), membership-протокол, failure-detector. celld отвечает отказом от всего этого (README дословно): «It needs no serving control plane or consensus service… The fleet needs no membership protocol, failure detector, or consensus service.» Как это достигается:

«A conditional bucket write gives a node the ownership of a cell, so exactly one node owns a cell at a time.»

То есть lease берётся условной записью в бакет (uslovie «запиши, только если текущее значение = X», CAS-подобно). Победивший узел — эксклюзивный владелец ячейки. Протух lease — другой узел перезаберёт. Никакого отдельного ZooKeeper/etcd — бакет и есть координатор. Это прямое продолжение идеи Дала «один бинарник, внешний единственный dependency — object storage»: минимум движущихся частей вместо облачного зоопарка, который он насмотрелся в Deno Deploy.

3. Почему состояние переживает замену машины

Ещё одна неочевидная деталь из README, важная для доверия: перед тем как перенять (takeover) ячейку другим узлом, celld восстанавливает «открытый лог» от прежнего владельца, чтобы бакет держал долгосрочное состояние, а узлы оставались заменяемыми («the bucket holds the long-term state and nodes stay replaceable»). Коммитнутые SQLite-записи снимаются в LTX-формате репликации и уходят в бакет (или, во флоте из 2+ узлов, быстрее — через peer-передачу, см. Часть III §4). Для читателя с VPS смысл простой: железо одноразовое, состояние — в бакете; упала машина — ячейка переехала, данные целы. Это ровно то, чего не даёт «просто workerd single-instance».


Часть III. Как это запустить: dev, деплой в бакет и два порта

Здесь — практическая часть self-hosting’а. Флаги взяты с README celld и приведены с условными адресами; полный набор — celld --help, и он двигается по релизам.

1. Локально — вообще без облака и бакета

celld dev

Одна команда поднимает один узел с локальным object store — ни Docker, ни облачный бакет не нужны. Worker-слушатель — на http://127.0.0.1:9876, состояние — в папке .celld/dev, которая переживает рестарт (данные durable). Это ваш «wrangler dev», только честнее: то, что вы гоняете локально, — тот же механизм, что пойдёт во флот.

Практические ловушки, которые README выносит явно:

  • .celld/dev не мигрирует при смене конфига — может остаться значение, которое новый конфиг отвергает, и ошибка выглядит не связанной с изменением. Лечится celld dev --clean (снести локальное состояние и стартовать с чистого).
  • celld dev следит за проектом и пересобирает после правки кода/конфига; при провале сборки оставляет текущее приложение работать. Для сборки Worker-кода нужен esbuild в PATH (asset-only проектам не нужен).

2. Продакшн: деплой, потом запуск узла против бакета

# 1) собрать и положить приложение в бакет
celld deploy . \
  --bucket s3://my-cells

# 2) запустить узел против того же бакета
celld \
  --bucket s3://my-cells \
  --listen 0.0.0.0:8080 \
  --internal-listen 10.0.0.12:8081 \
  --advertise node-a.internal:8081

Четыре флага — и в них вся модель:

  • --bucket s3://… — координатор и хранилище. gs:// = Google Cloud Storage, az:// = Azure Blob; для другого S3-совместимого — --endpoint (+--region, если не выводится). Рантайм использует стандартную цепочку AWS-креденшелов — ваши права на бакет = права админа флота (README прямо это говорит, см. §4).
  • --listen 0.0.0.0:8080 — публичный Worker-порт; его наружу через балансировщик.
  • --internal-listen 10.0.0.12:8081 — приватный operator-порт (peer HTTP + API).
  • --advertise node-a.internal:8081 — как этот узел видят другие узлы флота.

Модель сети — то, что отличает рабочий self-host от дырявого. Выносим отдельно.

3. Два порта и приватная сеть: обязательное условие

Peer-трафик и operator-API идут по внутреннему слушателю (8081). README формулирует требование жёстко:

«Put every advertised address on a trusted private network or an encrypted overlay such as WireGuard or Tailscale, and do not publish the internal port. celld rejects a literal public IP unless you supply --unsafe-public-advertise.»

То есть: advertise-адреса — только в доверенной сети или поверх шифрованного overlay (WireGuard/Tailscale), внутренний порт наружу не публикуется. Печатать --unsafe-public-advertise — красный флаг, а не решение. Для читателя с VPS это ровно та же гигиена сегментации, что разбирается в защите периметра и в обзоре systemd-сервисов/сети: публичная граница и приватная граница управления не должны совпадать. Заодно — доступ к бакету и его креденшелам равен доступу администратора флота; отсюда требование из Части IV про то, что бакет надо беречь как приватный ключ, а не как обычный диск.

4. Флот = одно приложение; узлы сами находят друг друга

Важное ограничение, чтобы не строить ложную картину:

«A fleet runs one application, and every node loads its latest successfully committed deployment from deploy/current.json.»

Один флот — одно приложение; узлы обнаруживают владельцев и пиров через leases в бакете, «there is no account or join service» (нет отдельного сервиса присоединения). Смешанные версии нельзя держать в одном serving-флоте (апгрейд: остановить все старые узлы, поднять новые с тем же конфигом; формат «wake» мигрируется на старте).

И численная часть, которая реально меняет решение «ставлю один узел или два» (README, с их замерами):

  • Один узел: каждая запись проходит через бакет → одна storage-операция на запись, это медленно. Их цифры: ~90 мс на region-local-хранилище, ~600 мс на нерокальном бакете в нагруженном стенде.
  • Два+ узла: владелец передаёт запись второму узлу, и она считается durable, как только тот держит её на своём диске; в бакет уходит позже. Их замер: ~25 мс при втором узле, держащем запись.

Вывод для планирования: один узел self-host «для экономии» — это ловушка производительности для всего, где есть запись в DO/KV/D1. Второй узел не требует доп.конфига («a node finds the other nodes through the bucket»), но требует второй машины. Это ровно тот компромисс, который облако делает за вас незаметно.

5. Диагностика и операторские команды

Не гадайте о состоянии флота руками: celld diagnose --bucket s3://my-cells перебирает node-leases и делает подписанный прямой зонд каждого живого пира, различая истёкшие записи, некорректные/небезопасные advertise-адреса, недоступных пиров и несовместимые протоколы, плюс печатает owned/resident cells, WebSocket, RSS, CPU, FD, pressure, shedding. Рядом — celld cell list, celld d1 migrations apply …, celld kv bulk put … (формат Wrangler-экспорта, чтобы экспорт с облака мигрировал напрямую), celld r2 … (читает бакет без запущенного узла), celld queue pause/resume. Это то, чего обычно нет у «голого» open-source рантайма: слой операторских инструментов, а не только исполнителя.


Часть IV. Насколько модель правда переносима (и SSR, и пределы)

Здесь — сердцевина тезиса серии: «поворот от Cloudflare». Проверим его на фактах, а не на лозунге.

1. Один fetch-хендлер — три рантайма

Формат, на котором держится переносимость, вы уже видели в статье про переезд: приложение Workers — это export default { fetch(request, env, ctx) }. Тот же fetch-контракт понимают Deno.serve и Bun.serve, и многие SSR-фреймворки отдают один и тот же хендлер, а «адаптер» лишь оборачивает его под конкретный рантайм. Схематично (в виде до фреймворка):

// один и тот же модуль — вход и для Cloudflare, и для workerd, и для celld
import { createFetchHandler } from "@myapp/server";

export default {
  fetch: createFetchHandler(),   // React SSR / SvelteKit / Nuxt отдаётся отсюда
};

Почему это «правда», а не обещание, видно по тому, что делает сам Варда в анонсе: он признаёт, что workerd открыт именно как escape-hatch («we open-sourced it… former customers migrated away using workerd»), а celld называет угрозой мифу о lock-in — и отвечает на неё не запретом, а намерением слить celld в workerd, чтобы self-hosting стал «first-class». То есть дверь наружу — часть официальной стратегии, а не случайная щель. Для SSR-фреймворков это означает: React/SvelteKit/Nuxt, развёрнутые под Workers-адаптер, не обязаны оставаться на облаке — тот же бандл едет на ваш celld-флот. (Точные имена адаптеров и поддержки — по документации конкретного фреймворка: у SvelteKit — adapter-cloudflare, у Nuxt — Cloudflare-пресет; это не константа, сверяйте.)

2. Что переносится один-в-один, а что — нет

Честная таблица (основана на описании celld и анонсах Cloudflare/Deno; колонка «предел» — то, что вы теряете или должны достроить, уходя с облака):

Примитив WorkersВ celld self-hostЗаметка/предел
Workers (fetch), service bindings, JS RPC, Node.js compat✅то же приложение из wrangler.json
Durable Objects (SQLite, alarms, hibernating WS)✅ распределённовот ради чего celld; на голом workerd — только single-instance
KV, Queues, D1, R2, Workflows, Cron, статика✅KV/D1/Queue/Workflow — тоже «ячейки», те же lease/репликация/failover
Static assets, _headers/_redirects✅asset-only или рядом с Worker
Containers/sandbox⚠ experimental«экспериментально», не для прода-ставки
Cloudflare Network (PoP-глобальность, их Anycast)❌«out of scope: product that needs the Cloudflare network»
GPU / browser-ферма (Workers AI инференс, Browser Rendering)❌тоже out of scope
их managed-безопасность/DDoS, WAF-периметр❌на self-host это ваша задача (файрвол, лимиты)

Строка, которую стоит держать в уме при разговоре о «AI на edge»: само-собой на celld инференс-модели не переезжает — GPU-класс (Workers AI) выведен из области. «AI на edge» в смысле сделки — это обвязка агента (состояние, очереди, WebSocket — про это первая статья, Часть V), а не «веса модели на краю». Если же цель — модель думать локально, это отдельная линия, и у вас она уже разобрана с другой стороны (Ollama на своём VPS, песочница LocalNode).

3. Что всё-таки привязывает (чтобы не врать)

Переносима модель, но не автоматом экосистема. Уходя на celld, вы забираете рантайм и примитивы, но:

  • глобальности PoP не получаете — она была сервисом Cloudflare, а не свойством формата; у вас будет то число регионов, сколько машин вы поставите (физика расстояния — в CDN-статье);
  • операционную нагрузку перекладываете на себя: бакет как админ-доступ, приватная сеть, два узла ради латентности записи, балансировка ownership (celld балансирует сам, но мониторинг — ваш);
  • часть Cloudflare-специфичного (их managed-базы поверх DO, AI, Browser) просто отсутствует.

Поэтому честная формулировка серии: сделка увеличивает переносимость Workers (выход, которого не было, теперь есть и официально финансируется), но не превращает Workers в «такой же переносимый, как nginx+PHP». У вашего стека переносимость исторически другая и очень высокая — см. Часть V.


Часть V. Нужен ли self-host Workers вам, у кого WordPress + PHP + VPS

Прямой ответ читателю сайта, как в первой статье, но теперь про self-host, а не про облако.

1. Когда вам это точно не надо

Если ваш прод — WordPress на своём VPS с PHP-FPM и MySQL (слои конфигурации, анатомия VPS), то вы уже «на self-host», просто в другом стеке: у вас состояние в MySQL, контроль на месте, данные не уходят третьей стороне, а приложение переносимо между серверами «scp + дамп БД» (миграция между серверами). Ставить рядом celld ради «модельности» — нет повода: вы платите новой экосистемой (Node/bundler, бакет-админ, peer-сеть) за то, что у вас уже есть дешевле. Аргумент первой статьи «свой VPS часто остаётся под контрольнее и дешевле» здесь усиливается: self-host Workers — это второй self-host рядом с первым.

2. Когда присмотреться

  • Реалтайм/stateful, которого WordPress не выносит — чат, живой редактор, счётчики, совместное редактирование. Абстракция «ячейка = адресуемый сервер с SQLite и живым сокетом» (Часть II) снимает ровно ту головную боль, ради которой на LAMP-стеке городят отдельный сокет-сервер + Redis-состояния + шардинг (SSE/WebSocket, очереди, монолит vs микросервисы). И celld даёт это у вас, а не на чужом облаке — то есть приватность состояния не теряется при переезде на новую модель.
  • Приватность «данные не уходят к вендору» как осознанный выбор — та же линия, что Ollama вместо облачного инференса: если вы ушли с облачных моделей на свои, потому что не хотите отправлять данные наружу, celld — логичный ответ на «хочу ту же Workers-модель, но тоже у себя».
  • Портативность по контракту, а не по вкусу к Docker — вы строите что-то, что должно бежать и у клиента в его VPS, и у вас, и на облаке, с одним артефактом. Тогда portable-воркер из Части IV — удобный ответ; смежно с контейнерами и оркестрацией, но уровень абстракции выше образа.

3. Честная цена входа

Даже в «когда надо» знайте счёт. Чтобы self-host Workers был production, а не игрушкой, из Части III складывается: минимум два узла (иначе записи по 90–600 мс), приватная сеть/overlay для internal-порта, S3-совместимый бакет под руками (с правами уровня админа флота), esbuild-тулчейн и готовность самим отвечать за то, за что на облаке отвечает их SRE. Это не «docker run и забыли». Для одного чат-виджета это может быть избыточно; для продукта, где реалтайм и приватность — ядро, — окупаемо.

4. Что делать прямо сейчас

Ничего срочного (как и в первой статье). Проверьте гипотезу на себе: есть ли у вас stateful-реалтайм, который сейчас кривится на PHP-стеке? Если нет — держите celld в прицеле как «модель про переносимое серверное JS», не как задачу на неделю. Если да — начните с celld dev на свободной машине с одним чат-DO, замерьте латентность записи на одном узле, потом поднимите второй и сравните: именно разница 600↔25 мс (их цифры, ваши будут свои) объясняет всю архитектуру флота лучше любого обзора.


Часть VI. Что конкретно даёт сделка и чего она пока не даёт

1. Намерение — слить, а не хоронить

Соберём разбросанное по анонсам в одну рамку. Из Cloudflare-поста (Варда, 09.10.2026):

«Ryan and Bert will be leading a new effort to make workerd self-hosting a first-class supported way to build and run apps using the Workers programming model. This will involve merging code and ideas from celld back into workerd.»

То есть план — не «поддержать чужой celld как побочный проект», а соединить celld и workerd в один поддерживаемый self-hostable рантайм. Дал в том же анонсе довершает картину: цель — «make this programming model the default way to build servers, whether you run on Cloudflare’s network or your own infrastructure», и про агентов: Durable Objects хороши для «agent harnesses» (дешёвое исполнение + персистентное состояние + WebSocket). Смысл всей этой серии именно в этом развороте: покупка увеличивает выход наружу, а не закрывает его.

Обе лицензии — Apache-2.0 (и workerd, и celld): разрешительная (permissive), допускает и использование, и модификацию, и релиз под другими условиями при сохранении уведомлений. Юридически «унести и поставить у себя» — не серая зона, а то, на что лицензия прямо даёт право.

2. Чего пока нет — и почему это важно проговорить

Зрелость надо мерить текущим состоянием, а не слайдом:

  • Единого сливного рантайма ещё нет. «We’ll have more announcements about this work in the coming months» — обещание в пути. Сегодня self-host Workers — это два разных инструмента с разным покрытием (голый workerd = одиночные DO; celld = распределённые DO, но независимый код), а не один вендорский «коробочный» продукт.
  • celld намеренно закрывает pull-requests («Pull requests are disabled. Coding agents make it too easy to send a large, low-context change…»; патчи — письмом на ry@deno.com). Это выбор жёсткого маленького контура мейнтейнмента, не признак заброшенности — но вы как оператор должны понимать: канал влияния на roadmap узкий, а CLA переводит права на патч на правообладателя.
  • Продакшн-зрелость self-hosting’а — не «коробочная». Нет вендорского SLA за ваш флот, нет их SRE за распределением, часть сервиса out of scope (сеть/GPU/browser). Это то, что скептики имели в виду: «дверь обещана, но кирпич ещё строится».

Значит для читателя: «вендор сам строит выход» — факт и он хороший; «выход готов и обслуживается как прод Cloudflare» — пока нет. Оба предложения надо держать одновременно, иначе серия продаст ту же наивность с другой стороны.

3. Как это стыкуется с остальной серией

  • Про что именно купили и три пути для живых на Deno — первая статья (вы уже там или пришли оттуда).
  • Про механический перенос приложения с Deno Deploy на Workers (тот же wrangler.json как вход) — вторая статья; после неё self-host — это просто «другой адрес, куда этот же бандл едет».
  • Про примитивы Workers с нуля (KV/D1/R2) — четвёртая статья серии; здесь мы их лишь перечислили как «то, что celld тоже исполняет».
  • Про итоговое сравнение edge / serverless / свой VPS с цифрами выбора — пятая статья: именно она превращает «я тут про self-host Workers прочитал» в «и вот почему для моего проекта я взял(а) VPS/облако/гибрид».

Инструменты под задачу

  • Собрать wrangler.json под и облако, и self-host / разобрать .capnp-скелет — AI-ассистент для кода на codestack.space: вставьте свой конфиг воркера и попросите показать, какие биндинги celld поддержит напрямую, а какие (сеть/GPU/AI) придётся вынести на облако или достроить самому.

Чек-лист граблей

  1. Путать workerd и celld как «один и тот же self-host». workerd открыт давно, но DO в нём single-instance (дыру признал сам Cloudflare). Распределение — это celld. Говорить «workerd = масштабируемый self-host Workers» — неверно по состоянию сегодня.
  2. Ставить один узел и ждать быстрой записи. Один узел = каждая запись через бакет (их замер ~90–600 мс). Второй узел (та же конфигурация, пир находится через бакет) даёт ~25 мс. Один узел — это dev-режим, не прод-экономия.
  3. Публиковать internal-порт (8081). Peer HTTP и operator-API идут туда; README требует держать advertise в доверенной/шифрованной сети (WireGuard/Tailscale) и не публиковать этот порт. --unsafe-public-advertise — красный флаг.
  4. Относиться к бакету как к обычному диску. Доступ к бакету и креденшелам = доступ администратора флота (там leases и состояние ячеек). Их права и защита — уровня приватного ключа.
  5. Ждать, что «AI на edge» переедет вместе с кодом. GPU-класс (Workers AI, Browser Rendering, сама сеть Cloudflare) — out of scope celld. «AI на edge» в сделке — про состояние агента (DO), не про инференс на краю; локальный инференс — другая линия (Ollama/LocalNode).
  6. Считать переносимость тотальной. Переносится модель и примитивы (fetch-хендлер, KV/D1/R2/DO), не глобальность PoP и не managed-безопасность. Глобальность = число ваших регионов; DDoS/WAF — ваша задача.
  7. Держать смешанные версии в одном serving-флоте. Нельзя; порядок апгрейда — остановить все старые узлы, поднять новые с тем же конфигом (формат wake мигрируется на старте).
  8. Ставить на «уже готовый прод от вендора». Слияние celld→workerd — намерение в пути, не релиз. Продакшн-зрелость self-hosting’а Workers сегодня = ваша операционная ответственность + независимый celld, без вендорского SLA.

👤 Зарегистрируйтесь, чтобы добавить материал в закладки.