Deployment Topologies

View Source

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 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