diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index f622e3686..d1825c041 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -5,7 +5,9 @@ x-db-env: &db_env x-rails-env: &rails_env DB_HOST: db - HOST: "0.0.0.0" + # Bind the dev server to all interfaces inside the container so Docker's + # published port reaches it from the host. Rails reads BINDING natively. + BINDING: "0.0.0.0" POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres BUNDLE_PATH: /bundle diff --git a/Procfile.dev b/Procfile.dev index eb6eadebd..a868e97a4 100644 --- a/Procfile.dev +++ b/Procfile.dev @@ -1,3 +1,3 @@ -web: bundle exec ${DEBUG:+rdbg -O -n -c --} bin/rails server -b 0.0.0.0 +web: bundle exec ${DEBUG:+rdbg -O -n -c --} bin/rails server css: bundle exec bin/rails tailwindcss:watch 2>/dev/null worker: bundle exec sidekiq diff --git a/compose.example.ai.yml b/compose.example.ai.yml index bac60163e..1265bb23d 100644 --- a/compose.example.ai.yml +++ b/compose.example.ai.yml @@ -255,10 +255,16 @@ services: # bypassing Pipelock's MCP scanning (auth token is still required). # For hardened deployments, use `expose: [3000]` instead and front # the web UI with a separate reverse proxy. + # + # To also publish on IPv6 (dual-stack), uncomment the line below AND + # set BINDING=:: in the environment block. See docs/hosting/docker.md + # "Binding to IPv6" for details. + # - "[::]:${PORT:-3000}:3000" - ${PORT:-3000}:3000 restart: unless-stopped environment: <<: *rails_env + # BINDING: "::" # Uncomment for IPv6 dual-stack inside the container depends_on: db: condition: service_healthy diff --git a/compose.example.yml b/compose.example.yml index b78ff4516..487234038 100644 --- a/compose.example.yml +++ b/compose.example.yml @@ -66,9 +66,14 @@ services: - app-storage:/rails/storage ports: - ${PORT:-3000}:3000 + # To also publish on IPv6 (dual-stack), uncomment the line below AND + # set BINDING=:: in the environment block. See docs/hosting/docker.md + # "Binding to IPv6" for details. + # - "[::]:${PORT:-3000}:3000" restart: unless-stopped environment: <<: *rails_env + # BINDING: "::" # Uncomment for IPv6 dual-stack inside the container depends_on: db: condition: service_healthy diff --git a/config/puma.rb b/config/puma.rb index 47a2362e2..04022936a 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -39,6 +39,8 @@ if rails_env == "production" end # Specifies the `port` that Puma will listen on to receive requests; default is 3000. +# The bind host is controlled via the Rails-native `BINDING` env var (set to +# `0.0.0.0` in containers, or `::` for IPv6 dual-stack). See docs/hosting/docker.md. port ENV.fetch("PORT") { 3000 } # Specifies the `environment` that Puma will run in. diff --git a/docs/hosting/docker.md b/docs/hosting/docker.md index 09114ed09..96ec28065 100644 --- a/docs/hosting/docker.md +++ b/docs/hosting/docker.md @@ -111,6 +111,43 @@ and change it to `true` RAILS_ASSUME_SSL: "true" ``` +#### Binding to IPv6 (optional) + +By default Sure listens on `0.0.0.0:3000` (IPv4 wildcard) inside the container and Docker publishes the port on the host's IPv4 interface only. If you want the app reachable over IPv6 as well, two things need to change: + +1. **Tell the app to bind to `[::]`** by setting `BINDING=::` in the container environment. `BINDING` is Rails' native env var for the server bind address. On any kernel with `net.ipv6.bindv6only=0` (the default on Linux and macOS) a single `[::]` bind is **dual-stack**: it accepts both IPv6 and IPv4 clients from the same socket. You do not need two binds and you do not need two ports. +2. **Tell Docker to publish the host port on IPv6** by adding a bracketed-host `ports:` entry alongside the existing IPv4 one. + +In `compose.yml`: + +```yaml +services: + web: + ports: + - ${PORT:-3000}:3000 + - "[::]:${PORT:-3000}:3000" + environment: + <<: *rails_env + BINDING: "::" +``` + +With both changes in place, `http://127.0.0.1:3000/` and `http://[::1]:3000/` both work against the same container. + +**Note:** Docker's default userland proxy already bridges host-side IPv6 publishes to the container's internal IPv4 address, so in many setups just adding the `[::]:` port entry is enough. Setting `BINDING=::` inside the container only becomes load-bearing when the Docker daemon has `"ipv6": true` + `"ip6tables": true` configured (uncommon for self-hosters) and forwards raw IPv6 packets into the container via netfilter instead of the proxy. Setting both is harmless and future-proof. + +If you are running behind a reverse proxy that terminates TLS, nothing else changes — `proxy_pass http://[::1]:3000` and `proxy_pass http://127.0.0.1:3000` both work because the `[::]` bind is dual-stack. + +#### Local development bind + +For `bin/dev` on your own machine, the server now defaults to Rails' native `localhost` bind (`127.0.0.1` + `[::1]`) — only reachable from the same machine. If you need external access (phone on the same WiFi, devcontainer port forwarding, LAN testing), set the Rails-native env var: + +```bash +BINDING=0.0.0.0 bin/dev # reachable from LAN +BINDING=:: bin/dev # IPv6 dual-stack +``` + +The bundled devcontainer at `.devcontainer/docker-compose.yml` already pins `BINDING: "0.0.0.0"` so Docker port forwarding reaches the app — no manual override needed when using the devcontainer. + ### Step 4: Run the app You are now ready to run the app. Start with the following command to make sure everything is working: