Railway

Deploy celld-on-railway

celld.dev (open source impl of Durable Objects + workers) on Railway

Deploy celld-on-railway

Just deployed

/var/lib/celld

celld-fleet-bucket

Bucket

Just deployed

Deploy and Host celld with Railway

celld is a self-hosted runtime for Cloudflare Workers and Durable Objects.

This Railway template deploys a single-node celld fleet with:

  • the official ghcr.io/denoland/celld image
  • a Railway Storage Bucket for durable fleet state
  • a Railway Volume for node-local working state
  • Public Networking for Worker HTTP traffic
  • Private Networking for celld peer/control traffic

A cell is celld's equivalent of a Durable Object: a named stateful object with private SQLite-backed state, HTTP handling, alarms, WebSockets, and durable persistence.


About Hosting celld

A celld fleet consists of one or more runtime nodes connected to the same object-storage bucket.

The bucket is the durable source of truth for:

  • Worker deployments
  • Durable Object / cell SQLite replicas
  • ownership records
  • node leases
  • fleet metadata
  • peer authentication material

The Railway service runs the Worker runtime and keeps active cells in memory. Inactive cells do not require dedicated containers; they are restored from durable storage when they receive work.

Internet
   │
   ▼
Railway HTTPS
   │
   ▼
celld-node-1:8080
   │
   ▼
Worker runtime
   │
   ├── Cell:alice → SQLite
   ├── Cell:bob   → SQLite
   └── Cell:room  → SQLite
            │
            ▼
   Railway Storage Bucket

The template creates:

Railway Project
├── celld-node-1
│   ├── public listener :8080
│   ├── private listener :8081
│   └── volume at /var/lib/celld
└── celld-fleet-bucket

The node runs approximately:

celld \
  --bucket "$CELLD_BUCKET_NAME" \
  --endpoint "$S3_ENDPOINT" \
  --region "$AWS_REGION" \
  --listen 0.0.0.0:8080 \
  --internal-listen 0.0.0.0:8081 \
  --advertise "$RAILWAY_PRIVATE_DOMAIN:8081"

Port 8080 serves public Worker traffic. Port 8081 is only for celld peer/operator traffic and should remain private.

The node advertises itself over Railway Private Networking as:

celld-node-1.railway.internal:8081

Why Deploy celld on Railway?

Railway provides the main infrastructure primitives celld needs in one place:

  • container hosting
  • S3-compatible Storage Buckets
  • persistent volumes
  • public HTTPS ingress
  • private service networking
  • horizontal and vertical scaling
  • reference variables for automatic storage wiring

This template connects those pieces into a ready-to-use celld fleet, so you can focus on writing Workers and Durable Objects rather than assembling the underlying infrastructure yourself.


Common Use Cases

celld works well for applications that naturally divide into named, stateful units:

  • AI agents — one cell per agent
  • chat rooms — one cell per room
  • multiplayer sessions
  • collaborative documents
  • per-user or per-tenant state machines
  • workflow orchestration
  • WebSocket applications
  • scheduled background work using alarms

Creating many cells does not create many Railway services. Thousands of logical Durable Objects can run within the same celld fleet.


Dependencies for celld Hosting

This template uses:

  • celld — Worker and Durable Object runtime
  • Railway Service — runs the celld container
  • Railway Storage Bucket — durable fleet state
  • Railway Volume — node-local working state
  • Railway Public Networking — routes HTTP traffic to port 8080
  • Railway Private Networking — carries internal celld traffic on port 8081

The bucket is wired automatically using Railway reference variables:

AWS_REGION=${{celld-fleet-bucket.REGION}}
S3_ENDPOINT=${{celld-fleet-bucket.ENDPOINT}}
AWS_ACCESS_KEY_ID=${{celld-fleet-bucket.ACCESS_KEY_ID}}
AWS_SECRET_ACCESS_KEY=${{celld-fleet-bucket.SECRET_ACCESS_KEY}}
CELLD_BUCKET_NAME=${{celld-fleet-bucket.BUCKET}}

The Railway Volume is mounted at:

/var/lib/celld

with:

CELLD_WATCH=/var/lib/celld/state

A reasonable default Worker pool size is:

CELLD_WORKERS=4

The bucket is the durable source of truth. The volume is node-local working state.


Quickstart: Deploy a Durable Object

1. Install celld

curl -fsSL https://celld.dev/install.sh | sh
npm install -g esbuild

2. Create a Worker project

my-agents/
├── wrangler.jsonc
└── index.js

wrangler.jsonc:

