Why rails s Ignores Your Puma Binds (The Hidden PORT Trap)
TL;DR
- ENV["PORT"] → treated as user-supplied by Rails
- Puma prioritizes user config → calls clear_binds!
- All binds in config/puma.rb are ignored
- Result: single bind only
- Fix: use MY_APP_PORT instead
Problem
config/puma.rb defines multiple binds, but rails s results in a single bind.
Expected
bind "tcp://127.0.0.1:3000"
bind "tcp://172.17.0.1:3000"
bundle exec puma
→ both binds active
Actual
rails s
→ single bind (localhost:PORT)
Execution Path
rails s → Rails::Server → Rack → Puma
# https://github.com/rails/rails/blob/v8.0.4/railties/lib/rails/commands/server/server_command.rb
user_supplied_options << :Port if ENV["PORT"]
Root Cause
| Tier | Source |
|---|---|
| user_config | env / CLI |
| file_config | puma.rb |
| default_config | internal |
First tier defining
:bindswins (no merge)
# https://github.com/puma/puma/blob/v7.2.0/lib/rack/handler/puma.rb#L96-L116
config.clear_binds! if host || port
# https://github.com/puma/puma/blob/v7.2.0/lib/puma/dsl.rb#L294-L296
def clear_binds!; @options[:binds]=[]; end
Flow: PORT → user-supplied → userconfig → clearbinds! → file binds ignored
Observed
| Command | PORT | Result |
|---|---|---|
| rails s | no | all binds |
| rails s | yes | single bind |
| puma | any | all binds |
Fix
app_port = ENV.fetch("MY_APP_PORT",3000)
bind "tcp://127.0.0.1:#{app_port}"
bind "tcp://172.17.0.1:#{app_port}"
MY_APP_PORT=3000 rails s
Summary
Rails promotes PORT → user config
Puma replaces binds (no merge)
Multi-bind requires PORT unset