# Why rails s Ignores Your Puma Binds (The Hidden PORT Trap)

## TL;DR

```plaintext
- 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

```plaintext
bind "tcp://127.0.0.1:3000"
bind "tcp://172.17.0.1:3000"
```

```plaintext
bundle exec puma
```

→ both binds active

## Actual

```plaintext
rails s
```

→ single bind (`localhost:PORT`)

## Execution Path

```plaintext
rails s → Rails::Server → Rack → Puma
```

```plaintext
# 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 `:binds` wins (no merge)

```plaintext
# https://github.com/puma/puma/blob/v7.2.0/lib/rack/handler/puma.rb#L96-L116
config.clear_binds! if host || port
```

```plaintext
# 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 → user*config → clear*binds! → file binds ignored

## Observed

| Command | PORT | Result |
| --- | --- | --- |
| rails s | no | all binds |
| rails s | yes | single bind |
| puma | any | all binds |

## Fix

```plaintext
app_port = ENV.fetch("MY_APP_PORT",3000)
bind "tcp://127.0.0.1:#{app_port}"
bind "tcp://172.17.0.1:#{app_port}"
```

```plaintext
MY_APP_PORT=3000 rails s
```

## Summary

*   Rails promotes PORT → user config
    
*   Puma replaces binds (no merge)
    
*   Multi-bind requires PORT unset
