Rails Integration Guide
Overview
Jetty is the perfect companion for Rails development. Whether you're building a classic Rails monolith, a Hotwire-powered interactive app, or a Rails API backend, Jetty makes it effortless to share your local development environment with the world.
Why Jetty works great with Rails:
- Zero configuration - Works out of the box with
rails server,bin/dev, or any Puma/Unicorn setup - Full WebSocket support - Action Cable connections work seamlessly through Jetty tunnels
- Hotwire/Turbo compatible - Stream updates, morphs, and broadcasts work perfectly
- Webhook testing - Test Stripe payments, GitHub webhooks, Action Mailbox, and more
- Mobile testing - Share your local Rails app with real devices or remote teammates
- Client demos - Show work-in-progress features without deploying to staging
Jetty understands that Rails developers need reliable, fast tunnels that just work. No messing with /etc/hosts, no complicated proxy configs—just jetty share 3000 and you're live.
Prerequisites
Before you start, make sure you have:
- Rails 6.0 or later (Rails 7+ recommended)
- Jetty CLI installed - See Quickstart for installation
- API token configured - Get one from your Jetty dashboard
- A Rails app running locally - If you don't have one, we'll help you create one below
First-time Jetty setup
If you haven't set up Jetty yet:
# Install Jetty CLI
curl -fsSL https://usejetty.online/install/jetty.sh | bash
# Configure your credentials
jetty config set server https://usejetty.online
jetty config set token YOUR_API_TOKEN_HERE
That's it! You're ready to share Rails apps.
Quick Start
Get your Rails app online in under 2 minutes.
1. Start your Rails server
# Standard Rails server
rails server
# Or if you're using bin/dev (Rails 7+)
bin/dev
Your app should now be running on http://localhost:3000.
2. Share it with Jetty
Open a new terminal tab and run:
jetty share 3000
You'll see:
Connected to Jetty Bridge
Tunnel established
Your tunnel is live at:
https://clever-panda-abc123.tunnels.usejetty.online
Press Ctrl+C to stop sharing.
3. Test it
Copy that URL and open it in your browser. You're now accessing your local Rails app through a public URL.
That's the basics. Now let's dive into Rails-specific workflows.
Development Server
Rails developers have several options for running local servers. Jetty works with all of them.
Standard rails server (Puma)
The default Rails server runs on port 3000:
# Terminal 1: Start Rails
rails server
# Terminal 2: Share it
jetty share 3000
bin/dev with Procfile.dev (Rails 7+)
Rails 7 apps often use bin/dev to run Puma + CSS/JS build processes together:
# Terminal 1: Start all services
bin/dev
# Terminal 2: Share the web server
jetty share 3000
The CSS/JS build processes (Tailwind, esbuild, etc.) will continue to work through the tunnel—Jetty forwards all requests, including asset requests, to your local server.
Custom ports
If your Rails app runs on a different port:
# Rails server on port 5000
rails server -p 5000
# Share it
jetty share 5000
Binding to 0.0.0.0
By default, Puma binds to localhost. This works fine with Jetty. However, if you need to bind to all interfaces:
rails server -b 0.0.0.0
jetty share 3000
This is useful when running Rails in Docker or when you want to access the server from multiple sources simultaneously.
Common Workflows
Local Development
Share with your team
Reserved subdomains make it easy to share a stable URL with your team:
# Reserve a subdomain for your project
jetty share 3000 --subdomain myapp-dev
# Everyone on your team can now access:
# https://myapp-dev.tunnels.usejetty.online
The subdomain persists across tunnel restarts, so your teammates can bookmark it.
Client demos
Show work-in-progress features to clients without deploying:
# Use a client-friendly subdomain
jetty share 3000 --subdomain acme-preview
# Share: https://acme-preview.tunnels.usejetty.online
No staging deployment, no waiting for CI/CD—just instant access to your local branch.
Webhook Testing
Rails apps frequently integrate with third-party services that send webhooks. Testing these locally used to require deploying to a staging server. Not anymore!
Stripe webhooks
Perfect for testing Rails e-commerce or SaaS billing:
# config/routes.rb
Rails.application.routes.draw do
post '/webhooks/stripe', to: 'webhooks/stripe#create'
end
# app/controllers/webhooks/stripe_controller.rb
class Webhooks::StripeController < ApplicationController
skip_before_action :verify_authenticity_token
def create
payload = request.body.read
sig_header = request.env['HTTP_STRIPE_SIGNATURE']
event = nil
begin
event = Stripe::Webhook.construct_event(
payload, sig_header, Rails.application.credentials.dig(:stripe, :webhook_secret)
)
rescue JSON::ParserError, Stripe::SignatureVerificationError => e
render json: { error: e.message }, status: 400
return
end
# Handle the event
case event.type
when 'payment_intent.succeeded'
handle_payment_success(event.data.object)
when 'customer.subscription.updated'
handle_subscription_update(event.data.object)
end
render json: { status: 'success' }, status: 200
end
private
def handle_payment_success(payment_intent)
Rails.logger.info "Payment succeeded: #{payment_intent.id}"
# Your business logic here
end
def handle_subscription_update(subscription)
Rails.logger.info "Subscription updated: #{subscription.id}"
# Your business logic here
end
end
Setup with Jetty:
# Start your tunnel
jetty share 3000 --subdomain myapp-stripe
# In Stripe Dashboard → Webhooks → Add endpoint:
# URL: https://myapp-stripe.tunnels.usejetty.online/webhooks/stripe
# Events: payment_intent.succeeded, customer.subscription.updated, etc.
Now trigger test payments in Stripe and watch them hit your local Rails app in real-time!
GitHub webhooks
Test GitHub App integrations or repository webhooks:
# config/routes.rb
post '/webhooks/github', to: 'webhooks/github#create'
# app/controllers/webhooks/github_controller.rb
class Webhooks::GithubController < ApplicationController
skip_before_action :verify_authenticity_token
def create
event_type = request.headers['X-GitHub-Event']
payload = JSON.parse(request.body.read)
case event_type
when 'push'
handle_push(payload)
when 'pull_request'
handle_pull_request(payload)
when 'issues'
handle_issue(payload)
end
render json: { status: 'ok' }, status: 200
end
private
def handle_push(payload)
repo = payload['repository']['full_name']
branch = payload['ref'].split('/').last
Rails.logger.info "Push to #{repo}:#{branch}"
end
def handle_pull_request(payload)
action = payload['action']
pr_number = payload['number']
Rails.logger.info "PR ##{pr_number}: #{action}"
end
def handle_issue(payload)
action = payload['action']
issue_number = payload['issue']['number']
Rails.logger.info "Issue ##{issue_number}: #{action}"
end
end
Setup:
jetty share 3000 --subdomain myapp-github
# In GitHub repo → Settings → Webhooks → Add webhook:
# Payload URL: https://myapp-github.tunnels.usejetty.online/webhooks/github
# Content type: application/json
# Events: Choose individual events (push, pull_request, issues, etc.)
Twilio webhooks
Handle SMS/voice callbacks in your Rails app:
# config/routes.rb
post '/webhooks/twilio/sms', to: 'webhooks/twilio#sms'
post '/webhooks/twilio/voice', to: 'webhooks/twilio#voice'
# app/controllers/webhooks/twilio_controller.rb
class Webhooks::TwilioController < ApplicationController
skip_before_action :verify_authenticity_token
def sms
from = params['From']
body = params['Body']
Rails.logger.info "SMS from #{from}: #{body}"
# Process the message
response = Twilio::TwiML::MessagingResponse.new do |r|
r.message body: "Thanks for your message: #{body}"
end
render xml: response.to_s
end
def voice
from = params['From']
response = Twilio::TwiML::VoiceResponse.new do |r|
r.say message: "Hello! Thanks for calling from #{from}."
r.play url: "https://example.com/your-audio.mp3"
end
render xml: response.to_s
end
end
Setup:
jetty share 3000 --subdomain myapp-twilio
# In Twilio Console → Phone Numbers → Your number:
# Messaging: https://myapp-twilio.tunnels.usejetty.online/webhooks/twilio/sms
# Voice: https://myapp-twilio.tunnels.usejetty.online/webhooks/twilio/voice
Send a text to your Twilio number and watch it hit your local Rails app!
Action Mailbox webhooks
Rails 6+ includes Action Mailbox for receiving emails. Many inbound email providers (Mailgun, SendGrid, Postmark) use webhooks:
# config/routes.rb
Rails.application.routes.draw do
# Action Mailbox creates these routes automatically
end
# app/mailboxes/support_mailbox.rb
class SupportMailbox < ApplicationMailbox
before_processing :require_valid_token
def process
# Create a support ticket from the email
SupportTicket.create!(
subject: mail.subject,
body: mail.body.decoded,
from_email: mail.from.first,
received_at: Time.current
)
end
private
def require_valid_token
# Optional: verify the email came through your service
unless mail.to.include?("support@myapp.com")
bounced!
end
end
end
Setup with Mailgun:
jetty share 3000 --subdomain myapp-mailbox
# In Mailgun → Receiving → Routes → Create Route:
# Match Recipient: support@yourdomain.com
# Forward: https://myapp-mailbox.tunnels.usejetty.online/rails/action_mailbox/mailgun/inbound_emails
Now emails sent to your configured address will be received by your local Rails app!
API Development
Share Rails API with mobile apps
If you're building a Rails API backend for mobile apps:
# config/routes.rb
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :posts, only: [:index, :show, :create]
resources :users, only: [:show, :update]
end
end
end
# app/controllers/api/v1/posts_controller.rb
class Api::V1::PostsController < ApplicationController
def index
posts = Post.all.limit(20)
render json: posts
end
def show
post = Post.find(params[:id])
render json: post
end
def create
post = Post.new(post_params)
if post.save
render json: post, status: :created
else
render json: { errors: post.errors }, status: :unprocessable_entity
end
end
private
def post_params
params.require(:post).permit(:title, :body, :author_id)
end
end
Share with your mobile dev team:
# Reserve a stable API endpoint
jetty share 3000 --subdomain myapp-api
# Mobile developers can now configure their apps to use:
# https://myapp-api.tunnels.usejetty.online/api/v1
The mobile team can test against your local API server without waiting for staging deploys. Perfect for rapid iteration!
GraphQL APIs
If you're using graphql-ruby:
# config/routes.rb
post "/graphql", to: "graphql#execute"
# app/controllers/graphql_controller.rb
class GraphqlController < ApplicationController
def execute
variables = prepare_variables(params[:variables])
query = params[:query]
operation_name = params[:operationName]
result = MyAppSchema.execute(
query,
variables: variables,
operation_name: operation_name
)
render json: result
rescue StandardError => e
render json: { errors: [{ message: e.message }] }, status: 500
end
private
def prepare_variables(variables_param)
case variables_param
when String
variables_param.present? ? JSON.parse(variables_param) : {}
when Hash
variables_param
when ActionController::Parameters
variables_param.to_unsafe_hash
when nil
{}
else
raise ArgumentError, "Unexpected variables: #{variables_param}"
end
end
end
Share your GraphQL endpoint:
jetty share 3000 --subdomain myapp-graphql
# Frontend developers can now query:
# https://myapp-graphql.tunnels.usejetty.online/graphql
Use GraphQL Playground or Apollo Studio to point at your tunnel URL and explore your schema!
Environment Configuration
Rails apps often use environment variables for configuration. Here's how to manage them with Jetty.
Using .env files (with dotenv-rails)
Most Rails apps use dotenv-rails to load environment variables:
# .env.development
DATABASE_URL=postgresql://localhost/myapp_development
REDIS_URL=redis://localhost:6379/0
SECRET_KEY_BASE=your_secret_key_here
# Jetty tunnel settings
JETTY_TUNNEL_URL=https://myapp-dev.tunnels.usejetty.online
You can reference ENV['JETTY_TUNNEL_URL'] in your Rails app if needed, though typically you won't need to—Jetty just forwards requests transparently.
Rails Credentials (Rails 5.2+)
For production-like secrets:
# Edit credentials
EDITOR="code --wait" rails credentials:edit
# Add your Jetty-related secrets:
stripe:
publishable_key: pk_test_...
secret_key: sk_test_...
webhook_secret: whsec_...
github:
client_id: Iv1.abc123
client_secret: def456
webhook_secret: ghp_xyz789
Access in your app:
Rails.application.credentials.dig(:stripe, :webhook_secret)
Rails.application.credentials.dig(:github, :webhook_secret)
Best practices
- Never commit secrets - Use
.env(in.gitignore) or Rails credentials - Different secrets for tunnels vs production - Use test API keys when developing with Jetty
- Document required env vars - Add a
.env.examplewith dummy values - Use webhook secrets - Always verify webhook signatures (Stripe, GitHub, etc.) even in development
Action Cable / WebSockets
Action Cable (Rails' WebSocket implementation) works perfectly with Jetty. No special configuration needed!
Basic Action Cable setup
# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from "chat_#{params[:room_id]}"
end
def receive(data)
ActionCable.server.broadcast(
"chat_#{params[:room_id]}",
message: data['message'],
user: current_user.name
)
end
end
# app/javascript/channels/chat_channel.js
import consumer from "./consumer"
consumer.subscriptions.create({ channel: "ChatChannel", room_id: 1 }, {
received(data) {
console.log("Received:", data.message)
},
send(message) {
this.perform('receive', { message: message })
}
})
Share with Jetty:
jetty share 3000 --subdomain myapp-chat
The WebSocket connection upgrades work seamlessly through the tunnel. Users accessing https://myapp-chat.tunnels.usejetty.online will connect to Action Cable via wss://myapp-chat.tunnels.usejetty.online/cable.
Production-like Action Cable config
If you want to test Action Cable in a production-like environment:
# config/environments/development.rb
Rails.application.configure do
# Allow Action Cable requests from your Jetty tunnel
config.action_cable.allowed_request_origins = [
'http://localhost:3000',
'https://myapp-dev.tunnels.usejetty.online'
]
# Or disable origin checking entirely in development
config.action_cable.disable_request_forgery_protection = true
end
Multiple browser testing
Action Cable through Jetty is perfect for testing real-time features across multiple devices:
# Start tunnel
jetty share 3000 --subdomain myapp-realtime
# Open on laptop: https://myapp-realtime.tunnels.usejetty.online
# Open on phone: https://myapp-realtime.tunnels.usejetty.online
# Open on tablet: https://myapp-realtime.tunnels.usejetty.online
# All three will receive broadcasts in real-time!
Turbo Streams (Rails 7 + Hotwire)
Turbo Streams over WebSockets work exactly like Action Cable:
# app/models/post.rb
class Post < ApplicationRecord
after_create_commit { broadcast_prepend_to "posts" }
after_update_commit { broadcast_replace_to "posts" }
after_destroy_commit { broadcast_remove_to "posts" }
end
# app/views/posts/index.html.erb
<%= turbo_stream_from "posts" %>
<div id="posts">
<%= render @posts %>
</div>
Share it:
jetty share 3000 --subdomain myapp-turbo
Create/update/delete posts in one browser tab and watch them appear in another tab instantly!
Reserved Subdomains
Reserve subdomains for your Rails projects to get stable URLs that don't change between tunnel sessions.
Reserve for your project
# Development environment
jetty share 3000 --subdomain myapp-dev
# Always get: https://myapp-dev.tunnels.usejetty.online
Common Rails subdomain patterns
# Main app
jetty share 3000 --subdomain myapp
# Staging-like environment
jetty share 3000 --subdomain myapp-staging
# Feature branches
jetty share 3000 --subdomain myapp-feature-x
# Team member sandboxes
jetty share 3000 --subdomain myapp-alice
jetty share 3000 --subdomain myapp-bob
# API endpoint
jetty share 3000 --subdomain myapp-api
# Admin panel
jetty share 3000 --subdomain myapp-admin
Pre-configured for Rails
These subdomains are already reserved for Rails developers on Jetty:
rails-staging.tunnels.usejetty.onlinerails-dev.tunnels.usejetty.online
Feel free to use them for quick testing without claiming your own subdomain.
Custom Domains
For production-like testing, use your own domain with Jetty.
Setup
- Reserve a custom domain in your Jetty dashboard
- Add DNS records pointing to Jetty's edge servers
- Share your tunnel with the custom domain flag
jetty share 3000 --domain dev.mycompany.com
Now your Rails app is accessible at https://dev.mycompany.com instead of *.tunnels.usejetty.online.
Why use custom domains?
- OAuth callbacks - Some OAuth providers require verified domains
- Cookie domains - Test cookie scoping with your actual domain
- Client demos - More professional than
tunnels.usejetty.online - Subdomain routing - Test Rails apps that use subdomain-based routing
Subdomain routing example
If your Rails app routes based on subdomains:
# config/routes.rb
Rails.application.routes.draw do
constraints subdomain: 'api' do
namespace :api do
resources :posts
end
end
constraints subdomain: 'admin' do
namespace :admin do
resources :users
end
end
# Main app (no subdomain)
root to: 'home#index'
end
You can test this with custom domains:
# Terminal 1
jetty share 3000 --domain myapp.com
# Terminal 2
jetty share 3000 --domain api.myapp.com
# Terminal 3
jetty share 3000 --domain admin.myapp.com
See the Custom Domains docs for full setup instructions.
Security Considerations
Rails has several security features that interact with Jetty tunnels. Here's how to configure them properly.
force_ssl configuration
Rails' force_ssl setting redirects HTTP to HTTPS. Jetty tunnels are always HTTPS, so this works out of the box:
# config/environments/development.rb
Rails.application.configure do
# Keep this false in development (default)
config.force_ssl = false
# If you want to test HTTPS-only features locally:
# config.force_ssl = true
end
When force_ssl = true, Rails checks the X-Forwarded-Proto header. Jetty automatically sets this to https for all tunnel requests, so SSL redirects work correctly.
Trusted hosts
Rails 6+ includes HostAuthorization middleware to prevent DNS rebinding attacks. You need to allow your Jetty tunnel domains:
# config/environments/development.rb
Rails.application.configure do
# Allow Jetty tunnel hosts
config.hosts << "myapp-dev.tunnels.usejetty.online"
# Or allow all Jetty tunnels (less secure but convenient)
config.hosts << /.*\.tunnels\.usejetty\.online$/
# Or disable host checking entirely in development (not recommended)
# config.hosts.clear
end
Best practice: Add specific subdomains you use regularly:
# config/environments/development.rb
Rails.application.configure do
config.hosts << "myapp-dev.tunnels.usejetty.online"
config.hosts << "myapp-staging.tunnels.usejetty.online"
config.hosts << "myapp-alice.tunnels.usejetty.online"
# Also allow random subdomains for quick testing
config.hosts << /[a-z]+-[a-z]+-[a-z0-9]+\.tunnels\.usejetty\.online$/
end
CSRF protection
Rails' CSRF protection works normally with Jetty tunnels. However, if you're testing from multiple domains or with API clients:
# For webhook endpoints, skip CSRF verification
class Webhooks::StripeController < ApplicationController
skip_before_action :verify_authenticity_token
# But verify the webhook signature instead!
before_action :verify_stripe_signature
private
def verify_stripe_signature
payload = request.body.read
sig_header = request.env['HTTP_STRIPE_SIGNATURE']
begin
Stripe::Webhook.construct_event(
payload,
sig_header,
Rails.application.credentials.dig(:stripe, :webhook_secret)
)
rescue Stripe::SignatureVerificationError
render json: { error: 'Invalid signature' }, status: 400
end
end
end
Session configuration
If you're using cookie-based sessions and testing across different tunnel URLs:
# config/initializers/session_store.rb
Rails.application.config.session_store :cookie_store,
key: '_myapp_session',
domain: :all, # Allow sessions across subdomains
secure: Rails.env.production?, # HTTPS-only in production
same_site: :lax # Protect against CSRF while allowing normal navigation
Best practices checklist
- Use
config.hoststo explicitly allow your Jetty tunnel domains - Keep
force_ssl = falsein development (unless testing HTTPS features) - Skip CSRF for webhooks, but verify signatures instead
- Use test API keys (Stripe, Twilio, etc.) with Jetty tunnels
- Never commit real secrets to version control
- Configure
allowed_request_originsfor Action Cable if using strict origin checking
Hotwire / Turbo
Rails 7 apps built with Hotwire work beautifully with Jetty. All Turbo features—Frames, Streams, Drive—function normally through tunnels.
Turbo Drive (default)
Turbo Drive navigation works out of the box:
<!-- app/views/layouts/application.html.erb -->
<!DOCTYPE html>
<html>
<head>
<title>My Hotwire App</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
</head>
<body>
<%= yield %>
</body>
</html>
jetty share 3000 --subdomain myapp-turbo
All navigation is accelerated through Turbo Drive, even over the Jetty tunnel.
Turbo Frames
Frames work perfectly:
<!-- app/views/posts/index.html.erb -->
<div id="posts">
<% @posts.each do |post| %>
<%= turbo_frame_tag dom_id(post) do %>
<h2><%= post.title %></h2>
<p><%= post.body %></p>
<%= link_to "Edit", edit_post_path(post) %>
<% end %>
<% end %>
</div>
<!-- app/views/posts/edit.html.erb -->
<%= turbo_frame_tag dom_id(@post) do %>
<%= form_with model: @post do |f| %>
<%= f.text_field :title %>
<%= f.text_area :body %>
<%= f.submit "Save" %>
<% end %>
<% end %>
Clicking "Edit" loads the form inline without a full page reload. Works great through Jetty!
Turbo Streams (WebSocket)
Real-time updates via Turbo Streams work seamlessly:
# app/models/post.rb
class Post < ApplicationRecord
broadcasts_to ->(post) { "posts" }, inserts_by: :prepend
end
# app/views/posts/index.html.erb
<%= turbo_stream_from "posts" %>
<div id="posts">
<%= render @posts %>
</div>
Multiple users on the same Jetty tunnel will all receive live updates when posts are created/updated/deleted.
Turbo Native (iOS/Android)
If you're building Turbo Native mobile apps, use Jetty to share your Rails backend:
jetty share 3000 --subdomain myapp-mobile
Configure your iOS/Android app to point at https://myapp-mobile.tunnels.usejetty.online and develop against your local Rails server!
Stimulus controllers
Stimulus works normally through Jetty tunnels—no special configuration needed:
// app/javascript/controllers/hello_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [ "name", "output" ]
greet() {
this.outputTarget.textContent = `Hello, ${this.nameTarget.value}!`
}
}
<div data-controller="hello">
<input data-hello-target="name" type="text">
<button data-action="click->hello#greet">Greet</button>
<span data-hello-target="output"></span>
</div>
Everything works as expected through the tunnel.
Docker / Docker Compose
Running Rails in Docker? Jetty supports containerized apps too.
Docker Compose setup
# docker-compose.yml
version: '3.8'
services:
web:
build: .
command: bundle exec rails server -b 0.0.0.0
volumes:
- .:/app
ports:
- "3000:3000"
depends_on:
- db
- redis
environment:
- DATABASE_URL=postgresql://postgres:password@db:5432/myapp_development
- REDIS_URL=redis://redis:6379/0
db:
image: postgres:14
environment:
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7
volumes:
postgres_data:
Share the containerized app
# Start Docker Compose
docker-compose up
# Share the Rails container (port 3000 is exposed)
jetty share 3000 --subdomain myapp-docker
Jetty connects to the exposed port on your host machine (3000), which Docker forwards to the container.
Important: Bind to 0.0.0.0
When Rails runs in a Docker container, it must bind to 0.0.0.0 (not localhost) so the host machine can connect:
# In Dockerfile or docker-compose.yml
bundle exec rails server -b 0.0.0.0
Or set in config/puma.rb:
# config/puma.rb
port ENV.fetch("PORT") { 3000 }
bind "tcp://0.0.0.0:3000"
Testing with multiple services
If your Docker Compose includes Sidekiq, Redis, PostgreSQL, etc., they all work through the tunnel:
services:
web:
# ... (as above)
sidekiq:
build: .
command: bundle exec sidekiq
depends_on:
- db
- redis
environment:
- DATABASE_URL=postgresql://postgres:password@db:5432/myapp_development
- REDIS_URL=redis://redis:6379/0
docker-compose up
jetty share 3000 --subdomain myapp-full-stack
Now your Jetty tunnel shows the full Rails app, with background jobs and everything!
CI/CD Integration
Use Jetty tunnels in your Rails CI/CD pipeline for webhook testing, end-to-end tests, and more.
GitHub Actions
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:14
env:
POSTGRES_PASSWORD: password
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Install Jetty CLI
run: |
curl -fsSL https://usejetty.online/install/jetty.sh | bash
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Configure Jetty
env:
JETTY_TOKEN: ${{ secrets.JETTY_TOKEN }}
run: |
jetty config set server https://usejetty.online
jetty config set token $JETTY_TOKEN
- name: Setup database
env:
DATABASE_URL: postgresql://postgres:password@localhost:5432/myapp_test
run: |
bin/rails db:create db:schema:load
- name: Start Rails server
env:
DATABASE_URL: postgresql://postgres:password@localhost:5432/myapp_test
run: |
bin/rails server -b 0.0.0.0 -p 3000 &
sleep 5
- name: Share with Jetty
run: |
jetty share 3000 --subdomain myapp-ci-${{ github.run_id }} &
sleep 3
- name: Run tests
env:
JETTY_URL: https://myapp-ci-${{ github.run_id }}.tunnels.usejetty.online
run: |
bundle exec rspec
- name: Test webhooks
env:
JETTY_URL: https://myapp-ci-${{ github.run_id }}.tunnels.usejetty.online
run: |
# Send a test webhook to your CI tunnel
curl -X POST $JETTY_URL/webhooks/test -d '{"test": true}'
Add JETTY_TOKEN to your GitHub repository secrets.
Heroku CI / Heroku Pipelines
# app.json (for Heroku CI)
{
"name": "My Rails App",
"environments": {
"test": {
"addons": ["heroku-postgresql"],
"scripts": {
"test": "bundle exec rake test"
},
"env": {
"JETTY_TOKEN": {
"required": true
}
},
"buildpacks": [
{ "url": "heroku/ruby" }
]
}
}
}
In your test script:
# bin/ci-test.sh
#!/bin/bash
set -e
# Install Jetty
curl -fsSL https://usejetty.online/install/jetty.sh | bash
export PATH="$HOME/.local/bin:$PATH"
# Configure Jetty
jetty config set server https://usejetty.online
jetty config set token $JETTY_TOKEN
# Start Rails server in background
rails server -b 0.0.0.0 -p 3000 &
sleep 5
# Share with Jetty
jetty share 3000 --subdomain myapp-ci-$HEROKU_TEST_RUN_ID &
sleep 3
# Run tests
bundle exec rake test
End-to-end testing with external services
If your tests need to verify webhooks from external services:
# test/integration/stripe_webhook_test.rb
class StripeWebhookTest < ActionDispatch::IntegrationTest
test "processes payment_intent.succeeded webhook" do
# Use Stripe CLI to forward webhook to Jetty tunnel
# stripe listen --forward-to $JETTY_URL/webhooks/stripe
# Trigger a test payment
# stripe trigger payment_intent.succeeded
# Assert the webhook was processed
assert_equal 1, Payment.count
end
end
Run in CI:
export JETTY_URL=https://myapp-ci-$BUILD_ID.tunnels.usejetty.online
# Start tunnel
jetty share 3000 --subdomain myapp-ci-$BUILD_ID &
# Use Stripe CLI to forward webhooks to the tunnel
stripe listen --forward-to $JETTY_URL/webhooks/stripe &
# Run the tests
bundle exec rails test
This lets you test real webhook flows in CI!
Troubleshooting
Common issues Rails developers encounter with Jetty and how to fix them.
localhost vs 0.0.0.0
Problem: Jetty tunnel doesn't connect to Rails server.
Solution: Rails typically binds to localhost by default, which works fine. If you're having connection issues, try binding to 0.0.0.0:
# Instead of:
rails server
# Try:
rails server -b 0.0.0.0
Or set it in config/puma.rb:
bind "tcp://0.0.0.0:3000"
Asset precompilation / Vite / Webpacker
Problem: CSS/JS assets aren't loading through the Jetty tunnel.
Solution:
For Rails 7 with import maps (default), assets are served directly and should work fine.
For Vite / Webpacker / Shakapacker, make sure the dev server is running:
# Terminal 1: Start Vite dev server
bin/vite dev
# Terminal 2: Start Rails
rails server
# Terminal 3: Share it
jetty share 3000
If using Tailwind CSS standalone, run the watcher:
# Terminal 1: Watch Tailwind
bin/rails tailwindcss:watch
# Terminal 2: Start Rails
rails server
# Terminal 3: Share it
jetty share 3000
Or use bin/dev to run everything together:
# Terminal 1
bin/dev
# Terminal 2
jetty share 3000
Database host configuration
Problem: Rails app works locally but throws database errors through Jetty tunnel.
Solution: This is usually not Jetty-related—your database config is the same whether accessed directly or through a tunnel. Double-check config/database.yml:
development:
adapter: postgresql
host: localhost # Or 127.0.0.1
database: myapp_development
username: postgres
password: password
If running in Docker, use the service name:
development:
adapter: postgresql
host: <%= ENV.fetch('DB_HOST', 'localhost') %>
database: myapp_development
# docker-compose.yml
services:
web:
environment:
- DB_HOST=db
HostAuthorization blocking requests
Problem: Accessing the Jetty tunnel returns "Blocked host" error.
Solution: Add your tunnel domain to config.hosts:
# config/environments/development.rb
Rails.application.configure do
config.hosts << "myapp-dev.tunnels.usejetty.online"
# Or allow all Jetty tunnels:
config.hosts << /.*\.tunnels\.usejetty\.online$/
end
Restart your Rails server after making this change.
Action Cable connection failures
Problem: WebSocket connections fail through the Jetty tunnel.
Solution:
- Check
allowed_request_originsinconfig/environments/development.rb:
config.action_cable.allowed_request_origins = [
'http://localhost:3000',
'https://myapp-dev.tunnels.usejetty.online'
]
# Or disable origin checking in development:
config.action_cable.disable_request_forgery_protection = true
-
Restart Rails server after changing this config.
-
Check browser console for WebSocket connection errors.
CSRF token issues
Problem: Form submissions fail with "Invalid authenticity token" when using Jetty tunnel.
Solution:
Rails CSRF protection should work normally. If you're seeing issues:
- Make sure your layout includes CSRF meta tags:
<!-- app/views/layouts/application.html.erb -->
<head>
<%= csrf_meta_tags %>
</head>
-
If using Turbo, ensure you're on the latest version.
-
For API-only endpoints or webhooks, skip CSRF verification:
class Api::V1::BaseController < ApplicationController
skip_before_action :verify_authenticity_token
end
Slow response times
Problem: Requests through Jetty tunnel are slower than accessing localhost directly.
Explanation: There's some overhead (typically 50-200ms) for requests to travel:
- Browser → Jetty edge server
- Edge server → Your machine via WebSocket
- Your machine → Local Rails app (localhost:3000)
- Response travels back the same way
This is normal and unavoidable with tunnels. For development/testing, the tradeoff is worth it.
Tips to minimize latency:
- Choose a Jetty region close to you (if available)
- Ensure your internet connection is stable
- Close unnecessary background apps
Port already in use
Problem: rails server fails with "Address already in use - bind(2) for 127.0.0.1:3000"
Solution: Another process is using port 3000. Find and kill it:
# Find the process
lsof -ti:3000
# Kill it
kill -9 $(lsof -ti:3000)
# Or use a different port
rails server -p 3001
jetty share 3001
SSL/TLS verification errors in logs
Problem: Rails logs show SSL verification errors when making outbound requests.
Solution: This is usually unrelated to Jetty. Your Rails app making outbound HTTPS requests might have SSL issues. Check:
- OpenSSL version:
openssl version - System certificates:
brew install openssl(macOS) orapt install ca-certificates(Linux)
If you're seeing this specifically when accessing external APIs from your Rails app through the tunnel, it's likely a local dev environment issue, not Jetty.
Tips & Tricks
Create a shell alias
Add to your .bashrc or .zshrc:
# Quick Rails tunnel
alias jshare='jetty share 3000 --subdomain myapp-dev'
# Or use the current directory name
alias jshare='jetty share 3000 --subdomain $(basename $(pwd))'
Now just run jshare to share your Rails app!
Use npm scripts
Add to package.json:
{
"scripts": {
"dev": "bin/dev",
"share": "jetty share 3000 --subdomain myapp-dev"
}
}
Then:
npm run dev # Start Rails and asset servers
npm run share # Share with Jetty
Foreman / Procfile.dev integration
If using foreman or bin/dev, add Jetty to your Procfile.dev:
# Procfile.dev
web: bin/rails server -p 3000
css: bin/rails tailwindcss:watch
js: yarn build --watch
tunnel: jetty share 3000 --subdomain myapp-dev
Now bin/dev starts everything including the tunnel!
Project-specific configuration
Create a .jetty.yml config in your Rails project:
# .jetty.yml
subdomain: myapp-dev
port: 3000
region: us-east
Then just run:
jetty share
It'll read the config from the file. (Check Jetty docs to see if this feature exists—implement if requested!)
Testing across devices
Quick device testing setup:
# Reserve a stable subdomain
jetty share 3000 --subdomain myapp-test
# Save as QR code for phone scanning
echo "https://myapp-test.tunnels.usejetty.online" | qrencode -t ansiutf8
# Or just share the link via Slack/iMessage
Open on phone, tablet, and desktop simultaneously to test responsive design!
Webhook debugging with Rails logger
Add extra logging for webhook requests:
# app/controllers/webhooks/base_controller.rb
class Webhooks::BaseController < ApplicationController
skip_before_action :verify_authenticity_token
before_action :log_webhook
private
def log_webhook
Rails.logger.info "=" * 80
Rails.logger.info "Webhook received: #{request.path}"
Rails.logger.info "Headers: #{request.headers.to_h.select { |k,v| k.start_with?('HTTP_') }}"
Rails.logger.info "Body: #{request.body.read}"
request.body.rewind
Rails.logger.info "=" * 80
end
end
# Inherit in specific webhook controllers
class Webhooks::StripeController < Webhooks::BaseController
# ...
end
Watch the Rails logs while testing webhooks through your Jetty tunnel!
Tunnel status check
Create a health check endpoint:
# config/routes.rb
get '/health', to: 'health#index'
# app/controllers/health_controller.rb
class HealthController < ApplicationController
def index
render json: {
status: 'ok',
timestamp: Time.current,
via: request.headers['X-Forwarded-For'] ? 'jetty_tunnel' : 'direct',
tunnel_url: ENV['JETTY_TUNNEL_URL']
}
end
end
Check it works:
curl https://myapp-dev.tunnels.usejetty.online/health
Share a specific branch
When working on feature branches:
# Name tunnel after your branch
git branch --show-current | xargs -I {} jetty share 3000 --subdomain myapp-{}
# Example: On branch "feature/new-ui", creates:
# https://myapp-feature-new-ui.tunnels.usejetty.online
Perfect for sharing work-in-progress features!
Next Steps
You're now a Jetty + Rails expert.
Explore more:
- Tunnel Reference - Advanced tunnel features
- Custom Domains - Use your own domain
- Laravel Guide - Check out the Laravel integration too!
Build something awesome:
- Share your Rails app with your team
- Test Stripe webhooks in development
- Demo features to clients without deploying
- Develop your Rails API while testing with mobile apps
Need help? Drop a message in our community or check the main docs!
Send feedback
Found an issue or have a suggestion? Let us know.