ccnames.dev

cnames.dev / blog

Tutorial12 min read

How to add custom domains to your Ruby on Rails app

Custom domains in Rails: the config.hosts gotcha that blocks unknown hosts, routing constraints, the real client IP, and SSL. DIY and the one-API-call way.

S
Saeed
July 3, 2026

Adding custom domains to a Rails app has one gotcha that stops most people cold: Rails rejects hosts it does not recognise. Get past config.hosts and the rest — tenant routing, real client IP, SSL — is straightforward. Here is the complete DIY path and the shortcut.

# config/application.rb — allow verified customer domains dynamically
config.hosts << /.*/ unless Rails.env.production?  # dev only; do NOT do this in prod

# Production: permit only hosts you have verified
config.hosts << ->(host) { Tenant.exists?(domain: host) }
config.host_authorization = { exclude: ->(req) { req.path == '/up' } } # health check

Step 1 — Point DNS at your app

The customer aims a record at infrastructure you control. Prefer a CNAME for subdomains:

app.customer.com.  CNAME  proxy.yourapp.com.

Bare domains hit the CNAME-at-apex problem; use an A record or ALIAS. Confirm with the DNS propagation checker.

Step 2 — The config.hosts gotcha

Since Rails 6, ActionDispatch::HostAuthorization guards against DNS-rebinding by rejecting any host not in config.hosts. A brand-new customer domain will get a “Blocked host” response until you permit it. The right fix is a matcher that checks your verified tenants — not clearing the list, which would disable the protection entirely:

config.hosts << ->(host) { Rails.cache.fetch("host:#{host}", expires_in: 5.minutes) { Tenant.exists?(domain: host) } }

Cache the lookup — it runs on every request.

Step 3 — Resolve the tenant

# app/controllers/concerns/tenant_scoped.rb
module TenantScoped
  extend ActiveSupport::Concern
  included { before_action :set_current_tenant }

  private

  def set_current_tenant
    return if request.host.end_with?('yourapp.com')
    Current.tenant = Tenant.find_by(domain: request.host)
    head :not_found unless Current.tenant   # fail closed
  end
end

Step 4 — Real client IP and SSL

Behind a proxy, tell Rails which hops to skip so request.remote_ip is the visitor, not your edge:

config.action_dispatch.trusted_proxies =
  ENV['EDGE_CIDRS'].split(',').map { |p| IPAddr.new(p) }

Rails does not terminate TLS. Put Caddy/Nginx in front with ACME (see the trade-offs in cnames.dev vs self-hosting Caddy on-demand TLS), or use a custom-domains service that issues certificates for you.

The one-API-call way

curl -X POST https://api.cnames.dev/v1/domains \
  -H "Authorization: Bearer sk_live_..." \
  -d '{"hostname":"app.customer.com","origin":"your-rails-app.internal:3000"}'

cnames.dev handles SSL, renewals, and forwarding; your Rails app keeps its config.hosts matcher and tenant lookup. It is a natural fit for course and community platforms. Full architecture in the complete guide.

Building a SaaS that needs custom domains? cnames.dev gives every customer their own domain — SSL, edge routing, and per-tenant isolation in one API call. Free for 25 domains, no demo call. Start free · Read the docs

Frequently asked questions

Why does my Rails app return "Blocked host" for custom domains?

Rails ships with ActionDispatch::HostAuthorization and only allows hosts listed in config.hosts (in development it defaults to localhost). Any customer domain that is not permitted is rejected with a Blocked host response. You must add customer domains to the allow-list, ideally dynamically.

How do I allow arbitrary customer domains in Rails?

Add a matcher to config.hosts that returns true for domains you have verified — for example a lambda that checks the host against your tenants table. Do not simply clear config.hosts in production; that disables DNS-rebinding protection for every request.

How do I get the real client IP in Rails behind a proxy?

Set config.action_dispatch.trusted_proxies to your edge ranges so request.remote_ip skips them and returns the actual client address from X-Forwarded-For.

Keep reading

S

SaeedFounder, cnames.dev — runs custom-domain infra for 5,000+ production sites at Lindo.ai.