{
  "name": "my-agents",
  "main": "index.js",
  "compatibility_date": "2026-08-24",
  "durable_objects": {
    "bindings": [
      {
        "name": "AGENTS",
        "class_name": "Agent"
      }
    ]
  },
  "migrations": [
    {
      "tag": "v1",
      "new_sqlite_classes": ["Agent"]
    }
  ]
}

index.js:

export class Agent {
  constructor(state, env) {
    this.state = state;
  }

  async fetch(request) {
    if (request.method === "POST") {
      const { message } = await request.json();

      const messages =
        (await this.state.storage.get("messages")) ?? [];

      messages.push({
        message,
        timestamp: Date.now()
      });

      await this.state.storage.put("messages", messages);

      return Response.json({ messages });
    }

    const messages =
      (await this.state.storage.get("messages")) ?? [];

    return Response.json({ messages });
  }
}

export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    const [, resource, agentId] = url.pathname.split("/");

    if (resource !== "agents" || !agentId) {
      return new Response("Use /agents/:agentId", {
        status: 404
      });
    }

    const id = env.AGENTS.idFromName(agentId);
    return env.AGENTS.get(id).fetch(request);
  }
};

The important line is:

env.AGENTS.idFromName(agentId)

The same name always maps to the same Durable Object, so /agents/alice and /agents/bob are independent cells with separate persistent state.


Deploy Your Worker

From celld-fleet-bucket in Railway, obtain:

BUCKET
ENDPOINT
REGION
ACCESS_KEY_ID
SECRET_ACCESS_KEY

Set them locally:

export CELLD_BUCKET=""
export S3_ENDPOINT=""
export AWS_REGION=""
export AWS_ACCESS_KEY_ID=""
export AWS_SECRET_ACCESS_KEY=""

Then deploy:

celld deploy . \
  --bucket "$CELLD_BUCKET" \
  --endpoint "$S3_ENDPOINT" \
  --region "$AWS_REGION"

celld deploy is roughly equivalent to wrangler deploy, except the deployment is written to your fleet bucket.

After deploying a new Worker version, restart or redeploy celld-node-1 so it loads the new application version.


Call Your Durable Object

Assume your Railway domain is:

https://your-service.up.railway.app

Write state to alice:

curl -X POST \
  https://your-service.up.railway.app/agents/alice \
  -H 'content-type: application/json' \
  -d '{"message":"Research distributed databases"}'

Read it back:

curl \
  https://your-service.up.railway.app/agents/alice

Create another cell:

curl -X POST \
  https://your-service.up.railway.app/agents/bob \
  -H 'content-type: application/json' \
  -d '{"message":"Build a landing page"}'

You now have:

Agent:alice → private SQLite state
Agent:bob   → private SQLite state

Both run inside the same celld fleet. No additional Railway services are created.


Durable Background Work

Cells can schedule future work with alarms.

export class Agent {
  constructor(state) {
    this.state = state;
  }

  async fetch(request) {
    const { task } = await request.json();

    await this.state.storage.put("task", task);
    await this.state.storage.put("step", 0);
    await this.state.storage.setAlarm(Date.now() + 5000);

    return Response.json({ status: "started" });
  }

  async alarm() {
    let step =
      (await this.state.storage.get("step")) ?? 0;

    step++;
    await this.state.storage.put("step", step);

    if (step < 10) {
      await this.state.storage.setAlarm(Date.now() + 5000);
    }
  }
}

This lets a cell persist progress, become idle, and resume later without keeping a dedicated process alive.


Scaling

A cell is not a Railway container.

agent-1
agent-2
...
agent-100000

represents logical Durable Objects, not 100,000 services.

As load grows, additional celld nodes can connect to the same fleet bucket. Nodes discover each other through leases stored in the bucket.

A common architecture is:

one Worker application
        │
        ├── many Agent cells
        ├── many User cells
        └── many Room cells

Cloudflare → celld

Cloudflarecelld on Railway
Workercelld Worker
Durable Objectcell
DO bindingcelld Durable Object binding
idFromName()idFromName()
DO storagecell SQLite
wrangler deploycelld deploy
Workers URLRailway public domain
Cloudflare runtimeRailway celld node
Cloudflare storage/control planeRailway Storage Bucket

Current Constraints

celld is still an early-stage runtime.

Important current constraints:

  • one fleet runs one application deployment
  • port 8081 must remain private
  • bucket credentials are highly privileged
  • deploying requires bucket access
  • nodes may need a restart after a new Worker deployment

Template Content

celld-fleet-bucket

Bucket

More templates in this category

View Template
Garage S3 Storage
Ultra-light S3 server: fast, open-source, plug-and-play.

PROJETOS
8
View Template
Redis
Self Host Latest Redis with Railway

2
View Template
EasyImg
Simple self-hostable Nuxt.js personal image hosting system.

Muhammad Bilal
0