Documentation for Jetty

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

  1. Never commit secrets - Use .env (in .gitignore) or Rails credentials
  2. Different secrets for tunnels vs production - Use test API keys when developing with Jetty
  3. Document required env vars - Add a .env.example with dummy values
  4. 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.online
  • rails-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

  1. Reserve a custom domain in your Jetty dashboard
  2. Add DNS records pointing to Jetty's edge servers
  3. 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.hosts to explicitly allow your Jetty tunnel domains
  • Keep force_ssl = false in 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_origins for 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:

  1. Check allowed_request_origins in config/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
  1. Restart Rails server after changing this config.

  2. 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:

  1. Make sure your layout includes CSRF meta tags:
<!-- app/views/layouts/application.html.erb -->
<head>
  <%= csrf_meta_tags %>
</head>
  1. If using Turbo, ensure you're on the latest version.

  2. 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:

  1. Browser → Jetty edge server
  2. Edge server → Your machine via WebSocket
  3. Your machine → Local Rails app (localhost:3000)
  4. 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:

  1. OpenSSL version: openssl version
  2. System certificates: brew install openssl (macOS) or apt 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:

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.