Cloudflare Tunnel
Run a remotely-managed Cloudflare tunnel alongside a Rails development server.
Offline startup and connection loss stay quiet and do not stop the server or
jobs. The gem supervises an existing cloudflared executable; it never installs,
downloads or updates it, creates a tunnel, or changes DNS.
Requires Ruby 4.0+ and a POSIX host (Linux or macOS). Rails integration requires host-installed Rails 8.1+. Runner and probe classes can load without Rails. MIT licensed; maintained by Ethos Link. This is an independent integration.
Rails setup
Add to the development group:
group :development do
gem "cloudflare-tunnel", "~> 0.1"
endbundle install
bin/rails generate cloudflare_tunnel:install \
--hostname=devel.example.com --port=3000 --metrics-port=20241The generator creates config/initializers/cloudflare_tunnel.rb and adds
tunnel: bin/rails cloudflare_tunnel:start to Procfile.dev.
Install the executable yourself using Cloudflare's instructions. When missing, the start task prints that link once and stays idle so Foreman keeps other services running. The check task reports it and exits with failure.
In Cloudflare, configure a remotely-managed tunnel's published application
hostname to use http://localhost:3000 (match your Rails port). Store its
connector token with bin/rails credentials:edit:
cloudflare:
tunnel:
token: YOUR_TUNNEL_TOKENTUNNEL_TOKEN overrides credentials. Tokens use temporary files with mode 0600,
stay out of command arguments and diagnostics, and are removed on normal exit,
INT and TERM. A forced OS kill cannot run cleanup; remove abandoned
cloudflare-tunnel-*.token files from your OS temporary directory if needed.
bin/dev
# In a second terminal, after Rails and the tunnel are running:
bin/rails cloudflare_tunnel:checkAdding the gem or booting Rails never starts a tunnel. Only the explicit start task does. Tasks refuse to run outside development. The diagnostic middleware is installed only in development. The generated initializer guards against the gem being excluded from other Bundler groups.
Configuration
if Rails.env.development?
Cloudflare::Tunnel.configure do |config|
config.hostname = "devel.example.com"
config.origin = "http://localhost:#{ENV.fetch('PORT', '3000')}"
config.metrics_port = 20241
config.log_level = :error
config.log_path = "log/cloudflare-tunnel.log"
config.retry_interval = 30
end
end| Option | Default | Purpose |
|---|---|---|
hostname |
required | Bare public hostname configured in Cloudflare |
origin |
http://localhost:$PORT (3000 otherwise) |
Expected Rails origin; change Cloudflare's origin setting separately |
metrics_port |
20241 |
Readiness/metrics port bound to 127.0.0.1; choose a distinct port for every concurrent app |
log_level |
:error |
Native cloudflared level: :debug, :info, :warn, :error, :fatal
|
log_path |
log/cloudflare-tunnel.log |
Append connector output here, relative to Rails root; nil discards it |
retry_interval |
30 |
Seconds before restarting an exited connector |
shutdown_timeout |
5 |
Seconds after TERM before killing a stuck child |
check_timeout |
5 |
HTTP connection/read/write timeouts, per diagnostic request |
executable |
cloudflared |
Name on PATH or an explicit filesystem path |
enabled |
true | False when TUNNEL is 0, false, off or no; configurable explicitly |
token |
lazy Rails credentials / TUNNEL_TOKEN
|
String or callable; keep real tokens out of initializers |
The connector handles its own reconnections. If it exits, the supervisor retries
after retry_interval, without recurring terminal messages. Disabled tunnels,
missing credentials and invalid configurations print one actionable message and
stay idle. Fix configuration and restart. The check fails for those states.
Use TUNNEL=0 bin/dev to disable the connector. Use your local Rails URL offline.
OAuth callbacks and provider requests still need internet. Turnstile behavior
remains application-owned.
The native log level filters cloudflared messages. Connector output stays out of
Rails logs and recurring Foreman output. Set log_path = nil to discard it.
Ignore log/ and tmp/ in Git; appended logs need normal local cleanup. Debug
logs may include request details; enable that level only while diagnosing.
A per-app file lock prevents two gem-managed connectors in the same working directory. Stop older Docker or system-service connectors for that tunnel yourself. Other working directories can still run replicas and receive traffic.
Diagnostics
bin/rails cloudflare_tunnel:check returns 0 only when all checks pass:
- Configuration, executable availability and token presence.
- The local Rails origin returns a fresh signed probe response.
- The connector's
/readyendpoint reports an active Cloudflare connection. - HTTPS on the public hostname returns the expected response from this app.
The development-only /.well-known/cloudflare-tunnel?nonce=… probe uses an HMAC
with a purpose-specific key derived from Rails' secret key base and returns
Cache-Control: no-store. It creates no records and exposes no credentials.
Fresh nonces prevent stale cached health pages passing the check. The signature
identifies the application's key, not a particular machine sharing that key.
The configured metrics port must belong to this connector.
Failures identify the layer and return 1, including offline failures. The check
does not start a connector or change configuration. Redirects, Access login pages
and incorrect origins/app keys fail. Executable availability alone does not
validate the token; the running connector and end-to-end checks establish that.
Authentication errors remain in the separate connector log. No Cloudflare
management API token is required.
References: run parameters and readiness example.
Development and releases
Use published Ethos Gemkit for shared tooling. Keep the exact Gemfile version and workflow commit pins on the same release.
bundle install
bundle exec lefthook install
bundle exec rake verifyTests use controlled local fixtures and need no service account. Release preparation, publication, registry setup and recovery are documented in the Gemkit release guide.