View Source Deployment Topologies
The routing system in Phoenix combined with the OTP distribution model opens many possibilities for deployment strategies and in this guide we'll cover the most common ones.
Code examples might be abbreviated and infrastructure details like load balancers are not covered in this guide to keep it short.
Clustering your application is also not covered in this guide but you can find the documentation on the platform's site you're using, for example https://fly.io/docs/elixir/the-basics/clustering and https://www.gigalixir.com/docs/cluster.
Core Concepts
Firstly, we need to understand a few core concepts:
URL Generation
The URL of a Beacon Page inside a Phoenix Router scope
is generated by the combination of the Endpoint and the Router configuration as endpoint host
+ scope path
+ site prefix
+ page path
, for example:
# endpoint
config :my_app, MyAppWeb.Endpoint, url: [host: "mysite.com"]
# router scope
scope "/2024" do
beacon_site "/campaigns"
end
# beacon page
%Beacon.Content.Page{path: "christmas"}
Results in:
http://mysite.com/2024/campaigns/christmas`
^ ^ ^ ^
| | | |
endpoint | | page path
| site prefix
scope path
See Beacon.Router and Phoenix.Router for added context.
URL Matching
A common technique to host multiple sites (hosts) in the same application is matching the :host
in the scope:
# only matches requests from sitea.com
scope host: "sitea.com" do
# ...
end
# only matches requests from siteb.com
scope host: "siteb.com" do
# ...
end
The downside of having such flexibility is creating a configuration that is either invalid or not optimized. Take for instance this configuration:
scope host: "sitea.com" do
beacon_site "/", site: :site_a
end
scope host: "siteb.com" do
beacon_site "/", site: :site_b
end
Note that :site_b
will never match when deployed to sitea.com
and :site_a
will never match on siteb.com
.
You can already tell that starting :site_b
in the node that is hosting sitea.com
is a complete waste of resources since it will never match any request,
which is not a big problem when you have a couple of small sites, but that becomes a problem as your environment grows.
To avoid this problem, Beacon will selectively boot only the sites that are reachable in the current host, so in the example above,
only :site_a
will be booted in the node hosting sitea.com
and only :site_b
in the node hosting siteb.com
.
This behavior also applies to route conflicts as well, for example:
scope do
get "/", PageController, :hello
beacon_site "/", site: :site_a
end
Two routes in the same prefix will cause a conflict.
Or this other example:
scope do
beacon_site "/", site: :main_site
beacon_site "/campaigns", site: :campaigns_site
end
The macro beacon_site
creates a catch-all route /*
so the second site will never be reached since /campaigns
is a valid route for the first site.
Those might look obvious but that's a common source of confusion, especially in long and more complex router files. So Beacon won't try to boot sites that can't be reached, but a warning will be displayed.
Admin Sites Discovery
BeaconLiveAdmin is designed to scan the all apps connected in the same cluster to find running sites and make them available in the admin interface as displayed below:
graph TD
subgraph Cluster
subgraph ProjectA[Project A]
site_a[site_a]
project_a_site_b[site_b]
end
subgraph ProjectB[Project B]
site_b[site_b]
site_c[site_c]
end
subgraph Admin[Admin]
admin_site_a[site_a]
admin_site_b[site_b]
admin_siet_c[site_c]
end
site_a --> admin_site_a
site_b --> admin_site_b
project_a_site_b --> admin_site_b
site_c --> admin_siet_c
end
The most important word here is "running" sites. Suppose :site_a
in the node Project A is not running, either due to a route conflict, invalid configuration,
or any other reason, then Admin won't find it and it won't be available in the admin interface:
graph TD
subgraph Cluster
subgraph ProjectA[Project A]
project_a_site_b[site_b]
end
subgraph ProjectB[Project B]
site_b[site_b]
site_c[site_c]
end
subgraph Admin[Admin]
admin_site_b[site_b]
admin_siet_c[site_c]
end
site_b --> admin_site_b
project_a_site_b --> admin_site_b
site_c --> admin_siet_c
end
With these constraints in mind, let's check some deployment strategies.
Strategies
Single application
The most simple strategy is a single project with one or more sites and the admin interface in the same project.
# endpoint
config :my_app, MyAppWeb.Endpoint, url: [host: "mysite.com"]
# router
scope "/" do
beacon_live_admin "/admin"
beacon_site "/campaigns", site: :campaigns
beacon_site "/", site: :root
end
flowchart TD
subgraph Node1["Node1"]
site_a["/campaigns, site: :campaigns"]
site_b["/, site: :root"]
subgraph Admin["/admin"]
admin_site_a[":campaigns"]
admin_site_b[":root"]
end
end
subgraph Cluster["Cluster"]
Node1
Admin
end
site_a --> admin_site_a
site_b --> admin_site_b
r1["mysite.com/campaigns/christmas"] --> site_a
r2["mysite.com/contact"] --> site_b
r3["mysite.com/admin"] --> Admin
Clustered single applications
Same project as the previous strategy but with multiple nodes deployed in the same cluster.
flowchart TD
subgraph Node1["Node1"]
n1_site_a["/campaigns, site: :campaigns"]
n1_site_b["/, site: :root"]
subgraph Node1Admin["/admin"]
n1_admin_site_a[":campaigns"]
n1_admin_site_b[":root"]
end
end
subgraph Node2["Node2"]
n2_site_a["/campaigns, site: :campaigns"]
n2_site_b["/, site: :root"]
subgraph Node2Admin["/admin"]
n2_admin_site_a[":campaigns"]
n2_admin_site_b[":root"]
end
end
subgraph Cluster["Cluster"]
Node1
Node2
end
n1_site_a --> n1_admin_site_a
n1_site_b --> n1_admin_site_b
n2_site_a --> n2_admin_site_a
n2_site_b --> n2_admin_site_b
r1["mysite.com/campaigns/christmas"] --> n1_site_a
r1["mysite.com/campaigns/christmas"] --> n2_site_a
r2["mysite.com/contact"] --> n1_site_b
r2["mysite.com/contact"] --> n2_site_b
r3["mysite.com/admin"] --> Node1Admin
r3["mysite.com/admin"] --> Node2Admin
Clustered applications with separated admin
You can observe the previous strategy duplicates the admin interface in each node, which works fairly well when you have no more than a couple of sites, but that setup tends to become harder to manage and also become a waste of resources if you start booting more site and more nodes.
So an optimization is to move the Admin interface into its own project and node (a new Phoenix project), and keep the sites in their own projects.
Note that in order to Admin find the sites, all the apps must be connected in the same cluster.
# endpoint
config :my_app, MyAppWeb.Endpoint, url: [host: "admin.mysite.com"]
# router
scope "/" do
beacon_live_admin "/"
end
flowchart TD
subgraph Node1["NodeApp1"]
n1_site_a["/campaigns, site: :campaigns"]
n1_site_b["/, site: :root"]
end
subgraph Node2["NodeApp2"]
n2_site_a["/campaigns, site: :campaigns"]
n2_site_b["/, site: :root"]
end
subgraph NodeAdmin["NodeAdmin"]
admin_site_a[":campaigns"]
admin_site_b[":root"]
end
subgraph Cluster["Cluster"]
Node1
Node2
NodeAdmin
end
n1_site_a --> admin_site_a
n1_site_b --> admin_site_b
n2_site_a --> admin_site_a
n2_site_b --> admin_site_b
r1["mysite.com/campaigns/christmas"] --> n1_site_a
r1["mysite.com/campaigns/christmas"] --> n2_site_a
r2["mysite.com/contact"] --> n1_site_b
r2["mysite.com/contact"] --> n2_site_b
r3["admin.mysite.com"] --> NodeAdmin
A huge benefit of this topology is the flexibility to protect the Admin interface behind a VPN or scale it independently from the main applications.
Multiple hosts in single project, separated hosting apps
Still a single project but now serving multiple sites at the root path for different dynamic hosts.
In this example let's assume the most simple scenario where you'd create a new app in the hosting platform to serve each domain:
- App1 -> mysite.com
- App2 -> campaigns.mysite.com
That means isolated apps not connected to each other, just sharing the same codebase.
# endpoint
host = System.get_env("PHX_HOST")
config :my_app, MyAppWeb.Endpoint, url: [host: host]
# router
scope "/", host: "campaigns.mysite.com" do
beacon_live_admin "/admin"
beacon_site "/", site: :campaigns
end
scope "/", host: "mysite.com" do
beacon_live_admin "/admin"
beacon_site "/", site: :root
end
flowchart TD
subgraph Node1["Node1"]
n1_site["/, site: :root"]
subgraph Node1Admin["/admin"]
n1_admin_site[":root"]
end
end
subgraph Node2["Node1"]
n2_site["/, site: :campaigns"]
subgraph Node2Admin["/admin"]
n2_admin_site[":campaigns"]
end
end
subgraph App1["App mysite.com"]
subgraph Cluster1["Cluster"]
Node1
end
end
subgraph App2["App campaigns.mysite.com"]
subgraph Cluster2["Cluster"]
Node2
end
end
n1_site --> n1_admin_site
n2_site --> n2_admin_site
r1["mysite.com/campaigns/christmas"] --> n2_site
r2["mysite.com/contact"] --> n1_site
r3["mysite.com/admin"] --> Node1Admin
r4["campaigns.mysite.com/admin"] --> Node2Admin
Multiple hosts in single project, connected hosting apps
Similar setup as the previous strategy but now connecting the apps in the same cluster with a separated admin interface.
# endpoint
host = System.get_env("PHX_HOST")
config :my_app, MyAppWeb.Endpoint, url: [host: host]
# router
scope "/admin", host: "admin.mysite.com" do
beacon_live_admin "/"
end
scope "/", host: "campaigns.mysite.com" do
beacon_site "/", site: :campaigns
end
scope "/", host: "mysite.com" do
beacon_site "/", site: :root
end
flowchart TD
subgraph Node1["Node1"]
n1_site["/, site: :root"]
end
subgraph Node2["Node2"]
n2_site["/, site: :campaigns"]
end
subgraph Admin["NodeAdmin"]
admin_site_a[":root"]
admin_site_b[":campaigns"]
end
subgraph Cluster["Cluster"]
subgraph App1["App1 mysite.com"]
Node1
end
subgraph App2["App2 campaigns.mysite.com"]
Node2
end
subgraph AppAdmin["App admin.mysite.com"]
Admin
end
end
n1_site --> admin_site_a
n2_site --> admin_site_b
r1["mysite.com/campaigns/christmas"] --> n2_site
r2["mysite.com/contact"] --> n1_site
r3["admin.mysite.com"] --> Admin