Railway

Deploy celld

Self-hosted distributed Durable Objects (Deno's celld) with its own bucket

Deploy celld

Just deployed

/var/lib/celld

celld-cells

Bucket

Just deployed

celld on Railway

One-click deploy of celld — self-hosted, distributed Durable Objects from the Deno team. celld runs Cloudflare Workers and Durable Objects on machines you control. Each object is its own SQLite database, replicated to an S3-compatible bucket; nodes coordinate through that bucket alone, with no control plane and no consensus service.

Deploy on Railway

What one click gets you

  • a celld node running the official ghcr.io/denoland/celld:0.0.2 image
  • a Railway Bucket (S3-compatible) created in your project — deployments, cell state, and fleet coordination all live there
  • storage credentials wired service→bucket entirely with reference variables (${{celld-cells.ACCESS_KEY_ID}} etc.) — zero prompts, zero secrets to paste, nothing to configure
  • a volume at /var/lib/celld for local cell state, a public domain, and a restart policy tuned for celld's boot behavior
internet ──► Railway edge (TLS) ──► celld node ──► celld-cells bucket
                                    (your service)   (source of truth)

After deploying

The node self-seeds: on first boot against the fresh empty bucket it auto-deploys a small welcome page, so the service URL shows "hello from celld" within a couple of minutes of clicking deploy — no setup.

Known Railway platform race (not a template bug): Railway sometimes creates the service before its bucket finishes provisioning, so the storage variables bake in empty (CELLD_BUCKET=s3://). Railway's own example bucket template has the same exposure; there is no ordering primitive in the template format. When it happens, this template's bootstrap prints a plain-English banner in the deploy logs:

celld template: storage variables are EMPTY … NOTHING IS BROKEN — just click Redeploy on this service.

One click on Redeploy re-resolves the variables and the welcome page appears. Verified end-to-end on real deploys (including via the actual marketplace deploy button).

Pushing your own app

celld loads applications from the bucket. Replace the welcome page whenever you're ready:

From your machine (Worker code needs esbuild on PATH; asset-only projects don't; celld accepts wrangler.jsonc only, it rejects wrangler.toml):

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

# grab your bucket credentials (Railway CLI, linked to the new project)
eval "$(railway bucket credentials --bucket celld-cells)"
export CELLD_BUCKET="s3://$AWS_S3_BUCKET_NAME"   # adjust to the printed names

# try the official example
git clone https://github.com/denoland/celld
cd celld/examples/counter
celld deploy . --bucket "$CELLD_BUCKET" --endpoint "$AWS_ENDPOINT_URL" --region auto

The crash-looping node picks the deployment up on its next automatic restart (within ~a minute). Open the service's Railway domain — your Worker is live.

Redeploys: as of celld 0.0.2, nodes load a deployment at startup only. After pushing a new version, restart the Railway service to serve it.

Check fleet health any time:

celld diagnose --bucket "$CELLD_BUCKET" --endpoint "$AWS_ENDPOINT_URL" --region auto

How the template is wired (for maintainers)

The template was generated programmatically from a verified live project (templateGenerate via the public GraphQL API). Key decisions, all empirically verified on Railway:

piecevaluewhy
start command/usr/local/bin/celld --listen [::]:8080Railway execs the start command directly (no shell, entrypoint discarded) — full binary path required. [::] binds IPv6+IPv4 so the edge and private network both reach it
CELLD_BUCKETs3://${{celld-cells.BUCKET}}bucket reference — each deployer's copy points at their own bucket
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / S3_ENDPOINT / AWS_REGION${{celld-cells.*}}same; REGION tracks whatever region the deployer's bucket lands in
CELLD_ADVERTISE${{RAILWAY_PRIVATE_DOMAIN}}:8080peers reach the node over the project-private network only
CELLD_WATCH/var/lib/celld/${{RAILWAY_ENVIRONMENT_NAME}}work dir on the volume; per-environment subdirs, and the embedded reference survives template generation (literal values get stripped to prompts)
restart policyALWAYSa node against an empty bucket exits; ALWAYS turns that into "wait for first deploy". The default ON_FAILURE(10) gives up permanently
volume/var/lib/celldSQLite + replication scratch; state is recoverable from the bucket, volume makes restarts fast

Template-generation gotchas learned the hard way:

  • templateGenerate strips every literal variable value (anti-secret-leak) and keeps only values containing ${{…}} references. Statics must live in the start command or be expressed as references.
  • Railway's start command is exec'd raw: no shell, no env-var syntax, image entrypoint ignored. /bin/sh -c "…" works if you need shell features.
  • railway redeploy re-runs the previous deployment's config snapshot — config/variable changes need a fresh deploy (serviceInstanceDeployV2), not a redeploy.

Scaling to more nodes

Do not use Railway replicas — replicas share one private domain and one volume; every celld node needs its own advertise address and work directory. Duplicate the service in the same project instead (same variables — the references resolve identically — plus its own volume). Each service gets its own RAILWAY_PRIVATE_DOMAIN, so CELLD_ADVERTISE is automatically distinct; nodes discover each other through leases in the bucket. No join step.

Under load, opt into pressure shedding per node, e.g. CELLD_MAX_RESIDENT_CELLS=1000 and CELLD_RESIDENT_LOW_WATER=800.

Security notes

  • Bucket access = fleet-admin access. The bucket holds deployments, all cell SQLite state, ownership records, and the fleet's peer-auth secret.
  • celld's peer protocol doesn't terminate TLS; upstream says keep the peer port off the public internet. Here peers talk over Railway's private network, and the public domain (needed to serve your app) exposes the same port — peer requests on it are versioned, HMAC-authenticated, clock-bounded, and replay-protected with the fleet secret, so exposure is authenticated-only. For strict parity with upstream's advice, disable public networking and front the node with your own ingress in-project.
  • Rotate storage credentials with railway bucket credentials --bucket celld-cells --reset --yes — reference variables pick the new ones up on the next deploy.

Files in this directory

filepurpose
Dockerfile + railway.jsonrepo-based fallback of the same service (image pin + config-as-code), kept for reference; the published template is image-sourced and needs no repo
.env.examplethe variable set, documented
check.shself-check for these assets

Upgrading celld

New celld release → update the image tag on the template's service (and the Dockerfile pin here). Releases are on GitHub, each with a build attestation. The bucket format is the contract; nodes are replaceable.


Template Content

celld-cells

Bucket

More templates in this category

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

PROJETOS
7
View Template
Redis
Self Host Latest Redis with Railway

Arloodots
1
View Template
Postgres Backup to Cloudflare R2 (S3-Compatible)
Automated PostgreSQL backups to S3-compatible storage with encryption

Artour
7