Phoenix listen on additional port?

I have a bit of a bizar production setup, my apps run on a VPS that only has an IPv6 address,
to support IPv4 I am using a HaProxy with the PROXY_PROTOCOL.

Currently I am running NGINX to support this:

  • listens on port 80, redirects to https on port 443
  • listens on port 443 https reverse proxies to phoenix
  • listens on port 1443 https with PROXY_PROTOCOL

This works but is a bother, I would prefer to just run Phoenix (with site_encrypt).

So the real question is, can I have Phoenix start listening on an additional?
This port would then have protocol_options: [ proxy_header: true ], because cowboy supports the PROXY_PROTOCOL.

I think the Cowboy2Adapter in Phoenix only allows one configuration for http and one for https per endpoint.
You could create a separate Endpoint for the proxy stuff (e.g. TestWeb.Endpoint and TestWeb.ProxyEndpoint) and start both of them with different configurations.

1 Like

As a quick and dirty option, I copied the code from the Cowboy2Adapter and allowed it to return multiple configurations, and that works.

Now I need to think how I can add the options I want to the other ports.

Changing the code to allow for multiple :http and :https keys works, but site_encrypt apparently replaces the :https key, so my additional ones get removed. :unamused:

  @doc false
  def child_specs(endpoint, config) do
    otp_app = Keyword.fetch!(config, :otp_app)

    refs_and_specs =
      for {scheme, port} <- [http: 4000, https: 4040],
          opts when opts != false <- :proplists.get_all_values(scheme, config) do

        port = :proplists.get_value(:port, opts, port)

        unless port do
          Logger.error(":port for #{scheme} config is nil, cannot start server")
          raise "aborting due to nil port"

        opts = [port: port_to_integer(port), otp_app: otp_app] ++ :proplists.delete(:port, opts)

        child_spec(scheme, endpoint, opts)

    {refs, child_specs} = Enum.unzip(refs_and_specs)

    if drainer = refs != [] && Keyword.get(config, :drainer, []) do
      child_specs ++ [{Plug.Cowboy.Drainer, Keyword.put_new(drainer, :refs, refs)}]

Replacing the for loop with this works :grinning:

      for {scheme, default_port} <- [http: 4000, https: 4040],
          opts = config[scheme],
          port <- :proplists.get_all_values(:port, opts) do
        {opts, port} =
          cond do
            is_list(port) ->
              port_ = :proplists.get_value(:port, port, default_port)
              opts_ = Keyword.merge(opts, port, fn _key, _v1, v2 -> v2 end)
              {opts_, port_}
            true ->
              {opts, port}

This requires me to write the config likes this:

  http: [ip: {127, 0 ,0 1}, port: 4000, port: [ port: 5000, protocol_options: [ proxy_header: true ]]],
  https: [port: 4040, port: [ port: 5050, protocol_options: [ proxy_header: true ]]],

The options given in the port keyword list will be merged in the original, overwriting any existing.