Skip to content

Instantly share code, notes, and snippets.

@cblavier
Last active April 30, 2023 08:33
Show Gist options
  • Star 16 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save cblavier/0c2cf3f101d82c32503774f179a66a3f to your computer and use it in GitHub Desktop.
Save cblavier/0c2cf3f101d82c32503774f179a66a3f to your computer and use it in GitHub Desktop.
Cowboy 2.5 proxy, used to bind a single port (on Heroku) for umbrella Phoenix applications. It supports HTTPS and websockets properly.
defmodule MasterProxy.Application do
alias MyApp.Endpoint, as: MyAppEndpoint
alias MyApp.UserSocket, as: MyAppUserSocket
alias MyOtherApp.Endpoint, as: MyOtherAppEndpoint
alias MyOtherApp.UserSocket, as: MyOtherAppUserSocket
alias Phoenix.LiveReloader.Socket, as: LiveReloadSocket
alias Plug.Cowboy
use Application
require Logger
@port Application.get_env(:master_proxy, :port)
def start(_type, _args) do
{:ok, pid} = in_phoenix?() |> children |> run_application()
Logger.info("successfully started master_proxy on port #{to_port(@port)}")
{:ok, pid}
end
defp run_application(children) do
import Supervisor.Spec, warn: false
Supervisor.start_link(
children,
[strategy: :one_for_one, name: MasterProxy.Supervisor]
)
end
defp children(_start_cowboy = false), do: []
defp children(_start_cowboy = true) do
[
Cowboy.child_spec(
plug: nil, # since we're using manual dispatch, plug is ignored
scheme: :http,
options: [
port: to_port(@port),
dispatch: [{:_, [
websocket_handler("/my_app/live_reload/socket/websocket", MyAppEndpoint, {LiveReloadSocket, :websocket}),
websocket_handler("/my_other_app/live_reload/socket/websocket", MyOtherAppEndpoint, {LiveReloadSocket, :websocket}),
websocket_handler("/my_app/socket/websocket", MyAppEndpoint, {MyAppUserSocket, websocket: true}),
websocket_handler("/my_other_app/socket/websocket", MyOtherAppEndpoint, {MyOtherAppUserSocket, websocket: true}),
{:_, Cowboy.Handler, {MasterProxy.Plug, []}}
]}]
]
)
]
end
defp websocket_handler(path, endpoint, options) do
{path, Phoenix.Endpoint.Cowboy2Handler, {endpoint, options}}
end
# we only want the proxy to start when phoenix is started as well
# (not in iex or tests)
defp in_phoenix? do
Application.get_env(:phoenix, :serve_endpoints)
end
defp to_port(nil) do
Logger.error "Server can't start because :port in config is nil, please use a valid port number"
exit(:shutdown)
end
defp to_port(binary) when is_binary(binary), do: String.to_integer(binary)
defp to_port(integer) when is_integer(integer), do: integer
defp to_port({:system, env_var}), do: to_port(System.get_env(env_var))
end
use Mix.Config
config :master_proxy, port: 5000
import_config "#{Mix.env}.exs"
defmodule MasterProxy.Plug do
def init(options) do
options
end
def call(conn, _opts) do
if conn.request_path =~ ~r{^/other_app} do
MyOtherApp.Endpoint.call(conn, [])
else
MyApp.Endpoint.call(conn, [])
end
end
end
use Mix.Config
config :master_proxy,
port: {:system, "PORT"}
@scorsi
Copy link

scorsi commented Jun 8, 2020

Hello @cblavier,
I'm running into issues with your sample.
The Phoenix LiveReload isn't running correctly, are you experiencing some issues too ?
It did detect changes on code, actualize pages but code isn't updated. I'm running Phoenix 1.5.3 with LiveView. I did have change the path of live reloading to "/phoenix/...". Everything else is running correctly : LiveViews, Sockets and LiveReload socket connection (and page actualization).
It's a quite weird behaviour here.
Thanks,

@sbouaked
Copy link

Hello ! @scorsi,
I succeed with LiveReload, but not with the LiveView socket :s
How did you manage it ?

@scorsi
Copy link

scorsi commented Feb 14, 2021

It’s far too old... dont remember sorry

@cblavier
Copy link
Author

Hey @sbouaked I'm still using this setup on my main project with Liveview, I'll check if this gist is still up to date
(sorry @scorsi for not answering, I guess I missed the mention 🤷‍♂️)

@cblavier
Copy link
Author

cblavier commented Feb 14, 2021

The gist is still accurate, the only different mention is that I also added LiveView sockets :

websocket_handler(
  "/my_app/live/websocket",
  MyAppEndpoint,
  {Phoenix.LiveView.Socket, :websocket}
),
websocket_handler(
  "/my_other_app/live/websocket",
  MyOtherAppEndpoint,
  {Phoenix.LiveView.Socket, :websocket}
)

in my JS, it looks like this:

const liveSocket = new LiveSocket('/my_app/live', Socket, { ... })

Let me know if it's working for you!

@sbouaked
Copy link

sbouaked commented Feb 15, 2021

Yeah thanks it's working ! (But have to make a phx.digest)
I don't know why, I don't succeed to change the path.

My websocket_handler for an app looks like :

 websocket_handler(
   "/live/websocket",
   FortressWeb.Endpoint,
   {Phoenix.LiveView.Socket, :websocket}
 ),
 websocket_handler(
   "/phoenix/live_reload/socket/websocket",
   FortressWeb.Endpoint,
   {Phoenix.LiveReloader.Socket, :websocket}
 ),
 websocket_handler(
   "/socket/websocket",
   FortressWeb.Endpoint,
   {FortressWeb.UserSocket, websocket: true}
 ),

I trying to change "/live/websocket" for "/fortress_web/live/websocket", by changing
let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}}) to let liveSocket = new LiveSocket("/fortress_web/live", Socket, {params: {_csrf_token: csrfToken}}) but doesn't work :/

@cblavier
Copy link
Author

In your apps/fortress_web/lib/fortress_web/endpoint.ex did you change your socket path to /fortress_web/live as well?

@sbouaked
Copy link

sbouaked commented Feb 15, 2021

yep :/

socket "/fortress_web/socket", FortressWeb.UserSocket,
    websocket: true,
    longpoll: false

socket "/fortress_web/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]

@sbouaked
Copy link

I don't understand why "/live/websocket" is working with LiveSocket("/live", Socket, {params: ... }) ^^'

@cblavier
Copy link
Author

When you expose a Phoenix Socket, it's available for two different protocols : websocket & long polling. That's why the actual websocket endpoint is /my_socket/websocket

@sbouaked
Copy link

Oh! ok thanks 🙏

@sukidhar
Copy link

sukidhar commented Feb 21, 2022

@cblavier Is there a sample repository which implemented this... I am kinda new to umbrella apps.. , I want to host my admin app in umbrella using subdomain

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment