<%= Phoenix.Flash.get(@flash, :info) %>
21 |<%= Phoenix.Flash.get(@flash, :error) %>
22 | <%= @inner_content %> 23 |├── .formatter.exs ├── .gitignore ├── LICENSE ├── README.md ├── assets ├── css │ ├── app.css │ └── phoenix.css ├── js │ ├── app.js │ └── socket.js └── static │ ├── favicon.ico │ ├── images │ ├── phoenix.png │ └── render.png │ └── robots.txt ├── build.sh ├── config ├── config.exs ├── dev.exs ├── prod.exs ├── releases.exs └── test.exs ├── lib ├── elixir_cluster_demo.ex ├── elixir_cluster_demo │ └── application.ex ├── elixir_cluster_demo_web.ex └── elixir_cluster_demo_web │ ├── channels │ └── user_socket.ex │ ├── controllers │ └── page_controller.ex │ ├── endpoint.ex │ ├── gettext.ex │ ├── router.ex │ ├── templates │ ├── layout │ │ └── app.html.heex │ └── page │ │ └── index.html.heex │ └── views │ ├── error_helpers.ex │ ├── error_view.ex │ ├── layout_view.ex │ └── page_view.ex ├── mix.exs ├── mix.lock ├── priv └── gettext │ ├── en │ └── LC_MESSAGES │ │ └── errors.po │ └── errors.pot ├── rel ├── config.exs ├── config │ └── config.exs ├── plugins │ └── .gitignore ├── prod.vm.args └── vm.args └── test ├── elixir_cluster_demo_web ├── controllers │ └── page_controller_test.exs └── views │ ├── error_view_test.exs │ ├── layout_view_test.exs │ └── page_view_test.exs ├── support ├── channel_case.ex └── conn_case.ex └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:ecto, :ecto_sql, :phoenix], 3 | subdirectories: ["priv/*/migrations"], 4 | plugins: [Phoenix.LiveView.HTMLFormatter], 5 | inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"] 6 | ] 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | elixir_cluster_demo-*.tar 24 | 25 | # If NPM crashes, it generates a log, let's ignore it too. 26 | npm-debug.log 27 | 28 | # The directory NPM downloads your dependencies sources to. 29 | /assets/node_modules/ 30 | 31 | # Since we are building assets from assets/, 32 | # we ignore priv/static. You may want to comment 33 | # this depending on your deployment strategy. 34 | /priv/static/ 35 | 36 | # Files matching config/*.secret.exs pattern contain sensitive 37 | # data and you should not commit them into version control. 38 | # 39 | # Alternatively, you may comment the line below and commit the 40 | # secrets files as long as you replace their contents by environment 41 | # variables. 42 | /config/*.secret.exs 43 | 44 | # the soft link to the build dir 45 | /_render 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2019, Render Developers 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Distributed Elixir Cluster on Render 2 | 3 | This is a sample application demonstrating a distributed Elixir cluster on [Render](https://render.com) using [libcluster](https://hexdocs.pm/libcluster), [Phoenix](https://phoenixframework.org) and [Mix releases](https://hexdocs.pm/mix/Mix.Tasks.Release.html) introduced in [Elixir 1.9](https://elixir-lang.org/blog/2019/06/24/elixir-v1-9-0-released/). 4 | 5 | ## Deployment 6 | 7 | Follow the guide at https://render.com/docs/deploy-elixir-cluster. 8 | -------------------------------------------------------------------------------- /assets/css/app.css: -------------------------------------------------------------------------------- 1 | /* This file is for your main application css. */ 2 | 3 | @import "./phoenix.css"; 4 | -------------------------------------------------------------------------------- /assets/css/phoenix.css: -------------------------------------------------------------------------------- 1 | /* Includes some default style for the starter application. 2 | * This can be safely deleted to start fresh. 3 | */ 4 | 5 | /* Milligram v1.3.0 https://milligram.github.io 6 | * Copyright (c) 2017 CJ Patoilo Licensed under the MIT license 7 | */ 8 | 9 | *,*:after,*:before{box-sizing:inherit}html{box-sizing:border-box;font-size:62.5%}body{color:#000000;font-family:'Helvetica', 'Arial', sans-serif;font-size:1.6em;font-weight:300;line-height:1.6}blockquote{border-left:0.3rem solid #d1d1d1;margin-left:0;margin-right:0;padding:1rem 1.5rem}blockquote *:last-child{margin-bottom:0}.button,button,input[type='button'],input[type='reset'],input[type='submit']{background-color:#0069d9;border:0.1rem solid #0069d9;border-radius:.4rem;color:#fff;cursor:pointer;display:inline-block;font-size:1.1rem;font-weight:700;height:3.8rem;letter-spacing:.1rem;line-height:3.8rem;padding:0 3.0rem;text-align:center;text-decoration:none;text-transform:uppercase;white-space:nowrap}.button:focus,.button:hover,button:focus,button:hover,input[type='button']:focus,input[type='button']:hover,input[type='reset']:focus,input[type='reset']:hover,input[type='submit']:focus,input[type='submit']:hover{background-color:#606c76;border-color:#606c76;color:#fff;outline:0}.button[disabled],button[disabled],input[type='button'][disabled],input[type='reset'][disabled],input[type='submit'][disabled]{cursor:default;opacity:.5}.button[disabled]:focus,.button[disabled]:hover,button[disabled]:focus,button[disabled]:hover,input[type='button'][disabled]:focus,input[type='button'][disabled]:hover,input[type='reset'][disabled]:focus,input[type='reset'][disabled]:hover,input[type='submit'][disabled]:focus,input[type='submit'][disabled]:hover{background-color:#0069d9;border-color:#0069d9}.button.button-outline,button.button-outline,input[type='button'].button-outline,input[type='reset'].button-outline,input[type='submit'].button-outline{background-color:transparent;color:#0069d9}.button.button-outline:focus,.button.button-outline:hover,button.button-outline:focus,button.button-outline:hover,input[type='button'].button-outline:focus,input[type='button'].button-outline:hover,input[type='reset'].button-outline:focus,input[type='reset'].button-outline:hover,input[type='submit'].button-outline:focus,input[type='submit'].button-outline:hover{background-color:transparent;border-color:#606c76;color:#606c76}.button.button-outline[disabled]:focus,.button.button-outline[disabled]:hover,button.button-outline[disabled]:focus,button.button-outline[disabled]:hover,input[type='button'].button-outline[disabled]:focus,input[type='button'].button-outline[disabled]:hover,input[type='reset'].button-outline[disabled]:focus,input[type='reset'].button-outline[disabled]:hover,input[type='submit'].button-outline[disabled]:focus,input[type='submit'].button-outline[disabled]:hover{border-color:inherit;color:#0069d9}.button.button-clear,button.button-clear,input[type='button'].button-clear,input[type='reset'].button-clear,input[type='submit'].button-clear{background-color:transparent;border-color:transparent;color:#0069d9}.button.button-clear:focus,.button.button-clear:hover,button.button-clear:focus,button.button-clear:hover,input[type='button'].button-clear:focus,input[type='button'].button-clear:hover,input[type='reset'].button-clear:focus,input[type='reset'].button-clear:hover,input[type='submit'].button-clear:focus,input[type='submit'].button-clear:hover{background-color:transparent;border-color:transparent;color:#606c76}.button.button-clear[disabled]:focus,.button.button-clear[disabled]:hover,button.button-clear[disabled]:focus,button.button-clear[disabled]:hover,input[type='button'].button-clear[disabled]:focus,input[type='button'].button-clear[disabled]:hover,input[type='reset'].button-clear[disabled]:focus,input[type='reset'].button-clear[disabled]:hover,input[type='submit'].button-clear[disabled]:focus,input[type='submit'].button-clear[disabled]:hover{color:#0069d9}code{background:#f4f5f6;border-radius:.4rem;font-size:86%;margin:0 .2rem;padding:.2rem .5rem;white-space:nowrap}pre{background:#f4f5f6;border-left:0.3rem solid #0069d9;overflow-y:hidden}pre>code{border-radius:0;display:block;padding:1rem 1.5rem;white-space:pre}hr{border:0;border-top:0.1rem solid #f4f5f6;margin:3.0rem 0}input[type='email'],input[type='number'],input[type='password'],input[type='search'],input[type='tel'],input[type='text'],input[type='url'],textarea,select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent;border:0.1rem solid #d1d1d1;border-radius:.4rem;box-shadow:none;box-sizing:inherit;height:3.8rem;padding:.6rem 1.0rem;width:100%}input[type='email']:focus,input[type='number']:focus,input[type='password']:focus,input[type='search']:focus,input[type='tel']:focus,input[type='text']:focus,input[type='url']:focus,textarea:focus,select:focus{border-color:#0069d9;outline:0}select{background:url('data:image/svg+xml;utf8,') center right no-repeat;padding-right:3.0rem}select:focus{background-image:url('data:image/svg+xml;utf8,')}textarea{min-height:6.5rem}label,legend{display:block;font-size:1.6rem;font-weight:700;margin-bottom:.5rem}fieldset{border-width:0;padding:0}input[type='checkbox'],input[type='radio']{display:inline}.label-inline{display:inline-block;font-weight:normal;margin-left:.5rem}.row{display:flex;flex-direction:column;padding:0;width:100%}.row.row-no-padding{padding:0}.row.row-no-padding>.column{padding:0}.row.row-wrap{flex-wrap:wrap}.row.row-top{align-items:flex-start}.row.row-bottom{align-items:flex-end}.row.row-center{align-items:center}.row.row-stretch{align-items:stretch}.row.row-baseline{align-items:baseline}.row .column{display:block;flex:1 1 auto;margin-left:0;max-width:100%;width:100%}.row .column.column-offset-10{margin-left:10%}.row .column.column-offset-20{margin-left:20%}.row .column.column-offset-25{margin-left:25%}.row .column.column-offset-33,.row .column.column-offset-34{margin-left:33.3333%}.row .column.column-offset-50{margin-left:50%}.row .column.column-offset-66,.row .column.column-offset-67{margin-left:66.6666%}.row .column.column-offset-75{margin-left:75%}.row .column.column-offset-80{margin-left:80%}.row .column.column-offset-90{margin-left:90%}.row .column.column-10{flex:0 0 10%;max-width:10%}.row .column.column-20{flex:0 0 20%;max-width:20%}.row .column.column-25{flex:0 0 25%;max-width:25%}.row .column.column-33,.row .column.column-34{flex:0 0 33.3333%;max-width:33.3333%}.row .column.column-40{flex:0 0 40%;max-width:40%}.row .column.column-50{flex:0 0 50%;max-width:50%}.row .column.column-60{flex:0 0 60%;max-width:60%}.row .column.column-66,.row .column.column-67{flex:0 0 66.6666%;max-width:66.6666%}.row .column.column-75{flex:0 0 75%;max-width:75%}.row .column.column-80{flex:0 0 80%;max-width:80%}.row .column.column-90{flex:0 0 90%;max-width:90%}.row .column .column-top{align-self:flex-start}.row .column .column-bottom{align-self:flex-end}.row .column .column-center{-ms-grid-row-align:center;align-self:center}@media (min-width: 40rem){.row{flex-direction:row;margin-left:-1.0rem;width:calc(100% + 2.0rem)}.row .column{margin-bottom:inherit;padding:0 1.0rem}}a{color:#0069d9;text-decoration:none}a:focus,a:hover{color:#606c76}dl,ol,ul{list-style:none;margin-top:0;padding-left:0}dl dl,dl ol,dl ul,ol dl,ol ol,ol ul,ul dl,ul ol,ul ul{font-size:90%;margin:1.5rem 0 1.5rem 3.0rem}ol{list-style:decimal inside}ul{list-style:circle inside}.button,button,dd,dt,li{margin-bottom:1.0rem}fieldset,input,select,textarea{margin-bottom:1.5rem}blockquote,dl,figure,form,ol,p,pre,table,ul{margin-bottom:2.5rem}table{border-spacing:0;width:100%}td,th{border-bottom:0.1rem solid #e1e1e1;padding:1.2rem 1.5rem;text-align:left}td:first-child,th:first-child{padding-left:0}td:last-child,th:last-child{padding-right:0}b,strong{font-weight:bold}p{margin-top:0}h1,h2,h3,h4,h5,h6{font-weight:300;letter-spacing:-.1rem;margin-bottom:2.0rem;margin-top:0}h1{font-size:4.6rem;line-height:1.2}h2{font-size:3.6rem;line-height:1.25}h3{font-size:2.8rem;line-height:1.3}h4{font-size:2.2rem;letter-spacing:-.08rem;line-height:1.35}h5{font-size:1.8rem;letter-spacing:-.05rem;line-height:1.5}h6{font-size:1.6rem;letter-spacing:0;line-height:1.4}img{max-width:100%}.clearfix:after{clear:both;content:' ';display:table}.float-left{float:left}.float-right{float:right} 10 | 11 | /* General style */ 12 | h1{font-size: 3.6rem; line-height: 1.25} 13 | h2{font-size: 2.8rem; line-height: 1.3} 14 | h3{font-size: 2.2rem; letter-spacing: -.08rem; line-height: 1.35} 15 | h4{font-size: 1.8rem; letter-spacing: -.05rem; line-height: 1.5} 16 | h5{font-size: 1.6rem; letter-spacing: 0; line-height: 1.4} 17 | h6{font-size: 1.4rem; letter-spacing: 0; line-height: 1.2} 18 | 19 | .container{ 20 | margin: 0 auto; 21 | max-width: 80.0rem; 22 | padding: 0 2.0rem; 23 | position: relative; 24 | width: 100% 25 | } 26 | select { 27 | width: auto; 28 | } 29 | 30 | /* Alerts and form errors */ 31 | .alert { 32 | padding: 15px; 33 | margin-bottom: 20px; 34 | border: 1px solid transparent; 35 | border-radius: 4px; 36 | } 37 | .alert-info { 38 | color: #31708f; 39 | background-color: #d9edf7; 40 | border-color: #bce8f1; 41 | } 42 | .alert-warning { 43 | color: #8a6d3b; 44 | background-color: #fcf8e3; 45 | border-color: #faebcc; 46 | } 47 | .alert-danger { 48 | color: #a94442; 49 | background-color: #f2dede; 50 | border-color: #ebccd1; 51 | } 52 | .alert p { 53 | margin-bottom: 0; 54 | } 55 | .alert:empty { 56 | display: none; 57 | } 58 | .help-block { 59 | color: #a94442; 60 | display: block; 61 | margin: -1rem 0 2rem; 62 | } 63 | 64 | /* Phoenix promo and logo */ 65 | .phx-hero { 66 | text-align: center; 67 | border-bottom: 1px solid #e3e3e3; 68 | background: #eee; 69 | border-radius: 6px; 70 | padding: 3em; 71 | margin-bottom: 3rem; 72 | font-weight: 200; 73 | font-size: 120%; 74 | } 75 | .phx-hero p { 76 | margin: 0; 77 | } 78 | .phx-logo { 79 | min-width: 300px; 80 | margin: 1rem; 81 | display: block; 82 | } 83 | .phx-logo img { 84 | width: auto; 85 | display: block; 86 | } 87 | 88 | /* Headers */ 89 | header { 90 | width: 100%; 91 | background: #fdfdfd; 92 | border-bottom: 1px solid #eaeaea; 93 | margin-bottom: 2rem; 94 | } 95 | header section { 96 | align-items: center; 97 | display: flex; 98 | flex-direction: column; 99 | justify-content: space-between; 100 | } 101 | header section :first-child { 102 | order: 2; 103 | } 104 | header section :last-child { 105 | order: 1; 106 | } 107 | header nav ul, 108 | header nav li { 109 | margin: 0; 110 | padding: 0; 111 | display: block; 112 | text-align: right; 113 | white-space: nowrap; 114 | } 115 | header nav ul { 116 | margin: 1rem; 117 | margin-top: 0; 118 | } 119 | header nav a { 120 | display: block; 121 | } 122 | 123 | @media (min-width: 40.0rem) { /* Small devices (landscape phones, 576px and up) */ 124 | header section { 125 | flex-direction: row; 126 | } 127 | header nav ul { 128 | margin: 1rem; 129 | } 130 | .phx-logo { 131 | flex-basis: 527px; 132 | margin: 2rem 1rem; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /assets/js/app.js: -------------------------------------------------------------------------------- 1 | // We need to import the CSS so that webpack will load it. 2 | // The MiniCssExtractPlugin is used to separate it out into 3 | // its own CSS file. 4 | import css from "../css/app.css" 5 | 6 | // webpack automatically bundles all modules in your 7 | // entry points. Those entry points can be configured 8 | // in "webpack.config.js". 9 | // 10 | // Import dependencies 11 | // 12 | import "phoenix_html" 13 | 14 | // Import local files 15 | // 16 | // Local files can be imported directly using relative paths, for example: 17 | // import socket from "./socket" 18 | -------------------------------------------------------------------------------- /assets/js/socket.js: -------------------------------------------------------------------------------- 1 | // NOTE: The contents of this file will only be executed if 2 | // you uncomment its entry in "assets/js/app.js". 3 | 4 | // To use Phoenix channels, the first step is to import Socket, 5 | // and connect at the socket path in "lib/web/endpoint.ex". 6 | // 7 | // Pass the token on params as below. Or remove it 8 | // from the params if you are not using authentication. 9 | import {Socket} from "phoenix" 10 | 11 | let socket = new Socket("/socket", {params: {token: window.userToken}}) 12 | 13 | // When you connect, you'll often need to authenticate the client. 14 | // For example, imagine you have an authentication plug, `MyAuth`, 15 | // which authenticates the session and assigns a `:current_user`. 16 | // If the current user exists you can assign the user's token in 17 | // the connection for use in the layout. 18 | // 19 | // In your "lib/web/router.ex": 20 | // 21 | // pipeline :browser do 22 | // ... 23 | // plug MyAuth 24 | // plug :put_user_token 25 | // end 26 | // 27 | // defp put_user_token(conn, _) do 28 | // if current_user = conn.assigns[:current_user] do 29 | // token = Phoenix.Token.sign(conn, "user socket", current_user.id) 30 | // assign(conn, :user_token, token) 31 | // else 32 | // conn 33 | // end 34 | // end 35 | // 36 | // Now you need to pass this token to JavaScript. You can do so 37 | // inside a script tag in "lib/web/templates/layout/app.html.eex": 38 | // 39 | // 40 | // 41 | // You will need to verify the user token in the "connect/3" function 42 | // in "lib/web/channels/user_socket.ex": 43 | // 44 | // def connect(%{"token" => token}, socket, _connect_info) do 45 | // # max_age: 1209600 is equivalent to two weeks in seconds 46 | // case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do 47 | // {:ok, user_id} -> 48 | // {:ok, assign(socket, :user, user_id)} 49 | // {:error, reason} -> 50 | // :error 51 | // end 52 | // end 53 | // 54 | // Finally, connect to the socket: 55 | socket.connect() 56 | 57 | // Now that you are connected, you can join channels with a topic: 58 | let channel = socket.channel("topic:subtopic", {}) 59 | channel.join() 60 | .receive("ok", resp => { console.log("Joined successfully", resp) }) 61 | .receive("error", resp => { console.log("Unable to join", resp) }) 62 | 63 | export default socket 64 | -------------------------------------------------------------------------------- /assets/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/render-examples/elixir_cluster/49060c5c9e6b9b148d2204f759b1ec3ac51fbe1f/assets/static/favicon.ico -------------------------------------------------------------------------------- /assets/static/images/phoenix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/render-examples/elixir_cluster/49060c5c9e6b9b148d2204f759b1ec3ac51fbe1f/assets/static/images/phoenix.png -------------------------------------------------------------------------------- /assets/static/images/render.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/render-examples/elixir_cluster/49060c5c9e6b9b148d2204f759b1ec3ac51fbe1f/assets/static/images/render.png -------------------------------------------------------------------------------- /assets/static/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # exit on error 3 | set -o errexit 4 | 5 | 6 | # Initial setup 7 | mix deps.get --only prod 8 | MIX_ENV=prod mix compile 9 | 10 | # Compile assets 11 | cp -r assets/static/ priv/static/ 12 | mix assets.deploy 13 | 14 | # Build the release and overwrite the existing release directory 15 | MIX_ENV=prod mix release --overwrite 16 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Config module. 3 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | 7 | # General application configuration 8 | import Config 9 | 10 | # Configures the endpoint 11 | config :elixir_cluster_demo, ElixirClusterDemoWeb.Endpoint, 12 | url: [host: "localhost"], 13 | secret_key_base: "a8BaBpsM9V7mluSGJxUAVUBcsut8LgO0Hu1rXk8a/yt3QEc2A1GIkrxzQUHcuCzb", 14 | render_errors: [view: ElixirClusterDemoWeb.ErrorView, accepts: ~w(html json)], 15 | pubsub_server: ElixirClusterDemo.PubSub 16 | 17 | config :esbuild, 18 | version: "0.20.0", 19 | default: [ 20 | args: ~w(js/app.js --bundle --target=es2016 --outdir=../priv/static/assets), 21 | cd: Path.expand("../assets", __DIR__), 22 | env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)} 23 | ] 24 | 25 | # Configures Elixir's Logger 26 | config :logger, :console, 27 | format: "$time $metadata[$level] $message\n", 28 | metadata: [:request_id] 29 | 30 | # Use Jason for JSON parsing in Phoenix 31 | config :phoenix, :json_library, Jason 32 | 33 | # Import environment specific config. This must remain at the bottom 34 | # of this file so it overrides the configuration defined above. 35 | import_config "#{Mix.env()}.exs" 36 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # For development, we disable any cache and enable 4 | # debugging and code reloading. 5 | # 6 | # The watchers configuration can be used to run external 7 | # watchers to your application. For example, we use it 8 | # with webpack to recompile .js and .css sources. 9 | config :elixir_cluster_demo, ElixirClusterDemoWeb.Endpoint, 10 | http: [port: 4000], 11 | debug_errors: true, 12 | code_reloader: true, 13 | check_origin: false, 14 | watchers: [ 15 | # Start the esbuild watcher by calling Esbuild.install_and_run(:default, args) 16 | esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]} 17 | ] 18 | 19 | # ## SSL Support 20 | # 21 | # In order to use HTTPS in development, a self-signed 22 | # certificate can be generated by running the following 23 | # Mix task: 24 | # 25 | # mix phx.gen.cert 26 | # 27 | # Note that this task requires Erlang/OTP 20 or later. 28 | # Run `mix help phx.gen.cert` for more information. 29 | # 30 | # The `http:` config above can be replaced with: 31 | # 32 | # https: [ 33 | # port: 4001, 34 | # cipher_suite: :strong, 35 | # keyfile: "priv/cert/selfsigned_key.pem", 36 | # certfile: "priv/cert/selfsigned.pem" 37 | # ], 38 | # 39 | # If desired, both `http:` and `https:` keys can be 40 | # configured to run both http and https servers on 41 | # different ports. 42 | 43 | # Watch static and templates for browser reloading. 44 | config :elixir_cluster_demo, ElixirClusterDemoWeb.Endpoint, 45 | live_reload: [ 46 | patterns: [ 47 | ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", 48 | ~r"priv/gettext/.*(po)$", 49 | ~r"lib/elixir_cluster_demo_web/{live,views}/.*(ex)$", 50 | ~r"lib/elixir_cluster_demo_web/templates/.*(eex)$" 51 | ] 52 | ] 53 | 54 | # Do not include metadata nor timestamps in development logs 55 | config :logger, :console, format: "[$level] $message\n" 56 | 57 | # Set a higher stacktrace during development. Avoid configuring such 58 | # in production as building large stacktraces may be expensive. 59 | config :phoenix, :stacktrace_depth, 20 60 | 61 | # Initialize plugs at runtime for faster development compilation 62 | config :phoenix, :plug_init_mode, :runtime 63 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # For production, don't forget to configure the url host 4 | # to something meaningful, Phoenix uses this information 5 | # when generating URLs. 6 | # 7 | # Note we also include the path to a cache manifest 8 | # containing the digested version of static files. This 9 | # manifest is generated by the `mix phx.digest` task, 10 | # which you should run after static files are built and 11 | # before starting your production server. 12 | config :elixir_cluster_demo, ElixirClusterDemoWeb.Endpoint, 13 | url: [host: System.get_env("RENDER_EXTERNAL_HOSTNAME") || "localhost", port: 80], 14 | cache_static_manifest: "priv/static/cache_manifest.json" 15 | 16 | # Do not print debug messages in production 17 | config :logger, level: :info 18 | 19 | dns_name = System.get_env("RENDER_DISCOVERY_SERVICE") 20 | app_name = System.get_env("RENDER_SERVICE_NAME") 21 | 22 | config :libcluster, topologies: [ 23 | render: [ 24 | strategy: Cluster.Strategy.Kubernetes.DNS, 25 | config: [ 26 | service: dns_name, 27 | application_name: app_name 28 | ] 29 | ] 30 | ] 31 | # ## SSL Support 32 | # 33 | # To get SSL working, you will need to add the `https` key 34 | # to the previous section and set your `:url` port to 443: 35 | # 36 | # config :elixir_cluster_demo, ElixirClusterDemoWeb.Endpoint, 37 | # ... 38 | # url: [host: "example.com", port: 443], 39 | # https: [ 40 | # :inet6, 41 | # port: 443, 42 | # cipher_suite: :strong, 43 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 44 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH") 45 | # ] 46 | # 47 | # The `cipher_suite` is set to `:strong` to support only the 48 | # latest and more secure SSL ciphers. This means old browsers 49 | # and clients may not be supported. You can set it to 50 | # `:compatible` for wider support. 51 | # 52 | # `:keyfile` and `:certfile` expect an absolute path to the key 53 | # and cert in disk or a relative path inside priv, for example 54 | # "priv/ssl/server.key". For all supported SSL configuration 55 | # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1 56 | # 57 | # We also recommend setting `force_ssl` in your endpoint, ensuring 58 | # no data is ever sent via http, always redirecting to https: 59 | # 60 | # config :elixir_cluster_demo, ElixirClusterDemoWeb.Endpoint, 61 | # force_ssl: [hsts: true] 62 | # 63 | # Check `Plug.SSL` for all available options in `force_ssl`. 64 | -------------------------------------------------------------------------------- /config/releases.exs: -------------------------------------------------------------------------------- 1 | # In this file, we load production configuration and secrets 2 | # from environment variables. You can also hardcode secrets, 3 | # although such is generally not recommended and you have to 4 | # remember to add this file to your .gitignore. 5 | import Config 6 | 7 | secret_key_base = 8 | System.get_env("SECRET_KEY_BASE") || 9 | raise """ 10 | environment variable SECRET_KEY_BASE is missing. 11 | You can generate one by calling: mix phx.gen.secret 12 | """ 13 | 14 | config :elixir_cluster_demo, ElixirClusterDemoWeb.Endpoint, 15 | http: [:inet6, port: String.to_integer(System.get_env("PORT") || "4000")], 16 | secret_key_base: secret_key_base 17 | 18 | # ## Using releases (Elixir v1.9+) 19 | # 20 | # If you are doing OTP releases, you need to instruct Phoenix 21 | # to start each relevant endpoint: 22 | # 23 | config :elixir_cluster_demo, ElixirClusterDemoWeb.Endpoint, server: true 24 | # 25 | # Then you can assemble a release by calling `mix release`. 26 | # See `mix help release` for more information. 27 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # We don't run a server during test. If one is required, 4 | # you can enable the server option below. 5 | config :elixir_cluster_demo, ElixirClusterDemoWeb.Endpoint, 6 | http: [port: 4002], 7 | server: false 8 | 9 | # Print only warnings and errors during test 10 | config :logger, level: :warn 11 | -------------------------------------------------------------------------------- /lib/elixir_cluster_demo.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirClusterDemo do 2 | @moduledoc """ 3 | ElixirClusterDemo keeps the contexts that define your domain 4 | and business logic. 5 | 6 | Contexts are also responsible for managing your data, regardless 7 | if it comes from the database, an external API or others. 8 | """ 9 | end 10 | -------------------------------------------------------------------------------- /lib/elixir_cluster_demo/application.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirClusterDemo.Application do 2 | # See https://hexdocs.pm/elixir/Application.html 3 | # for more information on OTP Applications 4 | @moduledoc false 5 | 6 | use Application 7 | 8 | def start(_type, _args) do 9 | # List all child processes to be supervised 10 | topologies = Application.get_env(:libcluster, :topologies, []) 11 | 12 | children = [ 13 | # Start the PubSub system 14 | {Phoenix.PubSub, name: MyApp.PubSub}, 15 | # start libcluster 16 | {Cluster.Supervisor, [topologies, [name: ElixirClusterDemo.ClusterSupervisor]]}, 17 | # Start the endpoint when the application starts 18 | ElixirClusterDemoWeb.Endpoint 19 | # Starts a worker by calling: ElixirClusterDemo.Worker.start_link(arg) 20 | # {ElixirClusterDemo.Worker, arg}, 21 | ] 22 | 23 | # See https://hexdocs.pm/elixir/Supervisor.html 24 | # for other strategies and supported options 25 | opts = [strategy: :one_for_one, name: ElixirClusterDemo.Supervisor] 26 | Supervisor.start_link(children, opts) 27 | end 28 | 29 | # Tell Phoenix to update the endpoint configuration 30 | # whenever the application is updated. 31 | def config_change(changed, _new, removed) do 32 | ElixirClusterDemoWeb.Endpoint.config_change(changed, removed) 33 | :ok 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/elixir_cluster_demo_web.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirClusterDemoWeb do 2 | @moduledoc """ 3 | The entrypoint for defining your web interface, such 4 | as controllers, views, channels and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use ElixirClusterDemoWeb, :controller 9 | use ElixirClusterDemoWeb, :view 10 | 11 | The definitions below will be executed for every view, 12 | controller, etc, so keep them short and clean, focused 13 | on imports, uses and aliases. 14 | 15 | Do NOT define functions inside the quoted expressions 16 | below. Instead, define any helper function in modules 17 | and import those modules here. 18 | """ 19 | 20 | def controller do 21 | quote do 22 | use Phoenix.Controller, namespace: ElixirClusterDemoWeb 23 | 24 | import Plug.Conn 25 | import ElixirClusterDemoWeb.Gettext 26 | alias ElixirClusterDemoWeb.Router.Helpers, as: Routes 27 | end 28 | end 29 | 30 | def view do 31 | quote do 32 | use Phoenix.View, 33 | root: "lib/elixir_cluster_demo_web/templates", 34 | namespace: ElixirClusterDemoWeb 35 | 36 | # Import convenience functions from controllers 37 | import Phoenix.Controller, only: [get_flash: 1, get_flash: 2, view_module: 1] 38 | 39 | # Use all HTML functionality (forms, tags, etc) 40 | use Phoenix.HTML 41 | 42 | import ElixirClusterDemoWeb.ErrorHelpers 43 | import ElixirClusterDemoWeb.Gettext 44 | alias ElixirClusterDemoWeb.Router.Helpers, as: Routes 45 | end 46 | end 47 | 48 | def router do 49 | quote do 50 | use Phoenix.Router 51 | import Plug.Conn 52 | import Phoenix.Controller 53 | end 54 | end 55 | 56 | def channel do 57 | quote do 58 | use Phoenix.Channel 59 | import ElixirClusterDemoWeb.Gettext 60 | end 61 | end 62 | 63 | @doc """ 64 | When used, dispatch to the appropriate controller/view/etc. 65 | """ 66 | defmacro __using__(which) when is_atom(which) do 67 | apply(__MODULE__, which, []) 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/elixir_cluster_demo_web/channels/user_socket.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirClusterDemoWeb.UserSocket do 2 | use Phoenix.Socket 3 | 4 | ## Channels 5 | # channel "room:*", ElixirClusterDemoWeb.RoomChannel 6 | 7 | # Socket params are passed from the client and can 8 | # be used to verify and authenticate a user. After 9 | # verification, you can put default assigns into 10 | # the socket that will be set for all channels, ie 11 | # 12 | # {:ok, assign(socket, :user_id, verified_user_id)} 13 | # 14 | # To deny connection, return `:error`. 15 | # 16 | # See `Phoenix.Token` documentation for examples in 17 | # performing token verification on connect. 18 | def connect(_params, socket, _connect_info) do 19 | {:ok, socket} 20 | end 21 | 22 | # Socket id's are topics that allow you to identify all sockets for a given user: 23 | # 24 | # def id(socket), do: "user_socket:#{socket.assigns.user_id}" 25 | # 26 | # Would allow you to broadcast a "disconnect" event and terminate 27 | # all active sockets and channels for a given user: 28 | # 29 | # ElixirClusterDemoWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{}) 30 | # 31 | # Returning `nil` makes this socket anonymous. 32 | def id(_socket), do: nil 33 | end 34 | -------------------------------------------------------------------------------- /lib/elixir_cluster_demo_web/controllers/page_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirClusterDemoWeb.PageController do 2 | use ElixirClusterDemoWeb, :controller 3 | 4 | def index(conn, _params) do 5 | self_node = inspect(node()) 6 | nodes = inspect(Node.list()) 7 | render(conn, "index.html", %{node: self_node, nodes: nodes}) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/elixir_cluster_demo_web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirClusterDemoWeb.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :elixir_cluster_demo 3 | 4 | socket "/socket", ElixirClusterDemoWeb.UserSocket, 5 | websocket: true, 6 | longpoll: false 7 | 8 | # Serve at "/" the static files from "priv/static" directory. 9 | # 10 | # You should set gzip to true if you are running phx.digest 11 | # when deploying your static files in production. 12 | plug Plug.Static, 13 | at: "/", 14 | from: :elixir_cluster_demo, 15 | gzip: false, 16 | only: ~w(assets fonts images favicon.ico robots.txt) 17 | 18 | # Code reloading can be explicitly enabled under the 19 | # :code_reloader configuration of your endpoint. 20 | if code_reloading? do 21 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket 22 | plug Phoenix.LiveReloader 23 | plug Phoenix.CodeReloader 24 | end 25 | 26 | plug Plug.RequestId 27 | plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] 28 | 29 | plug Plug.Parsers, 30 | parsers: [:urlencoded, :multipart, :json], 31 | pass: ["*/*"], 32 | json_decoder: Phoenix.json_library() 33 | 34 | plug Plug.MethodOverride 35 | plug Plug.Head 36 | 37 | # The session will be stored in the cookie and signed, 38 | # this means its contents can be read but not tampered with. 39 | # Set :encryption_salt if you would also like to encrypt it. 40 | plug Plug.Session, 41 | store: :cookie, 42 | key: "_elixir_cluster_demo_key", 43 | signing_salt: "9gg8+xg7" 44 | 45 | plug ElixirClusterDemoWeb.Router 46 | end 47 | -------------------------------------------------------------------------------- /lib/elixir_cluster_demo_web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirClusterDemoWeb.Gettext do 2 | @moduledoc """ 3 | A module providing Internationalization with a gettext-based API. 4 | 5 | By using [Gettext](https://hexdocs.pm/gettext), 6 | your module gains a set of macros for translations, for example: 7 | 8 | import ElixirClusterDemoWeb.Gettext 9 | 10 | # Simple translation 11 | gettext("Here is the string to translate") 12 | 13 | # Plural translation 14 | ngettext("Here is the string to translate", 15 | "Here are the strings to translate", 16 | 3) 17 | 18 | # Domain-based translation 19 | dgettext("errors", "Here is the error message to translate") 20 | 21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. 22 | """ 23 | use Gettext, otp_app: :elixir_cluster_demo 24 | end 25 | -------------------------------------------------------------------------------- /lib/elixir_cluster_demo_web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirClusterDemoWeb.Router do 2 | use ElixirClusterDemoWeb, :router 3 | 4 | pipeline :browser do 5 | plug :accepts, ["html"] 6 | plug :fetch_session 7 | plug :fetch_flash 8 | plug :protect_from_forgery 9 | plug :put_secure_browser_headers 10 | end 11 | 12 | pipeline :api do 13 | plug :accepts, ["json"] 14 | end 15 | 16 | scope "/", ElixirClusterDemoWeb do 17 | pipe_through :browser 18 | 19 | get "/", PageController, :index 20 | end 21 | 22 | # Other scopes may use custom stacks. 23 | # scope "/api", ElixirClusterDemoWeb do 24 | # pipe_through :api 25 | # end 26 | end 27 | -------------------------------------------------------------------------------- /lib/elixir_cluster_demo_web/templates/layout/app.html.heex: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 |<%= Phoenix.Flash.get(@flash, :info) %>
21 |<%= Phoenix.Flash.get(@flash, :error) %>
22 | <%= @inner_content %> 23 |Learn more at https://render.com/docs/deploy-elixir-cluster.
6 |Current Node | 14 |<%= @node %> | 15 |
---|---|
Other Nodes | 18 |<%= @nodes %> | 19 |