Cloudflare Deployment
Run your Rails app on Cloudflare’s global network with D1 database.
Table of Contents
- Overview
- Prerequisites
- Database Options
- Deployment
- Manual Deployment
- Generated Files
- Database Commands
- Environment Variables
- D1 Specifics
- Static Assets
- Troubleshooting
- ISR (Incremental Static Regeneration)
- Limitations
- Comparison with Vercel
Overview
Cloudflare Workers deployment runs your application on Cloudflare’s edge network—over 300 cities worldwide. D1 is Cloudflare’s native SQLite database, purpose-built for Workers.
Use cases:
- Global applications with SQLite simplicity
- Cloudflare ecosystem integration
- Edge computing with D1’s read replicas
- Cost-effective serverless deployment
Prerequisites
- Wrangler CLI
npm i -g wrangler wrangler login
That’s it! The db:prepare command handles D1 database creation automatically.
Database Options
| Adapter | Service | Notes |
|---|---|---|
d1 |
Cloudflare D1 | Native SQLite, recommended |
turso |
Turso | SQLite with sync, HTTP protocol |
D1 is the primary choice for Cloudflare deployments.
Deployment
# Prepare database (creates if needed, migrates, seeds if fresh)
bin/juntos db:prepare -d d1
# Deploy
bin/juntos deploy -d d1
The deploy command:
- Builds the app with Cloudflare configuration
- Generates
wrangler.tomlandsrc/index.js - Verifies the build loads correctly
- Runs
wrangler deploy
Manual Deployment
If you prefer manual control:
# Build only
bin/juntos build -t cloudflare -d d1
# Deploy with Wrangler
cd dist
wrangler deploy
Generated Files
wrangler.toml
name = "myapp"
main = "src/index.js"
compatibility_date = "2026-01-01"
compatibility_flags = ["nodejs_compat"]
[[d1_databases]]
binding = "DB"
database_name = "myapp_production"
database_id = "${D1_DATABASE_ID}"
[assets]
directory = "./app/assets"
src/index.js
import { Application, Router } from '../lib/rails.js';
import '../config/routes.js';
import { migrations } from '../db/migrate/index.js';
import { Seeds } from '../db/seeds.js';
import { layout } from '../app/views/layouts/application.js';
Application.configure({
migrations: migrations,
seeds: Seeds,
layout: layout
});
export default Application.worker();
Database Commands
D1 database management uses Wrangler under the hood:
bin/juntos db:create -d d1 # Create D1 database
bin/juntos db:migrate -d d1 # Run migrations only
bin/juntos db:seed -d d1 # Run seeds only
bin/juntos db:prepare -d d1 # All of the above (smart)
bin/juntos db:drop -d d1 # Delete database
The db:prepare command is the most common—it creates the database if needed, runs migrations, and seeds only if the database is fresh.
Environment Variables
Database IDs are stored in .env.local and are environment-specific:
| Variable | Description |
|---|---|
D1_DATABASE_ID |
D1 database ID (development) |
D1_DATABASE_ID_PRODUCTION |
D1 database ID (production) |
D1_DATABASE_ID_STAGING |
D1 database ID (staging) |
When you run juntos db:create -e production, the ID is saved to D1_DATABASE_ID_PRODUCTION. Commands fall back to D1_DATABASE_ID if the per-environment variable is not set.
For secrets:
wrangler secret put API_KEY
Access in code via env.API_KEY.
D1 Specifics
Bindings
D1 is accessed via bindings, not connection strings. The Worker receives the database as env.DB:
// In the runtime
await env.DB.prepare("SELECT * FROM articles").all();
The Juntos adapter handles this automatically.
SQL Dialect
D1 uses SQLite syntax. Most Rails migrations work, but some PostgreSQL-specific features won’t:
- ✅
create_table,add_column,add_index - ✅ Standard SQL types
- ❌ Arrays, JSONB (use JSON instead)
- ❌ PostgreSQL-specific functions
Read Replicas
D1 automatically replicates reads to edge locations. Writes go to the primary. This is transparent to your application.
Static Assets
Assets in app/assets/ are served via Cloudflare’s CDN:
[assets]
directory = "./app/assets"
For Tailwind CSS, ensure the built CSS is in app/assets/builds/.
Troubleshooting
“D1_ERROR: no such table”
Migrations haven’t run:
bin/juntos db:migrate -d d1
“Binding not found: DB”
The D1 database isn’t bound. Check wrangler.toml:
[[d1_databases]]
binding = "DB"
database_name = "myapp_production"
database_id = "your-actual-id" # Not ${D1_DATABASE_ID}
Local development
Use Wrangler’s local mode:
cd dist
wrangler dev
This runs locally with a local D1 instance.
ISR (Incremental Static Regeneration)
Juntos supports ISR for pages that benefit from caching. Add a pragma comment to cache pages:
# Pragma: revalidate 60
@posts = Post.all
__END__
<ul>
<% @posts.each do |post| %>
<li><%= post.title %></li>
<% end %>
</ul>
The revalidate value is in seconds. The Cloudflare ISR adapter uses the Cache API:
- First request renders and caches the page
- Subsequent requests serve the cached version
- After the revalidate period, stale content is served while regenerating in the background via
waitUntil
How It Works
The adapter checks the cache, serves cached responses, and handles background regeneration:
// Simplified - actual implementation in the adapter
const cache = caches.default;
let response = await cache.match(cacheKey);
if (response && age < revalidate) {
return response; // Fresh cache hit
}
if (response) {
// Stale - serve and regenerate in background
context.waitUntil(regenerate(context, cacheKey, renderFn));
return response;
}
// Cache miss - generate and cache
return await regenerate(context, cacheKey, renderFn);
On-Demand Revalidation
For immediate cache invalidation (e.g., after a content update):
class ArticlesController < ApplicationController
def update
@article.update!(article_params)
ISR.revalidate("/articles/#{@article.id}")
redirect_to @article
end
end
This deletes the cached page, forcing regeneration on the next request.
🧪 Feedback requested — Share your experience
Limitations
- No filesystem — Use R2 for object storage
- No WebSockets — Use Durable Objects or external services
- CPU limits — 10-50ms CPU time per request (not wall time)
- Memory limits — 128MB per Worker
Comparison with Vercel
| Aspect | Cloudflare | Vercel |
|---|---|---|
| Database | D1 (native SQLite) | Neon/Turso/PlanetScale |
| Edge locations | 300+ cities | ~20 regions |
| Pricing model | Requests + duration | Requests + compute |
| Static assets | Integrated CDN | Integrated CDN |
| Local dev | wrangler dev |
vercel dev |
Choose Cloudflare for SQLite simplicity and maximum global distribution. Choose Vercel for PostgreSQL/MySQL compatibility or tighter Git integration.