---
title: "Deploy celld-on-railway"
description: "celld.dev (open source impl of Durable Objects + workers) on Railway"
category: "Storage"
url: https://railway.com/deploy/celld-on-railway
---

# Deploy celld-on-railway

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

**[Deploy celld-on-railway on Railway](https://railway.com/template/celld-on-railway)**

- **Creator:** Arnav Gupta's Projects
- **Category:** Storage
- **Total deploys:** 1

## Template content

### celld-node-1 https://celld.dev/cells-mark.svg

- **Image:** ghcr.io/denoland/celld:latest
- **Start command:** `/bin/sh -c 'exec /usr/local/bin/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"'`
- **Health check:** /
- **Public domain:** Yes

## Buckets

- **celld-fleet-bucket**

## Documentation

# Deploy and Host celld with Railway

[celld](https://celld.dev/) 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.

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

The template creates:

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

The node runs approximately:

```bash
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:

```text
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:

```text
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:

```text
/var/lib/celld
```

with:

```text
CELLD_WATCH=/var/lib/celld/state
```

A reasonable default Worker pool size is:

```text
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

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

### 2. Create a Worker project

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

`wrangler.jsonc`:

```json
{
  "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`:

```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:

```js
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:

```text
BUCKET
ENDPOINT
REGION
ACCESS_KEY_ID
SECRET_ACCESS_KEY
```

Set them locally:

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

Then deploy:

```bash
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:

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

Write state to `alice`:

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

Read it back:

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

Create another cell:

```bash
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:

```text
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.

```js
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 &lt; 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.

```text
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:

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

---

## Cloudflare → celld

| Cloudflare | celld on Railway |
|---|---|
| Worker | celld Worker |
| Durable Object | cell |
| DO binding | celld Durable Object binding |
| `idFromName()` | `idFromName()` |
| DO storage | cell SQLite |
| `wrangler deploy` | `celld deploy` |
| Workers URL | Railway public domain |
| Cloudflare runtime | Railway celld node |
| Cloudflare storage/control plane | Railway 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


## Similar templates

- [Garage S3 Storage](https://railway.com/deploy/garage-s3-storage) — Ultra-light S3 server: fast, open-source, plug-and-play.
- [Redis](https://railway.com/deploy/redis-1) — Self Host Latest Redis with Railway
- [EasyImg](https://railway.com/deploy/easyimg) — Simple self-hostable Nuxt.js personal image hosting system.

Open this page in a browser: https://railway.com/deploy/celld-on-railway
