Deployment Topologies
View SourceThe 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 docs, 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 that it's also easy to create a configuration that is 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 boot in the node hosting sitea.com
and only :site_b
in the node hosting siteb.com
.
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.
For these cases, whenever possible, Beacon will emit warnings during the boot process.
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:
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
Below we'll describe some common deployment strategies but Beacon is not limited to the strategies below, you can adapt to your needs.
1. Single application on the same host
The most simple strategy is a single project with one or more sites and the admin interface in the same host.
# 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
2. Clustered single applications
Same project as the previous strategy but with multiple nodes deployed in the same cluster.
Gives more capacity to serve more requests but still sharing the same Endpoint and same host.
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
3. 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.
That scenario also opens the possibility to deploy the admin interface behind a VPN to increase security.
Note that in order for BeaconLiveAdmin to find all running 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.
4. Multiple hosts in single project
Still a single project but now it will serve each site on its own host (domain).
This scenario introduces a Proxy Endpoint to route requests to the appropriate Endpoint serving the site,
this configuration can be generated with mix beacon.gen.site --site my_site --host mysite.com
- see docs for more info.
In this case we're still deploying just one application but serving multiple domains for each site:
flowchart TD
subgraph Node1["Node1"]
proxy["ProxyEndpoint"]
site_a["site: :my_site, endpoint: MySiteEndpoint"]
site_b["site: :campaigns, endpoint: CampaignsEndpoint"]
subgraph Admin["/admin"]
admin_site_a[":my_site"]
admin_site_b[":campaigns"]
end
end
subgraph Cluster["Cluster"]
Node1
Admin
end
site_a --> admin_site_a
site_b --> admin_site_b
r1["mysite.com"] --> proxy
r2["campaigns.mysite.com"] --> proxy
proxy --> site_a
proxy --> site_b
r3["mysite.com/admin"] --> Admin