<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Miguel Cobá]]></title><description><![CDATA[I write about Elixir, Phoenix, Software Development and eBook writing]]></description><link>https://blog.miguelcoba.com</link><generator>RSS for Node</generator><lastBuildDate>Sat, 11 Apr 2026 18:35:49 GMT</lastBuildDate><atom:link href="https://blog.miguelcoba.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Editing forms with live_file_input]]></title><description><![CDATA[Imagine you are writing an app to handle products for a marketplace. Each Product has up to 3 images associated with it. When editing a Product, you should be able to remove any of the already associated images and add new images to the Product, alwa...]]></description><link>https://blog.miguelcoba.com/editing-forms-with-live-file-input</link><guid isPermaLink="true">https://blog.miguelcoba.com/editing-forms-with-live-file-input</guid><category><![CDATA[Elixir]]></category><category><![CDATA[liveview]]></category><category><![CDATA[phoenix liveview]]></category><category><![CDATA[Phoenix framework]]></category><category><![CDATA[File Upload]]></category><dc:creator><![CDATA[Miguel Cobá]]></dc:creator><pubDate>Tue, 23 May 2023 14:55:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/ldDmTgf89gU/upload/a049344733a732496590abcfa6165019.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Imagine you are writing an app to handle products for a marketplace. Each Product has up to 3 images associated with it. When editing a Product, you should be able to remove any of the already associated images and add new images to the Product, always respecting the limit of 3 images per product.</p>
<p>Quite common requirements, right?</p>
<p>And given that Phoenix has pretty good code generators and LiveView now has an amazing <code>live_file_input</code> component that automates uploading files in an interactive way, it should be pretty easy to implement.</p>
<p>Well...</p>
<p>Keep reading to discover what I learned in the last couple of days trying to code this simple task.</p>
<h1 id="heading-the-initial-project-and-schema">The initial project and schema</h1>
<p>Let's start by creating a new project with the latest Phoenix version. I use <code>asdf</code> for this:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Add plugins</span>
asdf plugin-add erlang
asdf plugin-add elixir
<span class="hljs-comment"># Install them</span>
asdf install erlang 25.3
asdf global erlang 25.3
asdf install elixir 1.14.4-otp-25
asdf global elixir 1.14.4-otp-25
<span class="hljs-comment"># Install Phoenix 1.7</span>
mix local.rebar --force
mix local.hex --force
mix archive.install hex phx_new 1.7.2 --force
</code></pre>
<p>We can now generate a project. I'll name mine <code>mercury</code>:</p>
<pre><code class="lang-bash">mix phx.new mercury --install
</code></pre>
<p>We'll use a simple migration and schema for a Product with two attributes: <code>name</code> and a list of <code>images</code>, stored as an array of strings.</p>
<h1 id="heading-phoenix-code-generators">Phoenix code generators</h1>
<p>Our first attempt is to use the Phoenix code generators:</p>
<pre><code class="lang-bash">mix phx.gen.live Products Product products name:string images:array:string
</code></pre>
<p>We'll see that the generated code for the <code>images</code> attribute is an input field with type of <code>select</code> (<em>lib/mercury_web/live/product_live/form_component.ex</em>):</p>
<pre><code class="lang-elixir">&lt;.input
  field={<span class="hljs-variable">@form</span>[<span class="hljs-symbol">:images</span>]}
  type=<span class="hljs-string">"select"</span>
  multiple
  label=<span class="hljs-string">"Images"</span>
  options={[{<span class="hljs-string">"Option 1"</span>, <span class="hljs-string">"option1"</span>}, {<span class="hljs-string">"Option 2"</span>, <span class="hljs-string">"option2"</span>}]}
/&gt;
</code></pre>
<p>That's reasonable because the code generators have no way to know that we intended the array of strings to be image URLs and that the input should have a type of <code>file</code> and be supported by the <code>live_file_input</code> component.</p>
<p>As powerful as the generators are for CRUD pages, in this case, we need to do it by ourselves.</p>
<p>(Find the code for this part on the <code>phoenix-code-generator</code> branch of the git repo)</p>
<h1 id="heading-official-livefileinput-docs">Official live_file_input docs</h1>
<p>Let's write the code for file uploads manually following the <a target="_blank" href="https://hexdocs.pm/phoenix_live_view/uploads.html">official docs</a> for <code>live_file_input</code>. We'll start from the generated code and we'll adapt it by adding the code from the official docs.</p>
<p>Add the following routes to <code>router.ex</code>:</p>
<pre><code class="lang-elixir">  scope <span class="hljs-string">"/"</span>, MercuryWeb <span class="hljs-keyword">do</span>
    pipe_through <span class="hljs-symbol">:browser</span>

    get <span class="hljs-string">"/"</span>, PageController, <span class="hljs-symbol">:home</span>

    live <span class="hljs-string">"/products"</span>, ProductLive.Index
    live <span class="hljs-string">"/products/new"</span>, ProductLive.New, <span class="hljs-symbol">:new</span>
    live <span class="hljs-string">"/products/:id"</span>, ProductLive.Show
    live <span class="hljs-string">"/products/:id/edit"</span>, ProductLive.Edit, <span class="hljs-symbol">:edit</span>
  <span class="hljs-keyword">end</span>
</code></pre>
<p>Replace the contents or create the following files.</p>
<p><em>lib/mercury_web/live/product_live/index.ex</em>:</p>
<pre><code class="lang-elixir"><span class="hljs-class"><span class="hljs-keyword">defmodule</span> <span class="hljs-title">MercuryWeb.ProductLive.Index</span></span> <span class="hljs-keyword">do</span>
  <span class="hljs-keyword">use</span> MercuryWeb, <span class="hljs-symbol">:live_view</span>

  <span class="hljs-keyword">alias</span> Mercury.Products

  <span class="hljs-variable">@impl</span> <span class="hljs-keyword">true</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">mount</span></span>(_params, _session, socket) <span class="hljs-keyword">do</span>
    socket =
      socket
      |&gt; assign(<span class="hljs-symbol">:page_title</span>, <span class="hljs-string">"Listing Products"</span>)
      |&gt; stream(<span class="hljs-symbol">:products</span>, Products.list_products())

    {<span class="hljs-symbol">:ok</span>, socket}
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p><em>lib/mercury_web/live/product_live/index.html.heex</em>:</p>
<pre><code class="lang-elixir">&lt;.header&gt;
  Listing Products
  &lt;<span class="hljs-symbol">:actions&gt;</span>
    &lt;.link navigate={<span class="hljs-string">~p"/products/new"</span>}&gt;
      &lt;.button&gt;New Product&lt;<span class="hljs-regexp">/.button&gt;
    &lt;/</span>.link&gt;
  &lt;<span class="hljs-regexp">/:actions&gt;
&lt;/</span>.header&gt;

&lt;.table
  id=<span class="hljs-string">"products"</span>
  rows={<span class="hljs-variable">@streams</span>.products}
  row_click={<span class="hljs-keyword">fn</span> {_id, product} -&gt; JS.navigate(<span class="hljs-string">~p"/products/<span class="hljs-subst">#{product}</span>"</span>) <span class="hljs-keyword">end</span>}
&gt;
  &lt;<span class="hljs-symbol">:col</span> <span class="hljs-symbol">:let=</span>{{_id, product}} label=<span class="hljs-string">"Name"</span>&gt;&lt;%= product.name %&gt;&lt;<span class="hljs-regexp">/:col&gt;
  &lt;:action :let={{_id, product}}&gt;
    &lt;div class="sr-only"&gt;
      &lt;.link navigate={~p"/products</span><span class="hljs-regexp">/<span class="hljs-subst">#{product}</span>"}&gt;Show&lt;/</span>.link&gt;
    &lt;<span class="hljs-regexp">/div&gt;
    &lt;.link navigate={~p"/products</span><span class="hljs-regexp">/<span class="hljs-subst">#{product}</span>/edit</span><span class="hljs-string">"}&gt;Edit&lt;/.link&gt;
  &lt;/:action&gt;
&lt;/.table&gt;</span>
</code></pre>
<p><em>lib/mixquic_web/live/artisan_courses_live/new.ex</em>:</p>
<pre><code class="lang-elixir"><span class="hljs-class"><span class="hljs-keyword">defmodule</span> <span class="hljs-title">MercuryWeb.ProductLive.New</span></span> <span class="hljs-keyword">do</span>
  <span class="hljs-keyword">use</span> MercuryWeb, <span class="hljs-symbol">:live_view</span>

  <span class="hljs-keyword">alias</span> Mercury.Products.Product

  <span class="hljs-variable">@impl</span> <span class="hljs-keyword">true</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">mount</span></span>(_params, _session, socket) <span class="hljs-keyword">do</span>
    socket =
      socket
      |&gt; assign(<span class="hljs-symbol">:page_title</span>, <span class="hljs-string">"New Product"</span>)
      |&gt; assign(<span class="hljs-symbol">:product</span>, %Product{})

    {<span class="hljs-symbol">:ok</span>, socket}
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p><em>lib/mercury_web/live/product_live/new.html.heex</em>:</p>
<pre><code class="lang-elixir">&lt;.live_component
  <span class="hljs-keyword">module</span>={MercuryWeb.ProductLive.FormComponent}
  id={<span class="hljs-symbol">:new</span>}
  title={<span class="hljs-variable">@page_title</span>}
  action={<span class="hljs-variable">@live_action</span>}
  product={<span class="hljs-variable">@product</span>}
  navigate={<span class="hljs-string">~p"/products"</span>}
/&gt;
</code></pre>
<p><em>lib/mercury_web/live/product_live/edit.ex</em> with this:</p>
<pre><code class="lang-elixir"><span class="hljs-class"><span class="hljs-keyword">defmodule</span> <span class="hljs-title">MercuryWeb.ProductLive.Edit</span></span> <span class="hljs-keyword">do</span>
  <span class="hljs-keyword">use</span> MercuryWeb, <span class="hljs-symbol">:live_view</span>

  <span class="hljs-keyword">alias</span> Mercury.Products

  <span class="hljs-variable">@impl</span> <span class="hljs-keyword">true</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">mount</span></span>(_params, _session, socket) <span class="hljs-keyword">do</span>
    {<span class="hljs-symbol">:ok</span>, socket}
  <span class="hljs-keyword">end</span>

  <span class="hljs-variable">@impl</span> <span class="hljs-keyword">true</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">handle_params</span></span>(%{<span class="hljs-string">"id"</span> =&gt; id}, _url, socket) <span class="hljs-keyword">do</span>
    {<span class="hljs-symbol">:noreply</span>,
     socket
     |&gt; assign(<span class="hljs-symbol">:page_title</span>, <span class="hljs-string">"Edit Course"</span>)
     |&gt; assign(<span class="hljs-symbol">:product</span>, Products.get_product!(id))}
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p><em>lib/mixquic_web/live/artisan_courses_live/edit.html.heex</em> with:</p>
<pre><code class="lang-elixir">&lt;.live_component
  <span class="hljs-keyword">module</span>={MercuryWeb.ProductLive.FormComponent}
  id={<span class="hljs-variable">@product</span>.id}
  title={<span class="hljs-variable">@page_title</span>}
  action={<span class="hljs-variable">@live_action</span>}
  product={<span class="hljs-variable">@product</span>}
  navigate={<span class="hljs-string">~p"/products"</span>}
/&gt;
</code></pre>
<p><em>lib/mercury_web/live/product_live/show.ex</em>:</p>
<pre><code class="lang-elixir"><span class="hljs-class"><span class="hljs-keyword">defmodule</span> <span class="hljs-title">MercuryWeb.ProductLive.Show</span></span> <span class="hljs-keyword">do</span>
  <span class="hljs-keyword">use</span> MercuryWeb, <span class="hljs-symbol">:live_view</span>

  <span class="hljs-keyword">alias</span> Mercury.Products

  <span class="hljs-variable">@impl</span> <span class="hljs-keyword">true</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">mount</span></span>(_params, _session, socket) <span class="hljs-keyword">do</span>
    {<span class="hljs-symbol">:ok</span>, socket}
  <span class="hljs-keyword">end</span>

  <span class="hljs-variable">@impl</span> <span class="hljs-keyword">true</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">handle_params</span></span>(%{<span class="hljs-string">"id"</span> =&gt; id}, _, socket) <span class="hljs-keyword">do</span>
    {<span class="hljs-symbol">:noreply</span>,
     socket
     |&gt; assign(<span class="hljs-symbol">:page_title</span>, <span class="hljs-string">"Show course"</span>)
     |&gt; assign(<span class="hljs-symbol">:product</span>, Products.get_product!(id))}
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p><em>lib/mixquic_web/live/artisan_courses_live/show.html.heex</em>:</p>
<pre><code class="lang-elixir">&lt;.header&gt;
  Product &lt;%= <span class="hljs-variable">@product</span>.id %&gt;
  &lt;<span class="hljs-symbol">:actions&gt;</span>
    &lt;.link navigate={<span class="hljs-string">~p"/products/<span class="hljs-subst">#{<span class="hljs-variable">@product</span>}</span>/edit"</span>} phx-click={JS.push_focus()}&gt;
      &lt;.button&gt;Edit product&lt;<span class="hljs-regexp">/.button&gt;
    &lt;/</span>.link&gt;
  &lt;<span class="hljs-regexp">/:actions&gt;
&lt;/</span>.header&gt;

&lt;.list&gt;
  &lt;<span class="hljs-symbol">:item</span> title=<span class="hljs-string">"Name"</span>&gt;&lt;%= <span class="hljs-variable">@product</span>.name %&gt;&lt;<span class="hljs-regexp">/:item&gt;
&lt;/</span>.list&gt;
&lt;div class=<span class="hljs-string">"mt-4"</span>&gt;
  &lt;.label&gt;Images&lt;<span class="hljs-regexp">/.label&gt;
  &lt;div class="grid justify-center md:grid-cols-2 lg:grid-cols-3 gap-5 lg:gap-7 my-10"&gt;
    &lt;figure
      :for={image &lt;- @product.images}
      class="rounded-lg border shadow-md max-w-xs md:max-w-none"
    &gt;
      &lt;img src={image} /</span>&gt;
    &lt;<span class="hljs-regexp">/figure&gt;
  &lt;/div</span>&gt;
&lt;<span class="hljs-regexp">/div&gt;
&lt;.back navigate={~p"/products</span><span class="hljs-string">"}&gt;Back to products&lt;/.back&gt;</span>
</code></pre>
<p><em>lib/mercury_web/live/product_live/form_component.ex</em>:</p>
<pre><code class="lang-elixir"><span class="hljs-class"><span class="hljs-keyword">defmodule</span> <span class="hljs-title">MercuryWeb.ProductLive.FormComponent</span></span> <span class="hljs-keyword">do</span>
  <span class="hljs-keyword">use</span> MercuryWeb, <span class="hljs-symbol">:live_component</span>

  <span class="hljs-keyword">alias</span> Mercury.Products

  <span class="hljs-variable">@impl</span> <span class="hljs-keyword">true</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">render</span></span>(assigns) <span class="hljs-keyword">do</span>
    <span class="hljs-string">~H"""
    &lt;div&gt;
      &lt;.header&gt;
        &lt;%= @title %&gt;
        &lt;:subtitle&gt;Use this form to manage product records in your database.&lt;/:subtitle&gt;
      &lt;/.header&gt;

      &lt;.simple_form
        for={@form}
        id="</span>product-form<span class="hljs-string">"
        phx-target={@myself}
        phx-change="</span>validate<span class="hljs-string">"
        phx-submit="</span>save<span class="hljs-string">"
      &gt;
        &lt;.input field={@form[:name]} type="</span>text<span class="hljs-string">" label="</span>Name<span class="hljs-string">" /&gt;
        &lt;.label for="</span><span class="hljs-comment">#images"&gt;Images&lt;/.label&gt;</span>
        &lt;div id=<span class="hljs-string">"images"</span>&gt;
          &lt;div
            class=<span class="hljs-string">"p-4 border-2 border-dashed border-slate-300 rounded-md text-center text-slate-600"</span>
            phx-drop-target={<span class="hljs-variable">@uploads</span>.images.ref}
          &gt;
            &lt;div class=<span class="hljs-string">"flex flex-row items-center justify-center"</span>&gt;
              &lt;.live_file_input upload={<span class="hljs-variable">@uploads</span>.images} /&gt;
              &lt;span class=<span class="hljs-string">"font-semibold text-slate-500"</span>&gt;<span class="hljs-keyword">or</span> drag <span class="hljs-keyword">and</span> drop here&lt;<span class="hljs-regexp">/span&gt;
            &lt;/div</span>&gt;

            &lt;div class=<span class="hljs-string">"mt-4"</span>&gt;
              &lt;.error <span class="hljs-symbol">:for=</span>{err &lt;- upload_errors(<span class="hljs-variable">@uploads</span>.images)}&gt;
                &lt;%= Phoenix.Naming.humanize(err) %&gt;
              &lt;<span class="hljs-regexp">/.error&gt;
            &lt;/div</span>&gt;

            &lt;div class=<span class="hljs-string">"mt-4 flex flex-row flex-wrap justify-start content-start items-start gap-2"</span>&gt;
              &lt;div
                <span class="hljs-symbol">:for=</span>{entry &lt;- <span class="hljs-variable">@uploads</span>.images.entries}
                class=<span class="hljs-string">"flex flex-col items-center justify-start space-y-1"</span>
              &gt;
                &lt;div class=<span class="hljs-string">"w-32 h-32 overflow-clip"</span>&gt;
                  &lt;.live_img_preview entry={entry} /&gt;
                &lt;<span class="hljs-regexp">/div&gt;

                &lt;div class="w-full"&gt;
                  &lt;div class="mb-2 text-xs font-semibold inline-block text-slate-600"&gt;
                    &lt;%= entry.progress %&gt;%
                  &lt;/div</span>&gt;
                  &lt;div class=<span class="hljs-string">"flex h-2 overflow-hidden text-base bg-slate-200 rounded-lg mb-2"</span>&gt;
                    &lt;span style={<span class="hljs-string">"width: <span class="hljs-subst">#{entry.progress}</span>%"</span>} class=<span class="hljs-string">"shadow-md bg-slate-500"</span>&gt;&lt;<span class="hljs-regexp">/span&gt;
                  &lt;/div</span>&gt;
                  &lt;.error <span class="hljs-symbol">:for=</span>{err &lt;- upload_errors(<span class="hljs-variable">@uploads</span>.images, entry)}&gt;
                    &lt;%= Phoenix.Naming.humanize(err) %&gt;
                  &lt;<span class="hljs-regexp">/.error&gt;
                &lt;/div</span>&gt;
                &lt;a phx-click=<span class="hljs-string">"cancel"</span> phx-target={<span class="hljs-variable">@myself</span>} phx-value-ref={entry.ref}&gt;
                  &lt;.icon name=<span class="hljs-string">"hero-trash"</span> /&gt;
                &lt;<span class="hljs-regexp">/a&gt;
              &lt;/div</span>&gt;
            &lt;<span class="hljs-regexp">/div&gt;
          &lt;/div</span>&gt;
        &lt;<span class="hljs-regexp">/div&gt;
        &lt;:actions&gt;
          &lt;.button phx-disable-with="Saving..."&gt;Save Product&lt;/</span>.button&gt;
        &lt;<span class="hljs-regexp">/:actions&gt;
      &lt;/</span>.simple_form&gt;
    &lt;<span class="hljs-regexp">/div&gt;
    """
  end

  @max_entries 3
  @max_file_size 5_000_000

  @impl true
  def update(%{product: product} = assigns, socket) do
    changeset = Products.change_product(product)

    {:ok,
     socket
     |&gt; assign(assigns)
     |&gt; assign_form(changeset)
     |&gt; allow_upload(:images,
       accept: ~w(.png .jpg .jpeg),
       max_entries: @max_entries,
       max_file_size: @max_file_size
     )}
  end

  @impl true
  def handle_event("validate", %{"product" =&gt; product_params}, socket) do
    changeset =
      socket.assigns.product
      |&gt; Products.change_product(product_params)
      |&gt; Map.put(:action, :validate)

    {:noreply, assign_form(socket, changeset)}
  end

  def handle_event("save", %{"product" =&gt; product_params}, socket) do
    images =
      consume_uploaded_entries(socket, :images, fn meta, entry -&gt;
        filename = "<span class="hljs-subst">#{entry.uuid}</span><span class="hljs-subst">#{Path.extname(entry.client_name)}</span>"
        dest = Path.join(MercuryWeb.uploads_dir(), filename)

        File.cp!(meta.path, dest)

        {:ok, ~p"/uploads</span><span class="hljs-regexp">/<span class="hljs-subst">#{filename}</span>"}
      end)

    product_params = Map.put(product_params, "images", images)

    save_product(socket, socket.assigns.action, product_params)
  end

  def handle_event("cancel", %{"ref" =&gt; ref}, socket) do
    {:noreply, cancel_upload(socket, :images, ref)}
  end

  defp save_product(socket, :edit, product_params) do
    case Products.update_product(socket.assigns.product, product_params) do
      {:ok, _product} -&gt;
        {:noreply,
         socket
         |&gt; put_flash(:info, "Product updated successfully")
         |&gt; push_navigate(to: socket.assigns.navigate)}

      {:error, %Ecto.Changeset{} = changeset} -&gt;
        {:noreply, assign_form(socket, changeset)}
    end
  end

  defp save_product(socket, :new, product_params) do
    case Products.create_product(product_params) do
      {:ok, _product} -&gt;
        {:noreply,
         socket
         |&gt; put_flash(:info, "Product created successfully")
         |&gt; push_navigate(to: socket.assigns.navigate)}

      {:error, %Ecto.Changeset{} = changeset} -&gt;
        {:noreply, assign_form(socket, changeset)}
    end
  end

  defp assign_form(socket, %Ecto.Changeset{} = changeset) do
    assign(socket, :form, to_form(changeset))
  end
end</span>
</code></pre>
<p><em>lib/mercury_web.ex</em>:</p>
<pre><code class="lang-elixir">
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">static_paths</span></span>, <span class="hljs-symbol">do:</span> <span class="hljs-string">~w(assets fonts images uploads favicon.ico robots.txt)</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">uploads_dir</span></span>, <span class="hljs-symbol">do:</span> Application.app_dir(<span class="hljs-symbol">:mercury</span>, [<span class="hljs-string">"priv"</span>, <span class="hljs-string">"static"</span>, <span class="hljs-string">"uploads"</span>])
</code></pre>
<p><em>lib/mercury/application.ex</em>:</p>
<pre><code class="lang-elixir">  <span class="hljs-variable">@impl</span> <span class="hljs-keyword">true</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">start</span></span>(_type, _args) <span class="hljs-keyword">do</span>
    MercuryWeb.uploads_dir() |&gt; File.mkdir_p!()
    <span class="hljs-comment"># ...</span>
  <span class="hljs-keyword">end</span>
</code></pre>
<p>Run migrations and start the server to try it:</p>
<pre><code class="lang-elixir">mix ecto.migrate
mix phx.server
</code></pre>
<p>If you go now to <code>http://localhost:4000/products</code></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684582749811/69cbb826-dfaa-4b3f-aa01-13f0a3b54f8b.png" alt class="image--center mx-auto" /></p>
<p>Create a new product:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684829871744/bb4dca4b-533f-4b23-8078-e84968a56697.png" alt class="image--center mx-auto" /></p>
<p>Save it</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684582901607/aeec72e5-9930-42be-bdb4-892c3b218c5c.png" alt class="image--center mx-auto" /></p>
<p>and go to the <code>show.html.heex</code>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684582919552/35ea50ed-ee4d-48e7-9548-b49d37fef705.png" alt class="image--center mx-auto" /></p>
<p>We are now able to create a product, add images to it, and display them. So far so good.</p>
<p>(Find the code for this part on the <code>official-live-file-input-docs</code> branch of the git repo)</p>
<h1 id="heading-editing-the-images-of-the-product">Editing the images of the product</h1>
<p>Let's try to edit the product and change the images:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684830350586/b075a8c9-62f8-4088-9a2a-158f61cc2947.png" alt class="image--center mx-auto" /></p>
<p>Now we see the issue. The existing images are not being shown the same way the product <code>name</code> is. If you select one image and save it, this will replace the three ones currently associated with the product. If you don't have the original images available to upload them again when editing the product, they will be replaced and lost.</p>
<p>That's not good. We need to:</p>
<ol>
<li><p>show the existing images if they exist</p>
</li>
<li><p>be able to remove some of the existing images if we want to</p>
</li>
<li><p>add new images to the product and save it</p>
</li>
<li><p>ensure that we don't exceed the max number of images allowed.</p>
</li>
</ol>
<p>Let's implement each of those points</p>
<h2 id="heading-showing-existing-images">Showing existing images</h2>
<p>Let's show the images of the product. The images to show are the ones currently associated to the product, not the entries in the <code>:images</code> uploads map.</p>
<pre><code class="lang-elixir">&lt;.label <span class="hljs-keyword">for</span>=<span class="hljs-string">"#images"</span>&gt;Images&lt;<span class="hljs-regexp">/.label&gt;
&lt;div id="images"&gt;
  &lt;div class="flex flex-row flex-wrap justify-start items-start text-center text-slate-500 gap-2 mb-8"&gt;
    &lt;div :for={image &lt;- @product.images} class="border shadow-md pb-1"&gt;
      &lt;figure class="w-32 h-32 overflow-clip"&gt;
        &lt;img src={image} /</span>&gt;
      &lt;<span class="hljs-regexp">/figure&gt;
    &lt;/div</span>&gt;
  &lt;<span class="hljs-regexp">/div&gt;</span>
</code></pre>
<p>Now we can see the images in the edit form:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684665960140/93782f5b-c77d-4d03-a718-41aab17d978d.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-removing-existing-images-and-adding-new-images">Removing existing images and adding new images</h2>
<p>To remove the existing images from the product we need to take into consideration a couple of things. First, when we remove them, we won't show them on the edit form anymore, but we don't really apply the changes to the product until we click the <code>Save Product</code> button. We'll need to keep this in memory but not persist it until told so by the user.</p>
<p>We can use an <code>removed_images</code> assign to hold the images the user has removed so far. It will start as an empty list and, because it is the initial state, we'll assign it on the <code>mount</code> callback instead of the <code>update</code> callback. Let's add it:</p>
<pre><code class="lang-elixir">  <span class="hljs-variable">@impl</span> <span class="hljs-keyword">true</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">mount</span></span>(socket) <span class="hljs-keyword">do</span>
    socket =
      socket
      |&gt; assign(<span class="hljs-symbol">:removed_images</span>, [])

    {<span class="hljs-symbol">:ok</span>, socket}
  <span class="hljs-keyword">end</span>
</code></pre>
<p>Now in the <code>update</code> handler we can calculate which images to show:</p>
<pre><code class="lang-elixir">  <span class="hljs-variable">@impl</span> <span class="hljs-keyword">true</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">update</span></span>(%{<span class="hljs-symbol">product:</span> product} = assigns, socket) <span class="hljs-keyword">do</span>
    changeset = Products.change_product(product)
    images_to_show = product.images -- socket.assigns.removed_images

    {<span class="hljs-symbol">:ok</span>,
     socket
     |&gt; assign(assigns)
     |&gt; assign_form(changeset)
     |&gt; assign(<span class="hljs-symbol">:images_to_show</span>, images_to_show)
     |&gt; allow_upload(<span class="hljs-symbol">:images</span>,
       <span class="hljs-symbol">accept:</span> <span class="hljs-string">~w(.png .jpg .jpeg)</span>,
       <span class="hljs-symbol">max_entries:</span> <span class="hljs-variable">@max_entries</span>,
       <span class="hljs-symbol">max_file_size:</span> <span class="hljs-variable">@max_file_size</span>
     )}
  <span class="hljs-keyword">end</span>
</code></pre>
<p>We add a new handler for the <code>remove</code> event, that will be sent each time we remove an existing image from the Product:</p>
<pre><code class="lang-elixir">  <span class="hljs-variable">@impl</span> <span class="hljs-keyword">true</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">handle_event</span></span>(<span class="hljs-string">"remove"</span>, %{<span class="hljs-string">"image"</span> =&gt; image}, socket) <span class="hljs-keyword">do</span>
    product = socket.assigns.product
    removed_images = [image | socket.assigns.removed_images]
    images_to_show = product.images -- removed_images

    socket =
      socket
      |&gt; assign(<span class="hljs-symbol">:removed_images</span>, removed_images)
      |&gt; assign(<span class="hljs-symbol">:images_to_show</span>, images_to_show)

    {<span class="hljs-symbol">:noreply</span>, socket}
  <span class="hljs-keyword">end</span>
</code></pre>
<p>Let's tweak the template to correctly show the images and allow removing them:</p>
<pre><code class="lang-elixir">&lt;div <span class="hljs-symbol">:for=</span>{image &lt;- <span class="hljs-variable">@images_to_show</span>} class=<span class="hljs-string">"border shadow-md pb-1"</span>&gt;
  &lt;figure class=<span class="hljs-string">"w-32 h-32 overflow-clip"</span>&gt;
    &lt;img src={image} /&gt;
  &lt;<span class="hljs-regexp">/figure&gt;
  &lt;a
    phx-click="remove"
    phx-target={@myself}
    phx-value-image={image}
    class="hover:text-slate-800"
  &gt;
    &lt;.icon name="hero-trash" /</span>&gt;
  &lt;<span class="hljs-regexp">/a&gt;
&lt;/div</span>&gt;
</code></pre>
<p>We can now remove images and if we reload without saving the changes, the original images are shown.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684667397032/326b2f6d-4149-4cc2-a835-1fb1cb05f900.png" alt class="image--center mx-auto" /></p>
<p>Let's modify the <code>save</code> event to correctly save the new set of images:</p>
<pre><code class="lang-elixir">  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">handle_event</span></span>(<span class="hljs-string">"save"</span>, %{<span class="hljs-string">"product"</span> =&gt; product_params}, socket) <span class="hljs-keyword">do</span>
    images_to_show = socket.assigns.images_to_show

    images =
      consume_uploaded_entries(socket, <span class="hljs-symbol">:images</span>, <span class="hljs-keyword">fn</span> meta, entry -&gt;
        filename = <span class="hljs-string">"<span class="hljs-subst">#{entry.uuid}</span><span class="hljs-subst">#{Path.extname(entry.client_name)}</span>"</span>
        dest = Path.join(MercuryWeb.uploads_dir(), filename)

        File.cp!(meta.path, dest)

        {<span class="hljs-symbol">:ok</span>, <span class="hljs-string">~p"/uploads/<span class="hljs-subst">#{filename}</span>"</span>}
      <span class="hljs-keyword">end</span>)

    product_params = Map.put(product_params, <span class="hljs-string">"images"</span>, images_to_show ++ images)

    save_product(socket, socket.assigns.action, product_params)
  <span class="hljs-keyword">end</span>
</code></pre>
<p>If we now remove an image and add a new one, the Product will correctly be saved</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684668349680/b2e0a42b-3cbd-48d0-b677-1d9958cadb64.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684668368119/1b6e4d15-aac5-4f66-848d-ac4d3d45623d.png" alt class="image--center mx-auto" /></p>
<p>Nice, right?</p>
<p>Well, not completely. If you play around with the current code, you'll see that you can add more images than the maximum allowed by our requirements. In fact, you can go to the edit page and add 3 more images every time you save the form.</p>
<p>(Find the code for this part on the <code>show-remove-add-images</code> branch of the git repo)</p>
<h2 id="heading-ensure-we-dont-exceed-the-allowed-number-of-images">Ensure we don't exceed the allowed number of images</h2>
<p>Now we have reached the part where the problem is. As it currently is, the <code>live_file_input</code> has no way of knowing additional information other than the one you provide when you call the <code>allow_update</code> function. It is also not possible to modify the configuration after initialization unless you destroy it and start over. <code>live_file_input</code> doesn't know how many files to allow at any given moment because that depends on external information not available to it.</p>
<p>We can try some ways to address this issue.</p>
<h3 id="heading-replacing-the-livefileinput-configuration-dynamically">Replacing the live_file_input configuration dynamically</h3>
<p>We want the <code>live_file_input</code> component to allow only enough files so that they, when added to the images that remain in the product after any removal by the user, don't exceed the maximum number of allowed files. For example, if we have a restriction of 3 files maximum, the product has already 2 images associated, and the user removes 1 in the edit page, then the <code>live_file_input</code> component should allow 2 additional files to be uploaded.</p>
<p>This suggests we should be able to reconfigure the <code>live_file_input</code> with the correct value for <code>:max_entries</code>.</p>
<p>Something like this:</p>
<pre><code class="lang-elixir">  <span class="hljs-variable">@impl</span> <span class="hljs-keyword">true</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">handle_event</span></span>(<span class="hljs-string">"remove"</span>, %{<span class="hljs-string">"image"</span> =&gt; image}, socket) <span class="hljs-keyword">do</span>
    product = socket.assigns.product
    removed_images = [image | socket.assigns.removed_images]
    images_to_show = product.images -- removed_images
    max_entries = <span class="hljs-variable">@max_entries</span> - length(images_to_show)

    socket =
      socket
      |&gt; assign(<span class="hljs-symbol">:removed_images</span>, removed_images)
      |&gt; assign(<span class="hljs-symbol">:images_to_show</span>, images_to_show)
      |&gt; allow_upload(<span class="hljs-symbol">:images</span>,
        <span class="hljs-symbol">accept:</span> <span class="hljs-string">~w(.png .jpg .jpeg)</span>,
        <span class="hljs-symbol">max_entries:</span> max_entries,
        <span class="hljs-symbol">max_file_size:</span> <span class="hljs-variable">@max_file_size</span>
      )

    {<span class="hljs-symbol">:noreply</span>, socket}
  <span class="hljs-keyword">end</span>
</code></pre>
<p>If we now go to the edit form and remove an image we see that, if we start with 3 images associated to the product and we remove 1, the <code>live_file_input</code> component allows us to upload only 1 file. If we remove 2, it will allow us to add 2 new files.</p>
<p>Trying to add 3 files, it shows the error message:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684670531387/6ef08999-bf2c-4b98-b477-545abb11b6f5.png" alt class="image--center mx-auto" /></p>
<p>Removing one upload and trying to upload only two images, the error disappears.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684670537595/42589e3a-bf87-4424-aea8-a4f03cf8d986.png" alt class="image--center mx-auto" /></p>
<p>It looks good!</p>
<p>Let's remove the last associated image and try to upload 3 completely new images:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684670958320/96c7429c-6de6-49d7-9748-dfd054284032.png" alt class="image--center mx-auto" /></p>
<p>Something went wrong.</p>
<pre><code class="lang-bash">  Parameters: %{<span class="hljs-string">"image"</span> =&gt; <span class="hljs-string">"/uploads/4c8cb89e-a9ef-4bb6-8672-960570e79fce.jpg"</span>}
[error] GenServer <span class="hljs-comment">#PID&lt;0.2173.0&gt; terminating</span>
** (ArgumentError) cannot allow_upload on an existing upload with active entries.

Use cancel_upload and/or consume_upload to handle the active entries before allowing a new upload.

    (phoenix_live_view 0.18.18) lib/phoenix_live_view/upload.ex:19: Phoenix.LiveView.Upload.allow_upload/3
</code></pre>
<p>There is a problem when we try to reconfigure the <code>live_file_input</code> if there are active entries (those are the files ready to be uploaded and that we are seeing the preview on the screen).</p>
<p>As the message states, one way to avoid this error is to not have active entries when removing images. We can check for active entries and cancel them before trying to reconfigure the <code>live_file_input</code>.</p>
<p>Phoenix has a method to do that. Let's use it:</p>
<pre><code class="lang-elixir">  <span class="hljs-variable">@impl</span> <span class="hljs-keyword">true</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">handle_event</span></span>(<span class="hljs-string">"remove"</span>, %{<span class="hljs-string">"image"</span> =&gt; image}, socket) <span class="hljs-keyword">do</span>
    product = socket.assigns.product
    removed_images = [image | socket.assigns.removed_images]
    images_to_show = product.images -- removed_images
    max_entries = <span class="hljs-variable">@max_entries</span> - length(images_to_show)

    socket =
      socket
      |&gt; assign(<span class="hljs-symbol">:removed_images</span>, removed_images)
      |&gt; assign(<span class="hljs-symbol">:images_to_show</span>, images_to_show)
      |&gt; maybe_cancel_uploads()
      |&gt; allow_upload(<span class="hljs-symbol">:images</span>,
        <span class="hljs-symbol">accept:</span> <span class="hljs-string">~w(.png .jpg .jpeg)</span>,
        <span class="hljs-symbol">max_entries:</span> max_entries,
        <span class="hljs-symbol">max_file_size:</span> <span class="hljs-variable">@max_file_size</span>
      )

    {<span class="hljs-symbol">:noreply</span>, socket}
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">defp</span> <span class="hljs-title">maybe_cancel_uploads</span></span>(socket) <span class="hljs-keyword">do</span>
    {socket, _} = Phoenix.LiveView.Upload.maybe_cancel_uploads(socket)
    socket
  <span class="hljs-keyword">end</span>
</code></pre>
<p>This does the work, but not perfectly. When we cancel the active uploads in the <code>remove</code> handler, they disappear from our preview section.</p>
<p>Before removing the remaining associated image</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684676121736/a762602c-c218-4c56-a999-730be9b9216c.png" alt class="image--center mx-auto" /></p>
<p>After removing it, the previews of the two active uploads are gone. :(</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684681090341/de84f93b-f4d4-4b80-a057-3ba8993bdcc5.png" alt class="image--center mx-auto" /></p>
<p>We can mitigate this a little by showing an alert to the user informing her that the pending uploads will be removed.</p>
<pre><code class="lang-elixir">&lt;a
  phx-click=<span class="hljs-string">"remove"</span>
  phx-target={<span class="hljs-variable">@myself</span>}
  phx-value-image={image}
  class=<span class="hljs-string">"hover:text-slate-800"</span>
  data-confirm=<span class="hljs-string">"Are you sure? The pending uploads will be removed and you need to select them again."</span>
&gt;
  &lt;.icon name=<span class="hljs-string">"hero-trash"</span> /&gt;
&lt;<span class="hljs-regexp">/a&gt;</span>
</code></pre>
<p>Now a warning is shown when you try to remove an existing image and there are active uploads.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684681389701/550ee759-55e0-4c10-a9bb-e853eb206938.png" alt class="image--center mx-auto" /></p>
<p>Still, I don't quite like this approach.</p>
<p>(Find the code for this part on the <code>replace-live-file-input-config</code> branch of the git repo)</p>
<h3 id="heading-updating-the-maxentries-option-of-livefileinput">Updating the max_entries option of live_file_input</h3>
<p>Another way I tried was to change just the <code>:max_entries</code> option of the <code>allow_update()</code> call instead of completely replacing the configuration. Something like this:</p>
<pre><code class="lang-elixir">  <span class="hljs-variable">@impl</span> <span class="hljs-keyword">true</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">handle_event</span></span>(<span class="hljs-string">"remove"</span>, %{<span class="hljs-string">"image"</span> =&gt; image}, socket) <span class="hljs-keyword">do</span>
    product = socket.assigns.product
    removed_images = [image | socket.assigns.removed_images]
    images_to_show = product.images -- removed_images
    max_entries = <span class="hljs-variable">@max_entries</span> - length(images_to_show)

    socket =
      socket
      |&gt; assign(<span class="hljs-symbol">:removed_images</span>, removed_images)
      |&gt; assign(<span class="hljs-symbol">images_to_show:</span> images_to_show)
      |&gt; maybe_update_upload_config(max_entries)

    {<span class="hljs-symbol">:noreply</span>, socket}
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">defp</span> <span class="hljs-title">maybe_update_upload_config</span></span>(socket, max_entries) <span class="hljs-keyword">do</span>
    images_config = Map.get(socket.assigns.uploads, <span class="hljs-symbol">:images</span>)

    new_uploads =
      Map.put(socket.assigns.uploads, <span class="hljs-symbol">:images</span>, %{images_config | <span class="hljs-symbol">max_entries:</span> max_entries})

    assign(socket, <span class="hljs-symbol">:uploads</span>, new_uploads)
  <span class="hljs-keyword">end</span>
</code></pre>
<p>But Phoenix wasn't happy about it, telling me that the <code>:uploads</code> is a reserved assign and I can't set it directly.</p>
<pre><code class="lang-elixir">  <span class="hljs-symbol">Parameters:</span> %{<span class="hljs-string">"image"</span> =&gt; <span class="hljs-string">"/uploads/c121a0d2-2724-4c03-8f7f-dc7c78c07ccc.jpg"</span>}
[error] GenServer <span class="hljs-comment">#PID&lt;0.2770.0&gt; terminating</span>
** (ArgumentError) <span class="hljs-symbol">:uploads</span> is a reserved assign by LiveView <span class="hljs-keyword">and</span> it cannot be set directly
</code></pre>
<p>A dead-end path.</p>
<p>(Find the code for this part on the <code>update-live-file-input-config</code> branch of the git repo)</p>
<h3 id="heading-leveraging-changeset-validations">Leveraging changeset validations</h3>
<p>One way to completely work around the constraints of the <code>live_file_input</code> config is to leave the configuration as it is and, instead, rely on the <code>changeset</code> validations to apply the business logic checks.</p>
<p>Let's start with the Product's changeset. We can set a validation there to check the max amount of images allowed:</p>
<pre><code class="lang-elixir"><span class="hljs-class"><span class="hljs-keyword">defmodule</span> <span class="hljs-title">Mercury.Products.Product</span></span> <span class="hljs-keyword">do</span>
  <span class="hljs-keyword">use</span> Ecto.Schema
  <span class="hljs-keyword">import</span> Ecto.Changeset

  schema <span class="hljs-string">"products"</span> <span class="hljs-keyword">do</span>
    field <span class="hljs-symbol">:images</span>, {<span class="hljs-symbol">:array</span>, <span class="hljs-symbol">:string</span>}
    field <span class="hljs-symbol">:name</span>, <span class="hljs-symbol">:string</span>

    timestamps()
  <span class="hljs-keyword">end</span>

  <span class="hljs-variable">@max_files</span> <span class="hljs-number">3</span>

  <span class="hljs-variable">@doc</span> <span class="hljs-keyword">false</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">changeset</span></span>(product, attrs) <span class="hljs-keyword">do</span>
    product
    |&gt; cast(attrs, [<span class="hljs-symbol">:name</span>, <span class="hljs-symbol">:images</span>])
    |&gt; validate_required([<span class="hljs-symbol">:name</span>, <span class="hljs-symbol">:images</span>])
    |&gt; validate_length(<span class="hljs-symbol">:images</span>, <span class="hljs-symbol">max:</span> <span class="hljs-variable">@max_files</span>, <span class="hljs-symbol">message:</span> <span class="hljs-string">"Max number of images is <span class="hljs-subst">#{<span class="hljs-variable">@max_files</span>}</span>"</span>)
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>The <code>validate_length</code> will check that we put at most <code>@max_files</code> elements on the <code>images</code> array.</p>
<p>Now we need to ensure the <code>validate</code> and <code>save</code> events put all the images the user wants to save in the <code>images</code> attribute and the changeset will do the rest.</p>
<p>Let's first create a helper function that does the validation of the current data in the socket: the <code>@images_to_show</code> and the <code>entries</code> in the <code>live_file_input</code> config:</p>
<pre><code class="lang-elixir">  <span class="hljs-function"><span class="hljs-keyword">defp</span> <span class="hljs-title">validate_images</span></span>(socket, product_params, images_to_show) <span class="hljs-keyword">do</span>
    images =
      socket.assigns.uploads.images.entries
      |&gt; Enum.map(<span class="hljs-keyword">fn</span> entry -&gt;
        filename = <span class="hljs-string">"<span class="hljs-subst">#{entry.uuid}</span><span class="hljs-subst">#{Path.extname(entry.client_name)}</span>"</span>
        <span class="hljs-string">~p"/uploads/<span class="hljs-subst">#{filename}</span>"</span>
      <span class="hljs-keyword">end</span>)

    product_params = Map.put(product_params, <span class="hljs-string">"images"</span>, images_to_show ++ images)

    changeset =
      socket.assigns.product
      |&gt; Products.change_product(product_params)
      |&gt; Map.put(<span class="hljs-symbol">:action</span>, <span class="hljs-symbol">:validate</span>)

    assign_form(socket, changeset)
  <span class="hljs-keyword">end</span>
</code></pre>
<p>This helper doesn't really consume the entries, but does iterate over them to generate the filenames so that it can add them to the images that remain associated to the product. That's the new set of images that the user is trying to associate to the product. Then delegates to the <code>Products.change_product/2</code> (that then calls the <code>Product.changeset/2</code>) to apply the validations. Finally it replaces the changeset in the socket so that the form in the browser shows the new errors if necessary.</p>
<p>With this in place the <code>validate</code> handler is now:</p>
<pre><code class="lang-elixir">  <span class="hljs-variable">@impl</span> <span class="hljs-keyword">true</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">handle_event</span></span>(<span class="hljs-string">"validate"</span>, %{<span class="hljs-string">"product"</span> =&gt; product_params}, socket) <span class="hljs-keyword">do</span>
    images_to_show = socket.assigns.images_to_show

    socket =
      socket
      |&gt; validate_images(product_params, images_to_show)

    {<span class="hljs-symbol">:noreply</span>, socket}
  <span class="hljs-keyword">end</span>
</code></pre>
<p>The <code>save</code> handler doesn't need changes as it already performs the same logic in addition to consuming the uploads.</p>
<p>There are other places where we need to do these checks, for example, when we remove an image from the product.</p>
<p>The <code>remove</code> handler is now this:</p>
<pre><code class="lang-elixir">  <span class="hljs-variable">@impl</span> <span class="hljs-keyword">true</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">handle_event</span></span>(<span class="hljs-string">"remove"</span>, %{<span class="hljs-string">"image"</span> =&gt; image}, socket) <span class="hljs-keyword">do</span>
    product = socket.assigns.product
    removed_images = [image | socket.assigns.removed_images]
    images_to_show = product.images -- removed_images

    socket =
      socket
      |&gt; assign(<span class="hljs-symbol">:removed_images</span>, removed_images)
      |&gt; assign(<span class="hljs-symbol">:images_to_show</span>, images_to_show)
      |&gt; validate_images(%{}, images_to_show)

    {<span class="hljs-symbol">:noreply</span>, socket}
  <span class="hljs-keyword">end</span>
</code></pre>
<p>In this case, as we are not validating the full form, we can just pass an empty <code>product_params</code> to the <code>validate_images</code> function so that the changeset can validate if the <code>images</code> are within the limits.</p>
<p>The <code>cancel</code> event, triggered when we remove one active upload from the form, becomes:</p>
<pre><code class="lang-elixir">  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">handle_event</span></span>(<span class="hljs-string">"cancel"</span>, %{<span class="hljs-string">"ref"</span> =&gt; ref}, socket) <span class="hljs-keyword">do</span>
    images_to_show = socket.assigns.images_to_show

    socket =
      socket
      |&gt; cancel_upload(<span class="hljs-symbol">:images</span>, ref)
      |&gt; validate_images(%{}, images_to_show)

    {<span class="hljs-symbol">:noreply</span>, socket}
  <span class="hljs-keyword">end</span>
</code></pre>
<p>Here again, we're not validating the full form, and we can just pass an empty <code>product_params</code> to get the validations applied.</p>
<p>Finally, we show the error message in the template, if present:</p>
<pre><code class="lang-elixir">&lt;.label <span class="hljs-keyword">for</span>=<span class="hljs-string">"#images"</span>&gt;Images&lt;<span class="hljs-regexp">/.label&gt;

&lt;.error :for={{msg, _} &lt;- @form[:images].errors}&gt;&lt;%= msg %&gt;&lt;/</span>.error&gt;

&lt;div id=<span class="hljs-string">"images"</span>&gt;
</code></pre>
<p>With that in place, we can now try it.</p>
<p>We start with 3 images already in the Product:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684771133642/302a350c-813c-4cf2-8960-1a7c68b38fdf.png" alt class="image--center mx-auto" /></p>
<p>If we try to upload an extra image, the validation shows the error:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684845908794/eca3b591-5e23-4d80-a4cc-b1f3d083f13c.png" alt class="image--center mx-auto" /></p>
<p>We can now either remove one of the uploads or one of the images associated to the Product.</p>
<p>Removing one of the uploads:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684771422006/9ac76175-f63c-48af-9404-bb8bd72556f1.png" alt class="image--center mx-auto" /></p>
<p>Removing one of the associated images:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684845976438/adfb306f-d67d-4bdc-807f-5846615f8b74.png" alt class="image--center mx-auto" /></p>
<p>In both cases, the validations are reapplied and the error message is correctly handled.</p>
<p>I think this has better UX than the other options but still is not perfect. If you play around with the form you'll soon notice that the validations on other parts of the form are removed when we pass <code>%{}</code> as the product_params to the <code>validate_images</code> function. Sigh!</p>
<p>(Find the code for this part on the <code>changeset-validations</code> branch of the git repo)</p>
<h2 id="heading-last-resort">Last resort</h2>
<p>One final approach is to completely abandon <code>live_file_input</code> and <code>changesets</code> and perform all the logic in the <code>form_component.ex</code> module:</p>
<ul>
<li><p>Use a boolean <em>assign</em> to toggle the showing/hiding of the max files error message in the template</p>
</li>
<li><p>Set/unset the toggle in each of the <code>remove</code>, <code>cancel</code>, <code>validate</code>, and <code>save</code> handlers</p>
</li>
<li><p>Check the <em>assign</em> before saving so that we don't save the form if the toggle is set.</p>
</li>
</ul>
<p>This will work and won't mess with the form's internal errors or with the <code>live_file_input</code> configuration.</p>
<p>But also it feels like is not in line with the "Elixir way" of doing things. But hey, it works, right?</p>
<h2 id="heading-final-considerations">Final considerations</h2>
<p>This post is different than the ones I usually write, where I end with a happy feeling of accomplishment. This one feels like I personally couldn't find a way to make it work in an elegant way.</p>
<p>One thing is for sure, <code>live_file_input</code> is amazing for the use case it was created. I am sure it will evolve to support or at least facilitate other use cases like the one discussed here.</p>
<p>Judging by the lightning speed the Phoenix core team and contributors gift us with amazing tools, I'm sure minds more brilliant than mine will find a better way to handle scenarios like this one.</p>
<p>And just to avoid any misunderstanding, I'm not complaining about <code>live_file_input</code>, the bittersweet conclusions of this post fall totally on me and my limited experience with the way LiveView works.</p>
<p>What do you people think? Do you have another solution to this scenario that you want to share? I for sure want to learn it!</p>
<p>You can find the code for this article in this <a target="_blank" href="https://github.com/miguelcoba/mercury">github repo</a></p>
]]></content:encoded></item><item><title><![CDATA[Elixir metrics and StatsD]]></title><description><![CDATA[Last time we saw how to send our metrics to Prometheus. This time, we'll send them to a StatsD server.
What's StatsD?
StatsD is a network daemon that listens for statistics, like counters and timers, sent over UDP or TCP and sends aggregates to one o...]]></description><link>https://blog.miguelcoba.com/elixir-metrics-and-statsd</link><guid isPermaLink="true">https://blog.miguelcoba.com/elixir-metrics-and-statsd</guid><category><![CDATA[Elixir]]></category><category><![CDATA[statsd]]></category><category><![CDATA[metrics]]></category><dc:creator><![CDATA[Miguel Cobá]]></dc:creator><pubDate>Thu, 02 Jun 2022 22:55:07 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/qwtCeJ5cLYs/upload/v1654210247636/qfEP7w1Nb.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Last time we saw how to send our metrics to Prometheus. This time, we'll send them to a StatsD server.</p>
<h1 id="heading-whats-statsd">What's StatsD?</h1>
<p><a target="_blank" href="https://github.com/statsd/statsd">StatsD</a> is a network daemon that listens for statistics, like counters and timers, sent over UDP or TCP and sends aggregates to one or more pluggable backend services.</p>
<p>StatsD does aggregation of data.</p>
<p>To visualize the data you need another tool. For example, to create graphs from the aggregated data you can use <a target="_blank" href="http://graphiteapp.org/">Graphite</a>.</p>
<p>StatsD forwards aggregated data to </p>
<h1 id="heading-whats-graphite">What's Graphite?</h1>
<p>Graphite does two things:</p>
<ol>
<li>Store numeric time-series data</li>
<li>Render graphs of this data on demand</li>
</ol>
<p>In summary, StatsD collects and aggregates metrics and then sends them to Graphite to be rendered in nice graphs.</p>
<p>In this tutorial we'll send our Elixir metrics to StatsD and then we'll use the Graphite dashboard to see the graphs generated from our metrics.</p>
<h2 id="heading-install-statsd-and-graphite">Install StatsD and Graphite</h2>
<p>There is a way to install separately StatsD server and Graphite but for this tutorial, I'll just use the official Graphite <a target="_blank" href="https://github.com/graphite-project/docker-graphite-statsd">docker image</a> that also includes the StatsD server.</p>
<p>In a terminal run:</p>
<pre><code class="lang-bash">docker run -d \
 --name graphite \
 --restart=always \
 -p 80:80 \
 -p 2003-2004:2003-2004 \
 -p 2023-2024:2023-2024 \
 -p 8125:8125/udp \
 -p 8126:8126 \
 graphiteapp/graphite-statsd
</code></pre>
<p>This will start the StatsD on port 8125 and the Graphite dashboard on port 80.</p>
<p>Let's add some sample data just to check that the graphs render correctly.</p>
<p>Evaluate this on another terminal:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">echo</span> <span class="hljs-string">"foo:2|c"</span> | nc -u -w0 127.0.0.1 8125
</code></pre>
<p>Wait at least 10 seconds and then run this:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">echo</span> <span class="hljs-string">"foo:4|c"</span> | nc -u -w0 127.0.0.1 8125
</code></pre>
<p>We have sent two counter values separated by at least 10 seconds.</p>
<p>Let's open a browser and go to http://localhost. You'll see something like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1654033979086/9gqwIs1gn.png" alt="Screen Shot 2022-05-31 at 23.52.51.png" /></p>
<p>On the left panel expand the tree (Metrics -&gt; stats_count) and select the <code>foo</code> entry.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1654034083329/S43tmuoZ5.png" alt="Screen Shot 2022-05-31 at 23.54.36.png" /></p>
<p>You'll see the graph on the main panel populated with some data. But it is too difficult to see the data. Let's change the resolution of the graph.</p>
<p>Click on the clock icon:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1654034321217/iezOCrQwb.png" alt="Screen Shot 2022-05-31 at 23.57.36.png" /></p>
<p>And select a time range of 10 minutes. Now you can see clearly the two data points we send to StatsD:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1654034325457/-7LPrFSNs.png" alt="Screen Shot 2022-05-31 at 23.58.08.png" /></p>
<p>Why we had to wait 10 seconds in the previous examples? It is because that's the default flush interval timeout that StatsD waits before aggregating the stats and sending them to Graphite.</p>
<p>Try sending this data one after the other without any delay:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">echo</span> <span class="hljs-string">"foo:3|c"</span> | nc -u -w0 127.0.0.1 8125
<span class="hljs-built_in">echo</span> <span class="hljs-string">"foo:5|c"</span> | nc -u -w0 127.0.0.1 8125
</code></pre>
<p>If you go to the graph and click the <code>Auto-Refresh</code> button, you'll see that a new value appears on the graph, but the value is <code>8</code>, that, as you can tell is the sum of <code>3</code> and <code>5</code> that we sent. StatsD aggregated all the values inside a flush interval and sent the result to Graphite.</p>
<p>Ok, enough with StatsD and Graphite. Let's do some Elixir.</p>
<h2 id="heading-send-elixir-metrics-to-statsd">Send Elixir metrics to StatsD</h2>
<p>Let's continue using the project we created for this series. Clone it from <a target="_blank" href="https://github.com/miguelcoba/metrics">here</a> if you don't have it. We'll start working on the <code>main</code> branch</p>
<p>Add the <code>:telemetry_metrics_statsd</code> dependency to <code>mix.exs</code>:</p>
<pre><code class="lang-elixir">  defp deps do
    [
      {:telemetry, "~&gt; 1.0"},
      {:telemetry_metrics, "~&gt; 0.6.1"},
      {:telemetry_metrics_statsd, "~&gt; 0.6.0"}
    ]
  end
</code></pre>
<p>and run <code>mix deps.get</code></p>
<p>Now edit the <code>telemetry.ex</code> file and add the <code>TelemetryMetricsStatsd</code> reporter to the list of children:</p>
<pre><code class="lang-elixir">  def init(_arg) do
    children = [
      {TelemetryMetricsStatsd, metrics: metrics()}
      {Metrics.Telemetry.CustomReporter, metrics: metrics()}
    ]

    Supervisor.init(children, strategy: :one_for_one)
  end
</code></pre>
<p>Now our metrics will be also sent to the StatsD server. By default the <code>TelemetryMetricsStatsd</code> reporter sends the data to <code>127.0.0.1:8125</code>, but you can configure the host and port passing additional <code>:host</code> and <code>:port</code> options.</p>
<p>One thing to know is that the reporter doesn't aggregate metrics. It just forwards them to StatsD and it is StatsD the one that will do the aggregation before sending the data to Graphite.</p>
<p>Let's generate some metrics. Start the application:</p>
<pre><code class="lang-bash">iex -S mix
</code></pre>
<p>and run this:</p>
<pre><code class="lang-elixir">1..80 |&gt; Enum.each(&amp;Metrics.emit(&amp;1))
</code></pre>
<p>Wait at least 10 seconds and then run:</p>
<pre><code class="lang-elixir">1..100 |&gt; Enum.each(&amp;Metrics.emit(&amp;1))
</code></pre>
<p>If you navigate the left pane and visualize the <code>stats_count.metrics.emit.value</code> you'll see a graph like the following, showing that the aggregated values in two subsequent intervals are 80 and 100. This is because we generated 80 metric events in the first flush interval and after 10 seconds we generated another 100 events in the second flush interval:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1654152493329/QvjR7Zuis.png" alt="Screen Shot 2022-06-02 at 08.47.59.png" /></p>
<p>If you now visualize the <code>stats.gauges.metrics.emit.value</code> you'll see the sum metric values. </p>
<p>You'll see that in the first interval the value is <code>3240</code>: the sum of the first 80 events. </p>
<p>In the second interval, the value is <code>8290</code>: the sum of the next 100 events.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1654152546678/lOxMcBCi8.png" alt="Screen Shot 2022-06-02 at 08.48.59.png" /></p>
<p>Now our application is using a new reporter to send the metrics to a StatsD server and we are using Graphite to visualize the metrics in configurable graphs.</p>
<p>There is no limit to the amount of data you can send to StatsD and visualize it with Graphite. You can also configure Graphite and save graphs arranged in dashboards so that you can see your data all together in a simple place</p>
<p>You can find the code for this article in the <code>metrics-statsd</code> branch in the <a target="_blank" href="https://github.com/miguelcoba/metrics">github repo</a>.</p>
<h2 id="heading-about">About</h2>
<p>I'm <a target="_blank" href="https://miguelcoba.com">Miguel Cobá</a>. I write about Elixir, Elm, Software Development, and eBook writing.</p>
<ul>
<li>Follow me on <a target="_blank" href="https://twitter.com/MiguelCoba_">Twitter</a></li>
<li>Subscribe to my <a target="_blank" href="https://newsletter.miguelcoba.com">newsletter</a></li>
<li>Read all my articles on my <a target="_blank" href="https://blog.miguelcoba.com">blog</a></li>
<li>Get my books:<ul>
<li><a target="_blank" href="https://store.miguelcoba.com/l/100elixirtips">100 Elixir Tips</a> [FREE]</li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/deployingelixir">Deploying Elixir</a> [FREE]</li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/advancedtopics">Deploying Elixir: Advanced Topics</a></li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/ebookwriting">eBook Writing Workflow for Developers</a></li>
</ul>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Elixir, Kubernetes, and minikube]]></title><description><![CDATA[A couple of weeks ago I presented my talk:
Elixir, Kubernetes, and minikube
in the Elixir Meetup #5 organized by the fine people on curiosum.com.
This talk is based on chapter 4 of my book "Deploying Elixir: Advanced Topics". 
If you haven't gotten t...]]></description><link>https://blog.miguelcoba.com/elixir-kubernetes-and-minikube</link><guid isPermaLink="true">https://blog.miguelcoba.com/elixir-kubernetes-and-minikube</guid><category><![CDATA[Elixir]]></category><category><![CDATA[Kubernetes]]></category><dc:creator><![CDATA[Miguel Cobá]]></dc:creator><pubDate>Sun, 29 May 2022 22:59:03 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1653864955187/ZVh24_ks1.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A couple of weeks ago I presented my talk:</p>
<p><strong>Elixir, Kubernetes, and minikube</strong></p>
<p>in the Elixir Meetup #5 organized by the fine people on curiosum.com.</p>
<p>This talk is based on chapter 4 of my book <a target="_blank" href="https://store.miguelcoba.com/l/advancedtopics">"Deploying Elixir: Advanced Topics"</a>. </p>
<p>If you haven't gotten the book, this is an excellent way to see what the book is about.</p>
<p>Watch the recorded talk here:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=AvUPX6cAjzk&amp;t=995s">https://www.youtube.com/watch?v=AvUPX6cAjzk&amp;t=995s</a></div>
<h2 id="heading-about">About</h2>
<p>I'm <a target="_blank" href="https://miguelcoba.com">Miguel Cobá</a>. I write about Elixir, Elm, Software Development, and eBook writing.</p>
<ul>
<li>Follow me on <a target="_blank" href="https://twitter.com/MiguelCoba_">Twitter</a></li>
<li>Subscribe to my <a target="_blank" href="https://newsletter.miguelcoba.com">newsletter</a></li>
<li>Read all my articles on my <a target="_blank" href="https://blog.miguelcoba.com">blog</a></li>
<li>Get my books:<ul>
<li><a target="_blank" href="https://store.miguelcoba.com/l/100elixirtips">100 Elixir Tips</a> [FREE]</li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/deployingelixir">Deploying Elixir</a> [FREE]</li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/advancedtopics">Deploying Elixir: Advanced Topics</a></li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/ebookwriting">eBook Writing Workflow for Developers</a></li>
</ul>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA["Deploying Elixir: Advanced Topics" eBook]]></title><description><![CDATA[I have written a new Elixir book:
Deploying Elixir: Advanced Topics

It is based on the feedback and suggestions I got from the readers of my previous book "Deploying Elixir".
They asked me to write more about clustering in Elixir and deploying to Ku...]]></description><link>https://blog.miguelcoba.com/deploying-elixir-advanced-topics-ebook</link><guid isPermaLink="true">https://blog.miguelcoba.com/deploying-elixir-advanced-topics-ebook</guid><category><![CDATA[Elixir]]></category><category><![CDATA[AWS]]></category><category><![CDATA[Azure]]></category><category><![CDATA[GCP]]></category><category><![CDATA[Kubernetes]]></category><dc:creator><![CDATA[Miguel Cobá]]></dc:creator><pubDate>Tue, 03 May 2022 22:09:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1651661634498/R5Shd4ayL.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I have written a new Elixir book:</p>
<h2 id="heading-deploying-elixir-advanced-topics">Deploying Elixir: Advanced Topics</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1651609510451/8Bdw9n6rY.png" alt="Deploying Elixir: Advanced Topics" /></p>
<p>It is based on the feedback and suggestions I got from the readers of my previous book "Deploying Elixir".</p>
<p>They asked me to write more about clustering in Elixir and deploying to Kubernetes.</p>
<p><strong>I'll take you from zero to deploying a Kubernetes cluster in the three big Cloud Providers.</strong></p>
<p>In this book:</p>
<ul>
<li>I'll show you how to create an Elixir Release from your application and Dockerize it</li>
<li>Learn what an Erlang cluster is and how to create one</li>
<li>We explore the different strategies to create clusters using the <code>libcluster</code> library</li>
<li>I'll show you how to deploy it to a Kubernetes cluster using minikube</li>
<li>When you feel comfortable scaling your app I'll show you how to deploy to Amazon Web Service, Microsoft Azure, and Google Cloud Platform</li>
</ul>
<p>You can download a sample chapter here:</p>
<p><a target="_blank" href="https://miguelcoba.com/deploying-elixir-advanced-topics-sample.pdf">Download sample chapter</a></p>
<p>If you prefer a video version, I presented part of chapter 4 in the this Elixir meetup talk:</p>
<p><a target="_blank" href="https://youtu.be/AvUPX6cAjzk?t=995">Watch it here</a></p>
<p>Get the full book here:</p>
<h2 id="heading-deploying-elixir-advanced-topicshttpsstoremiguelcobacomladvancedtopics"><a target="_blank" href="https://store.miguelcoba.com/l/advancedtopics">Deploying Elixir: Advanced Topics</a></h2>
]]></content:encoded></item><item><title><![CDATA[Elixir Wizards Podcast S8E1]]></title><description><![CDATA[A couple of weeks ago I was invited to talk on the Elixir Wizards podcast by Smartlogic.
We talk about Elixir in polyglot environments (that is, where you have a lot of languages used at the same time), my beginnings in the software field, my books, ...]]></description><link>https://blog.miguelcoba.com/elixir-wizards-podcast-s8e1</link><guid isPermaLink="true">https://blog.miguelcoba.com/elixir-wizards-podcast-s8e1</guid><category><![CDATA[Elixir]]></category><category><![CDATA[podcast]]></category><dc:creator><![CDATA[Miguel Cobá]]></dc:creator><pubDate>Fri, 15 Apr 2022 11:04:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1650020568472/28As4gjgY.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A couple of weeks ago I was invited to talk on the <a target="_blank" href="https://smartlogic.io/podcast/elixir-wizards/">Elixir Wizards</a> podcast by <a target="_blank" href="https://smartlogic.io/">Smartlogic</a>.</p>
<p>We talk about Elixir in polyglot environments (that is, where you have a lot of languages used at the same time), my beginnings in the software field, my books, and other sideprojects.</p>
<p>I had a lot of fun chatting with <a target="_blank" href="https://twitter.com/sundikhin">Sundi Myint</a> and <a target="_blank" href="https://twitter.com/owenbickford">Owen Bickford</a> who were amazing in guiding my ramblings to a coherent discussion.
Thank you so much for having me.</p>
<p>You can listen to the podcast here:</p>
<p><a target="_blank" href="https://smartlogic.io/podcast/elixir-wizards/s8e1-coba/">Elixir Wizards S8E1</a></p>
<h2 id="heading-about">About</h2>
<p>I'm <a target="_blank" href="https://miguelcoba.com">Miguel Cobá</a>. I write about Elixir, Elm, Software Development, and eBook writing.</p>
<ul>
<li>Follow me on <a target="_blank" href="https://twitter.com/MiguelCoba_">Twitter</a></li>
<li>Subscribe to my <a target="_blank" href="https://newsletter.miguelcoba.com">newsletter</a></li>
<li>Read all my articles on my <a target="_blank" href="https://blog.miguelcoba.com">blog</a></li>
<li>Get my books:<ul>
<li><a target="_blank" href="https://store.miguelcoba.com/l/100elixirtips">100 Elixir Tips</a> [FREE]</li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/deployingelixir">Deploying Elixir</a> [FREE]</li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/advancedtopics">Deploying Elixir: Advanced Topics</a></li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/ebookwriting">eBook Writing Workflow for Developers</a></li>
</ul>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[[PRESALE] Deploying Elixir: Advanced Topics]]></title><description><![CDATA[I have a new Elixir book for you:
"Deploying Elixir: Advanced Topics"

What is it about?
I'll show you how to deploy to:

AWS
Azure
GCP

You'll deploy like a PRO using:

clustering

Kubernetes orchestration


Tell me more
This book expands on my prev...]]></description><link>https://blog.miguelcoba.com/presale-deploying-elixir-advanced-topics</link><guid isPermaLink="true">https://blog.miguelcoba.com/presale-deploying-elixir-advanced-topics</guid><category><![CDATA[Elixir]]></category><category><![CDATA[AWS]]></category><category><![CDATA[Azure]]></category><category><![CDATA[GCP]]></category><category><![CDATA[deployment]]></category><dc:creator><![CDATA[Miguel Cobá]]></dc:creator><pubDate>Thu, 07 Apr 2022 12:49:56 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1649294682290/9SAF8y69O.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I have a new Elixir book for you:</p>
<p><strong>"Deploying Elixir: Advanced Topics"</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1649294682290/9SAF8y69O.png" alt="Book cover" /></p>
<h2 id="heading-what-is-it-about">What is it about?</h2>
<p>I'll show you how to deploy to:</p>
<ul>
<li>AWS</li>
<li>Azure</li>
<li>GCP</li>
</ul>
<p>You'll deploy like a PRO using:</p>
<ul>
<li><p>clustering</p>
</li>
<li><p>Kubernetes orchestration</p>
</li>
</ul>
<h2 id="heading-tell-me-more">Tell me more</h2>
<p>This book expands on my previous book "Deploying Elixir".</p>
<p>Many of you asked me to write about common scenarios used in the Enterprise! 🏭 </p>
<p>Well, I did it! 💪</p>
<p>If you want to deploy your application like the PROS in the Industry do, this book is for you! 🎯</p>
<h2 id="heading-when-it-will-be-released">When it will be released?</h2>
<p>The book will be published on <strong>May 3rd, 2022</strong>! 🗓️</p>
<p>But as a <strong>SPECIAL, TIME-LIMITED OFFER</strong> you can get <strong>35% off</strong> the final price if you purchase it on <strong>PRESALE</strong>.</p>
<p>⚡ <a target="_blank" href="https://miguelcoba.gumroad.com/l/advancedtopics">Get it on PRESALE and save 35% off the final price</a> ⚡</p>
]]></content:encoded></item><item><title><![CDATA[Elixir, Telemetry, and Prometheus]]></title><description><![CDATA[Yes, just as you read it in the title. I'm going to show you how to send your Elixir telemetry data to Prometheus.
First, let's recapitulate. In the previous article, I showed you how to create an Elixir Metrics Reporter to do the aggregation of your...]]></description><link>https://blog.miguelcoba.com/elixir-telemetry-and-prometheus</link><guid isPermaLink="true">https://blog.miguelcoba.com/elixir-telemetry-and-prometheus</guid><category><![CDATA[Elixir]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Miguel Cobá]]></dc:creator><pubDate>Mon, 21 Feb 2022 23:25:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/PkZo8mERNak/upload/v1644794981791/1zlSmksZ3.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Yes, just as you read it in the title. I'm going to show you how to send your Elixir telemetry data to <a target="_blank" href="https://prometheus.io/">Prometheus</a>.</p>
<p>First, let's recapitulate. In the <a target="_blank" href="https://blog.miguelcoba.com/telemetry-and-metrics-in-elixir">previous article</a>, I showed you how to create an Elixir Metrics Reporter to do the aggregation of your telemetry data and convert it to meaningful metrics. 
Now I am going to show you how to use the <code>telemetry_metrics_prometheus</code> library to expose this data so that Prometheus can retrieve it and process it.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>We are going to use the repo we did for the previous article. Clone it from <a target="_blank" href="https://github.com/miguelcoba/metrics">here</a>.</p>
<h2 id="heading-install-the-telemetrymetricsprometheus-library">Install the telemetry_metrics_prometheus library</h2>
<p>Add the <code>telemetry_metrics_prometheus</code> to your <code>mix.exs</code> dependencies.</p>
<pre><code class="lang-elixir">  defp deps do
    [
      {:telemetry, "~&gt; 1.0"},
      {:telemetry_metrics, "~&gt; 0.6.1"},
      {:telemetry_metrics_prometheus, "~&gt; 1.1.0"}
    ]
  end
</code></pre>
<p>Then get the new dependency:</p>
<pre><code class="lang-bash">mix deps.get
</code></pre>
<h2 id="heading-configuring-the-prometheus-reporter">Configuring the Prometheus reporter</h2>
<p>Now we need to add the reporter to <code>telemetry.ex</code>. Change the <code>init/1</code> function to this:</p>
<pre><code class="lang-elixir">  def init(_arg) do
    children = [
      {TelemetryMetricsPrometheus, metrics: metrics()},
      {Metrics.Telemetry.CustomReporter, metrics: metrics()}
    ]

    Supervisor.init(children, strategy: :one_for_one)
  end
</code></pre>
<p>And that should be all we need to do.</p>
<p>Unfortunately, it is not. The current version of the library doesn't allow several metrics to have the same metric name. Right now, we have this:</p>
<pre><code class="lang-elixir">  defp metrics do
    [
      counter("metrics.emit.value"),
      sum("metrics.emit.value")
    ]
  end
</code></pre>
<p>We need to use different metric names but still handle the same events. We can take advantage of the second parameter to the metric functions that allow us to be explicit about events and measurements. Let's change it to this:</p>
<pre><code class="lang-elixir">  defp metrics do
    [
      counter("metrics.emit.value.counter", event_name: [:metrics, :emit], measurement: :value),
      sum("metrics.emit.value.sum", event_name: [:metrics, :emit], measurement: :value)
    ]
  end
</code></pre>
<p>You can see that now the metric names are different but the event names and measurements are correct. With this, <code>telemetry_metrics_prometheus</code> will work correctly.</p>
<h2 id="heading-testing-the-metrics-generated">Testing the metrics generated</h2>
<p>The Prometheus library includes a web server that exposes a <code>/metrics</code> endpoint on port <code>9568</code>. We can use it to check the metrics collected by Prometheus. Let's try it. Start your app with iex:</p>
<pre><code class="lang-bash">iex -S mix
</code></pre>
<p>and go to http://localhost:9568/metrics</p>
<p>And you'll see an empty page. Disappointing. But that's because we haven't yet generated any event.</p>
<p>Let's emit some events. In your just opened <code>iex</code> session write this:</p>
<pre><code class="lang-elixir">iex -S mix
iex(1)&gt; 1..100 |&gt; Enum.each(&amp;Metrics.emit(&amp;1))
Metric: Elixir.Telemetry.Metrics.Counter. Current value: {1, 0}
Metric: Elixir.Telemetry.Metrics.Sum. Current value: {1, 1}
...
Metric: Elixir.Telemetry.Metrics.Counter. Current value: {100, 4950}
Metric: Elixir.Telemetry.Metrics.Sum. Current value: {100, 5050}
:ok
iex(2)&gt;
</code></pre>
<p>As you can see, I am emitting 100 events, with values from 1 to 100. As we have <code>count</code> and a <code>sum</code> metrics defined, we should get the count of events (100) and the sum of all those events (the sum from values from 1 to 100 is 5050).</p>
<p>From the <code>iex</code> output, at least we can affirm that our <code>CustomReporter</code> works. What about the Prometheus reporter?</p>
<p>Go again to http://localhost:9568/metrics and you'll see this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1644782624676/lC9Vik5Tb.png" alt="Output of the /metrics endpoint" /></p>
<p>Yay, the output of the <code>/metrics</code> endpoint is correctly counting and adding the values we emit with our events. Good.</p>
<p>We now need <code>Prometheus</code> to ingest this information. So far, our app is only exposing that endpoint but if nobody access it, is not really useful.</p>
<h2 id="heading-install-prometheus">Install Prometheus</h2>
<p>Let's install <code>Prometheus</code>. You can go to the <a target="_blank" href="https://prometheus.io/docs/prometheus/latest/getting_started/">prometheus page</a> and follow the install instructions. Or, if you are in mac, use <code>brew</code> to install it. </p>
<p>For this tutorial, I am assuming you used brew.</p>
<pre><code class="lang-bash">brew install prometheus
</code></pre>
<p>This will install it locally and you can either start it manually or as a service. But first, we need to configure it. 
The output of <code>brew install</code> will show where the configuration files are. You should see messages similar to these:</p>
<pre><code class="lang-bash">==&gt; Caveats
When run from `brew services`, `prometheus` is run from
`prometheus_brew_services` and uses the flags <span class="hljs-keyword">in</span>:
   /usr/<span class="hljs-built_in">local</span>/etc/prometheus.args

To restart prometheus after an upgrade:
  brew services restart prometheus
Or, <span class="hljs-keyword">if</span> you don<span class="hljs-string">'t want/need a background service you can just run:
  /usr/local/opt/prometheus/bin/prometheus_brew_services</span>
</code></pre>
<p> Be sure to look at that output to know where it is. On my laptop, the config file is in <code>/usr/local/etc/prometheus.yml</code></p>
<p>Open it and add a new section for our elixir app:</p>
<pre><code><span class="hljs-attribute">global</span>:
  <span class="hljs-attribute">scrape_interval</span>: <span class="hljs-number">15s</span>

<span class="hljs-attribute">scrape_configs</span>:
  - <span class="hljs-attribute">job_name</span>: <span class="hljs-string">"prometheus"</span>
    <span class="hljs-attribute">static_configs</span>:
    - <span class="hljs-attribute">targets</span>: [<span class="hljs-string">"localhost:9090"</span>]
  - <span class="hljs-attribute">job_name</span>: <span class="hljs-string">"telemetry_metrics_prometheus"</span>
    <span class="hljs-attribute">static_configs</span>:
    - <span class="hljs-attribute">targets</span>: [<span class="hljs-string">"localhost:9568"</span>]
</code></pre><p>I only added the last <code>job_name</code> named <code>"telemetry_metrics_prometheus"</code>. You can see that I specified the <code>9568</code> port and left out the <code>/metrics</code> part. Prometheus by default tries to access the <code>/metrics</code> endpoint of the target.</p>
<p>Save it and now we can start the <code>Prometheus</code> server:</p>
<pre><code class="lang-bash">brew services restart prometheus
Stopping `prometheus`... (might take a <span class="hljs-keyword">while</span>)
==&gt; Successfully stopped `prometheus` (label: homebrew.mxcl.prometheus)
==&gt; Successfully started `prometheus` (label: homebrew.mxcl.prometheus)
</code></pre>
<p>Prometheus runs on port <code>9090</code> so open http://localhost:9090/ and you'll see something like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1644783803990/4TFovs-Ht.png" alt="Prometheus home page" /></p>
<p>Not much to see yet, but that's because we haven't specified what metric we want to see. If you remember the metrics we expose on <code>/metrics</code> have the names <code>metrics_emit_value_sum</code> and <code>metrics_emit_value_counter</code>.  Let's use <code>metrics_emit_value_sum</code> to see the values Prometheus scrapped from our Elixir app. Put it in the "Expression" input field and click on the "Execute" button.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1644784010051/jPHkflik8.png" alt="Searching for metrics_emit_value_sum" /></p>
<p>You'll see now the last value Prometheus scrapped from our Elixir app:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1644784041950/WBbSF7jk0.png" alt="Last scrapped value" /></p>
<p>Nice. If you go to the <code>graph</code> tab you'll see the value too.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1644793244891/c9tZ8KfB0.png" alt="graph tab showing the sum metric" /></p>
<p>As we only have a single data point, we see a simple horizontal line. Let's add more values, but let's wait 15 seconds between each one (that's the interval Prometheus waits between scraps).</p>
<pre><code class="lang-elixir">1..80 |&gt; Enum.each(&amp;Metrics.emit(&amp;1))
1..110 |&gt; Enum.each(&amp;Metrics.emit(&amp;1))
1..50 |&gt; Enum.each(&amp;Metrics.emit(&amp;1))
1..170 |&gt; Enum.each(&amp;Metrics.emit(&amp;1))
</code></pre>
<p>After that, if you refresh the graph, you'll see something like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1644787523441/-LmMS_kVL.png" alt="Updated graph" /></p>
<p>That's it. Prometheus is now scraping the values from the <code>/metrics</code> endpoint in our Elixir application.</p>
<h2 id="heading-whats-next">What's next</h2>
<p>There is a lot more to explore from here. You could for example integrate with <a target="_blank" href="https://grafana.com/">Grafana</a> to create stunning dashboards for your metrics. If you go that way, don't forget to take a look at the <a target="_blank" href="https://hex.pm/packages/prom_ex">prom_ex</a> library, because it automates exposing your Prometheus data and creating Grafana dashboards on application start.</p>
<h2 id="heading-summary">Summary</h2>
<p>We learned:</p>
<ul>
<li>what the <code>telemetry_metrics_prometheus</code> library is</li>
<li>how to scrap our metrics with <code>Prometheus</code></li>
</ul>
<h2 id="heading-source-code">Source code</h2>
<p>You can find the source code for this article in this <a target="_blank" href="https://github.com/miguelcoba/metrics">GitHub repository</a>. Check the <code>metrics-prometheus</code>branch.</p>
<h2 id="heading-about">About</h2>
<p>I'm <a target="_blank" href="https://miguelcoba.com">Miguel Cobá</a>. I write about Elixir, Elm, Software Development, and eBook writing.</p>
<ul>
<li>Follow me on <a target="_blank" href="https://twitter.com/MiguelCoba_">Twitter</a></li>
<li>Subscribe to my <a target="_blank" href="https://newsletter.miguelcoba.com">newsletter</a></li>
<li>Read all my articles on my <a target="_blank" href="https://blog.miguelcoba.com">blog</a></li>
<li>Get my books:<ul>
<li><a target="_blank" href="https://store.miguelcoba.com/l/100elixirtips">100 Elixir Tips</a> [FREE]</li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/deployingelixir">Deploying Elixir</a> [FREE]</li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/advancedtopics">Deploying Elixir: Advanced Topics</a></li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/ebookwriting">eBook Writing Workflow for Developers</a></li>
</ul>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Telemetry and Metrics in Elixir]]></title><description><![CDATA[In the previous article, I introduced you to Telemetry. The telemetry library provides a way to:

generate events
create handler functions that will receive the events
register which handlers will be called when an event occurs

The principal advanta...]]></description><link>https://blog.miguelcoba.com/telemetry-and-metrics-in-elixir</link><guid isPermaLink="true">https://blog.miguelcoba.com/telemetry-and-metrics-in-elixir</guid><category><![CDATA[Elixir]]></category><category><![CDATA[Beginner Developers]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Miguel Cobá]]></dc:creator><pubDate>Sun, 13 Feb 2022 23:25:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/JKUTrJ4vK00/upload/v1644681099405/f6htAzrSx.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the <a target="_blank" href="https://blog.miguelcoba.com/introduction-to-telemetry-in-elixir">previous</a> article, I introduced you to Telemetry. The <code>telemetry</code> library provides a way to:</p>
<ul>
<li><strong>generate</strong> events</li>
<li><strong>create</strong> handler functions that will receive the events</li>
<li><strong>register</strong> which handlers will be called when an event occurs</li>
</ul>
<p>The principal advantage of <code>telemetry</code> is a <strong>standardized</strong> way to emit events and collect measurements in Elixir. This way, third-party libraries and code that we write ourselves can use it to generate events in a consistent way.</p>
<p>Let's now take a step back and consider a couple of things. An event is just a measure of something that happened at a given point in time. By itself doesn't tell us much. We might need more data to compare it against or to infer some trend or some rate of change. But the measure by itself is not very useful.</p>
<p>With that in mind, the telemetry developers created a library called <code>telemetry_metrics</code>. According to their documentation, <code>telemetry_metrics</code> is:</p>
<blockquote>
<p>Common interface for defining metrics based on :telemetry events.</p>
</blockquote>
<p>Ok, but what are metrics, and how it is different from measurements taken with events? Well, again according to their docs:</p>
<blockquote>
<p>Metrics are aggregations of Telemetry events with specific name, providing a view of the system's behavior over time.</p>
</blockquote>
<p>Ok, it seems clear, right? Not really. </p>
<p>If you start using the <code>telemetry_metrics</code> library you'll notice that in fact <strong>DOESN'T AGGREGATE</strong> anything!!!</p>
<p>What? </p>
<p>Exactly that. Doesn't aggregate anything. </p>
<p>If you generate 1000 events with a measurement each, this library is not going to keep, manipulate, calculate, summarize or do anything by itself.</p>
<p>So, what is useful for, you might ask? </p>
<p>Its aim is to <strong>define</strong> what kind of metrics you're going to create from events. That's it. Just a declaration, a definition, a contract if you will. Nothing else.</p>
<p>So, how do you get the real metrics, the processed results of your carefully collected measurements?</p>
<p>For that, you need something else. You need a <code>reporter</code>.</p>
<p>The reporter's responsibility is to aggregate and process each event received so that it conforms to the specified metric it handles.</p>
<p><code>telemetry_metrics</code> ships with a simple reporter called <code>ConsoleReporter</code> that doesn't do much. It only outputs the measurement and metric details to the console. Not much, but allows you to verify that everything works and you're collecting data to generate metrics correctly.</p>
<p>For more useful reporters you should either write them yourself or get them from someone that has already written it.</p>
<p>In this article, I am going to show you how to use the default <code>ConsoleReporter</code> and how to create a custom reporter.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Use <code>asdf</code> and install these versions of elixir and erlang:</p>
<pre><code class="lang-console">asdf install erlang 24.2.1
asdf global erlang 24.2.1
asdf install elixir 1.13.3-otp-24
asdf global elixir 1.13.3-otp-24
</code></pre>
<h2 id="heading-create-an-elixir-app">Create an Elixir app</h2>
<pre><code>mix <span class="hljs-keyword">new</span> metrics <span class="hljs-operator">-</span><span class="hljs-operator">-</span>sup
</code></pre><p>This will create an Elixir app with an application callback where we can attach our telemetry events and configure our metrics.</p>
<h2 id="heading-install-dependencies">Install dependencies</h2>
<p>Add <code>telemetry</code> and <code>telemetry_metrics</code> to your <code>mix.exs</code>:</p>
<pre><code class="lang-elixir">  defp deps do
    [
      {:telemetry, "~&gt; 1.0"},
      {:telemetry_metrics, "~&gt; 0.6.1"}
    ]
  end
</code></pre>
<h2 id="heading-create-an-event-emitter">Create an event emitter</h2>
<p>Let's create a simple event emitter that we can use to test our metrics. Open the <code>lib/metrics.ex</code> file that mix created for you and replace its contents with this:</p>
<pre><code class="lang-elixir">defmodule Metrics do
  def emit(value) do
    :telemetry.execute([:metrics, :emit], %{value: value})
  end
end
</code></pre>
<p>As you can see it uses <code>:telemetry.execute</code> to emit an event passing the value we provide.</p>
<h2 id="heading-define-some-metrics">Define some metrics</h2>
<p>Ok, now the interesting part: let's define some metrics we want to collect. <code>telemetry_metrics</code> allows to create several types of metrics:</p>
<ul>
<li><code>counter/2</code> which counts the total number of emitted events</li>
<li><code>sum/2</code> which keeps track of the sum of selected measurement</li>
<li><code>last_value/2</code> holding the value of the selected measurement from the most recent event</li>
<li><code>summary/2</code> calculating statistics of the selected measurement, like maximum, mean, percentiles, etc.</li>
<li><code>distribution/2</code> which builds a histogram of selected measurement</li>
</ul>
<p>We are going to start with the basics and define a counter metric, assuming we want to count how many times the event has happened.</p>
<p>Create a new file <code>lib/metrics/telemetry.ex</code> and put this in it:</p>
<pre><code class="lang-elixir">defmodule Metrics.Telemetry do
  use Supervisor
  import Telemetry.Metrics

  def start_link(arg) do
    Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
  end

  def init(_arg) do
    children = [
      {Telemetry.Metrics.ConsoleReporter, metrics: metrics()}
    ]

    Supervisor.init(children, strategy: :one_for_one)
  end

  defp metrics do
    [
      counter("metrics.emit.value")
    ]
  end
end
</code></pre>
<p>Let's analyze it. First of all, it is a simple <code>Supervisor</code> that we need to start from some other process in order to work. We'll attach it to our main application supervisor tree. More on that later.</p>
<p>Then, the <code>init</code> function is configuring the children for this supervisor. We have a single child here, the <code>ConsoleReporter</code>. And we are passing a list of metrics to it as initial arguments.</p>
<p>Here is where it becomes interesting. <code>Telemetry.Metrics.counter</code> is the <strong>definition</strong> of the metric we want to use. We pass a string: <code>"metrics.emit.value"</code>. This string will be split using the period as separator and everything but the last part (metrics.emit), will be the event to attach to. The last part (value) will be taken from the measurement passed to the telemetry event handler.</p>
<p>Let's explain it. We have this call in <code>Metrics.emit/1</code>:</p>
<pre><code class="lang-elixir">    :telemetry.execute([:metrics, :emit], %{value: value})
</code></pre>
<p>and we have a metrics definition in <code>Metrics.Telemetry.metrics/0</code>:</p>
<pre><code class="lang-elixir">      counter("metrics.emit.value")
</code></pre>
<p>As you see, the counter is going to attach a handler for the <code>[:metrics, :emit]</code> event and get the <code>:value</code> attribute from the second argument to <code>telemetry.execute</code> call (the measurement):</p>
<ul>
<li><strong>event</strong>: "metrics.emit" -&gt; [:metrics, :emit]</li>
<li><strong>measurement attribute</strong>: "value" -&gt;  :value</li>
</ul>
<p>If we were collecting the <code>:query_time</code> of <code>[:my_app, :repo, :query]</code> event, we would write <code>"my_app.repo.query.query_time"</code>.</p>
<p>Ok, let's continue.</p>
<p>We are using the <code>ConsoleReporter</code> to collect our metrics. We defined the <code>counter</code> metric. We also said that <code>ConsoleReporter</code> do nothing but output the values received. That's is enough, for now, to check that we are in fact collecting metrics with our <code>ConsoleReporter</code>.</p>
<p>But one thing is missing. We need to attach our <code>Metrics.Telemetry</code> supervisor to the application supervisor, otherwise, nothing will happen. Open <code>application.ex</code> and change the <code>start/2</code> function to this:</p>
<pre><code class="lang-elixir">
  def start(_type, _args) do
    children = [
      Metrics.Telemetry
    ]

    opts = [strategy: :one_for_one, name: Metrics.Supervisor]
    Supervisor.start_link(children, opts)
  end
</code></pre>
<p>As you see we are putting our <code>Metrics.Telemetry</code> as a child of the application Supervisor.</p>
<h2 id="heading-test-it">Test it</h2>
<p>Let's try it with <code>iex</code>. Open a shell terminal, get the dependencies and open an <code>iex</code> session:</p>
<pre><code class="lang-bash">mix deps.get
iex -S mix
</code></pre>
<p>Emit an event:</p>
<pre><code class="lang-elixir">iex(1)&gt; Metrics.emit(4)
[Telemetry.Metrics.ConsoleReporter] Got new event!
Event name: metrics.emit
All measurements: %{value: 4}
All metadata: %{}

Metric measurement: :value (counter)
Tag values: %{}

:ok
iex(2)&gt;
</code></pre>
<p>Yay, it works. Although we didn't attach any handler to the [:metrics, :emit] event manually, the <code>ConsoleReporter</code> handled it for us thanks to <code>telemetry_metrics</code>. As I said, it only echoes what is passed in, but the important thing here is that it works.</p>
<p>If the reporter were a little more advanced it would do something with the passed value.</p>
<h2 id="heading-customreporter">CustomReporter</h2>
<p>Let's create a simple <code>CustomReporter</code> by ourselves to see how this works. One thing to notice is that the reporter needs to have a memory of previous values in order to do its calculations. In our case, we need to remember how many events we have received in order to increment it every time we receive a new event. We could use <code>:ets</code> or a <code>GenServer</code> or <code>Agent</code> depending on how complex our implementation needs to be. For this tutorial, I am going to use an <code>Agent</code> as it is very simple to use to keep some persistent value.</p>
<p>Create a file called <code>lib/metrics/telemetry/reporter_state.ex</code> and write this:</p>
<pre><code class="lang-elixir">defmodule Metrics.Telemetry.ReporterState do
  use Agent

  def start_link(initial_value) do
    Agent.start_link(fn -&gt; initial_value end, name: __MODULE__)
  end

  def value do
    Agent.get(__MODULE__, &amp; &amp;1)
  end

  def increment do
    Agent.update(__MODULE__, &amp;(&amp;1 + 1))
  end
end
</code></pre>
<p>Let's attach it as a child to our main application supervisor so that it is started when the app starts. Edit <code>application.ex</code> and change the <code>start/2</code> function to this:</p>
<pre><code class="lang-elixir">  def start(_type, _args) do
    children = [
      {Metrics.Telemetry.ReporterState, 0},
      Metrics.Telemetry
    ]

    opts = [strategy: :one_for_one, name: Metrics.Supervisor]
    Supervisor.start_link(children, opts)
  end
end
</code></pre>
<p>Now the application will automatically start a counter agent with an initial value of 0 that we can use from every other part of the app by using its name <code>Metrics.Telemetry.CounterAgent</code>. Let's try it:</p>
<pre><code class="lang-bash">iex -S mix
iex(1)&gt; Metrics.Telemetry.ReporterState.value()
0
iex(2)&gt; Metrics.Telemetry.ReporterState.increment()
:ok
iex(3)&gt; Metrics.Telemetry.ReporterState.value()    
1
</code></pre>
<p>Nice, we have our agent working. We can use it to maintain the state of our custom reporter.</p>
<p>Let's write our reporter. Create a file called <code>lib/metrics/telemetry/custom_reporter.ex</code>:</p>
<pre><code class="lang-elixir">defmodule Metrics.Telemetry.CustomReporter do
  use GenServer

  alias Metrics.Telemetry.ReporterState
  alias Telemetry.Metrics

  def start_link(metrics: metrics) do
    GenServer.start_link(__MODULE__, metrics)
  end

  @impl true
  def init(metrics) do
    Process.flag(:trap_exit, true)

    groups = Enum.group_by(metrics, &amp; &amp;1.event_name)

    for {event, metrics} &lt;- groups do
      id = {__MODULE__, event, self()}
      :telemetry.attach(id, event, &amp;__MODULE__.handle_event/4, metrics)
    end

    {:ok, Map.keys(groups)}
  end

  def handle_event(_event_name, measurements, metadata, metrics) do
    metrics
    |&gt; Enum.map(&amp;handle_metric(&amp;1, measurements, metadata))
  end

  defp handle_metric(%Metrics.Counter{} = metric, _measurements, _metadata) do
    ReporterState.increment()

    current_value = ReporterState.value()

    IO.puts("Metric: #{metric.__struct__}. Current value: #{current_value}")
  end

  defp handle_metric(metric, _measurements, _metadata) do
    IO.puts("Unsupported metric: #{metric.__struct__}")
  end

  @impl true
  def terminate(_, events) do
    for event &lt;- events do
      :telemetry.detach({__MODULE__, event, self()})
    end

    :ok
  end
end
</code></pre>
<p>A lot of things happening here, but don't get distracted by those. What you should notice is the <code>handle_event</code> function that distributes the event to all the specific metric handlers to, well, handle the event.</p>
<p>The <code>handle_metric</code> function uses <code>ReporterState</code> to keep the current state and increments the running counter that we use to track how many times we have received this event. It also prints the new current state to the console.</p>
<p>Let's use our <code>CustomReporter</code> instead of the <code>ConsoleReporter</code>. Change the <code>init/1</code> function in <code>telemetry.ex</code> to be like this:</p>
<pre><code class="lang-elixir">  def init(_arg) do
    children = [
      {Metrics.Telemetry.CustomReporter, metrics: metrics()}
    ]

    Supervisor.init(children, strategy: :one_for_one)
  end
</code></pre>
<p>As you see, now we use our <code>CustomReporter</code> and we pass the metrics to it just as we did with the <code>ConsoleReporter</code>. Let's try it. Open the console and start <code>iex</code>:</p>
<pre><code class="lang-console">iex -S mix
iex(1)&gt; Metrics.emit(4)
Metric: Elixir.Telemetry.Metrics.Counter. Current value: 1
:ok
iex(2)&gt; Metrics.emit(5)
Metric: Elixir.Telemetry.Metrics.Counter. Current value: 2
:ok
iex(3)&gt; Metrics.emit(2)
Metric: Elixir.Telemetry.Metrics.Counter. Current value: 3
:ok
</code></pre>
<p>And there it is, now the counter is correctly keeping track of how many times we have received the event.</p>
<p>Let's review a few more things about the <code>CustomReporter</code>. Most of them are better explained in the official docs <a target="_blank" href="https://hexdocs.pm/telemetry_metrics/Telemetry.Metrics.ConsoleReporter.html">here</a>.</p>
<p>It traps the exit signal to detach the handlers when the app terminates or the process is about to finish. It also groups together similar events that require several metrics tracking. For example, suppose you want to collect <code>counter</code> and <code>sum</code> metrics for the event <code>"my_app.repo.query.query_time"</code>. This code gets the event once and then calls <code>handle_metric</code> two times, one with the <code>%Telemetry.Metrics.Counter{}</code> as the first argument and one with <code>%Telemetry.Metrics.Sum{}</code> as the first argument. And for both of those calls, the second argument is the measurement we just received.</p>
<h2 id="heading-add-support-for-more-metrics">Add support for more metrics</h2>
<p>Ok, so far so good. But if you notice we are just handling one metric. Let's add support for the <code>sum</code> metric.</p>
<p>First, to sum all the event measurements, we need to also keep track of the running sum so far. Our <code>ReporterState</code> is only handling a single integer as the state. Let's change it to now store a tuple, with the first element being the count and the second element being the sum.</p>
<p>Change <code>ReporterState</code> to this:</p>
<pre><code class="lang-elixir">defmodule Metrics.Telemetry.ReporterState do
  use Agent

  def start_link(initial_value) do
    Agent.start_link(fn -&gt; initial_value end, name: __MODULE__)
  end

  def value do
    Agent.get(__MODULE__, &amp; &amp;1)
  end

  def increment do
    Agent.update(__MODULE__, fn {count, sum} -&gt; {count + 1, sum} end)
  end

  def sum(value) do
    Agent.update(__MODULE__, fn {count, sum} -&gt; {count, sum + value} end)
  end
end
</code></pre>
<p>Now the agent has a composite state and a function to increment the count part and a function to add to the total part. This is a naive implementation, of course, and doesn't even care about both parts of the state getting out of sync, but for our purposes, it will suffice. We needed a way to store state, we have it. </p>
<p>Let's change our <code>CustomReporter</code>. Remove the old <code>handle_metric</code> functions and put these ones instead:</p>
<pre><code class="lang-elixir">  defp handle_metric(%Metrics.Counter{} = metric, _measurements, _metadata) do
    ReporterState.increment()

    current_value = ReporterState.value()

    IO.puts("Metric: #{metric.__struct__}. Current value: #{inspect(current_value)}")
  end

  defp handle_metric(%Metrics.Sum{} = metric, %{value: value}, _metadata) do
    ReporterState.sum(value)

    current_value = ReporterState.value()

    IO.puts("Metric: #{metric.__struct__}. Current value: #{inspect(current_value)}")
  end

  defp handle_metric(metric, _measurements, _metadata) do
    IO.puts("Unsupported metric: #{metric.__struct__}")
  end
</code></pre>
<p>Again, nothing fancy. Now we handle a second type of metric, the <code>%Metrics.Sum{}</code> and, similarly to the count one, we use the <code>ReporterState</code> to keep track of the event measurements so far.</p>
<p>Let's tell telemetry_metrics to also handle this type of metrics. Update the <code>metrics/0</code> function of <code>telemetry.ex</code> to this:</p>
<pre><code class="lang-elixir">  defp metrics do
    [
      counter("metrics.emit.value"),
      sum("metrics.emit.value")
    ]
  end
</code></pre>
<p>Now the <code>sum</code> metric is also being collected.</p>
<p>Finally, change the <code>start/2</code> function in <code>application.ex</code> to this:</p>
<pre><code class="lang-elixir"> def start(_type, _args) do
    children = [
      {Metrics.Telemetry.ReporterState, {0, 0}},
      Metrics.Telemetry
    ]

    opts = [strategy: :one_for_one, name: Metrics.Supervisor]
    Supervisor.start_link(children, opts)
  end
</code></pre>
<p>We are set. Let's try it. In the shell:</p>
<pre><code class="lang-bash">iex -S mix
iex(1)&gt; Metrics.emit(4)
Metric: Elixir.Telemetry.Metrics.Counter. Current value: {1, 0}
Metric: Elixir.Telemetry.Metrics.Sum. Current value: {1, 4}
:ok
iex(2)&gt; Metrics.emit(3)
Metric: Elixir.Telemetry.Metrics.Counter. Current value: {2, 4}
Metric: Elixir.Telemetry.Metrics.Sum. Current value: {2, 7}
:ok
iex(3)&gt; Metrics.emit(2)
Metric: Elixir.Telemetry.Metrics.Counter. Current value: {3, 7}
Metric: Elixir.Telemetry.Metrics.Sum. Current value: {3, 9}
:ok
iex(4)&gt; Metrics.emit(1)
Metric: Elixir.Telemetry.Metrics.Counter. Current value: {4, 9}
Metric: Elixir.Telemetry.Metrics.Sum. Current value: {4, 10}
:ok
</code></pre>
<p>And that's it. Our <code>CustomReporter</code> is now capable of tracking two different metrics for us. Of course, a production system will have a better way to store and keep track of the series of measurements so that it has more guarantees about the data ingested. But that is out of the scope of this article.</p>
<h2 id="heading-summary">Summary</h2>
<p>We learned that <code>telemetry_metrics</code>:</p>
<ul>
<li>offers a way to define 5 types of metrics in a standard way.</li>
<li>doesn't really care or dictates how the measurements should be stored, aggregated, or manipulated</li>
<li>the reporter responsibility is to ingest and implement the real manipulation of the data</li>
</ul>
<p>There are several open-source implementations of reporters that allow us to make our measurements available to tools like Prometheus or StatsD servers.</p>
<p>In a future article, I'll talk about integrating with them.</p>
<h2 id="heading-source-code">Source code</h2>
<p>You can find the source code for this article in this <a target="_blank" href="https://github.com/miguelcoba/metrics">github repository</a>.</p>
<h2 id="heading-about">About</h2>
<p>I'm <a target="_blank" href="https://miguelcoba.com">Miguel Cobá</a>. I write about Elixir, Elm, Software Development, and eBook writing.</p>
<ul>
<li>Follow me on <a target="_blank" href="https://twitter.com/MiguelCoba_">Twitter</a></li>
<li>Subscribe to my <a target="_blank" href="https://newsletter.miguelcoba.com">newsletter</a></li>
<li>Read all my articles on my <a target="_blank" href="https://blog.miguelcoba.com">blog</a></li>
<li>Get my books:<ul>
<li><a target="_blank" href="https://store.miguelcoba.com/l/100elixirtips">100 Elixir Tips</a> [FREE]</li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/deployingelixir">Deploying Elixir</a> [FREE]</li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/advancedtopics">Deploying Elixir: Advanced Topics</a></li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/ebookwriting">eBook Writing Workflow for Developers</a></li>
</ul>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Introduction to Telemetry in Elixir]]></title><description><![CDATA[Maybe you, like me, have found Telemetry while reading Elixir but have had no time to learn more details about it. I recently put some time aside to learn it and decided to write about it.
In this article I am going to show you what Telemetry is, why...]]></description><link>https://blog.miguelcoba.com/introduction-to-telemetry-in-elixir</link><guid isPermaLink="true">https://blog.miguelcoba.com/introduction-to-telemetry-in-elixir</guid><category><![CDATA[Elixir]]></category><category><![CDATA[data]]></category><category><![CDATA[introduction]]></category><category><![CDATA[Beginner Developers]]></category><dc:creator><![CDATA[Miguel Cobá]]></dc:creator><pubDate>Thu, 03 Feb 2022 11:13:28 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/xVptEZzgVfo/upload/v1643886675865/cg_DCmRp-.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Maybe you, like me, have found Telemetry while reading Elixir but have had no time to learn more details about it. I recently put some time aside to learn it and decided to write about it.</p>
<p>In this article I am going to show you what Telemetry is, why is needed, and how to use it in your own Elixir project.</p>
<h2 id="heading-software-telemetry">Software Telemetry</h2>
<p>First of all, what is telemetry?</p>
<p><a target="_blank" href="https://en.wikipedia.org/wiki/Telemetry">Wikipedia</a> explains it very clearly:</p>
<blockquote>
<p>Telemetry is the [...] collection of measurements [...] and their automatic transmission [...] for monitoring</p>
</blockquote>
<p>We can apply that to software systems, too, by collecting measurements while a software system is running and keeping track of that data.</p>
<p>Why would we do this, you may ask? Glad you ask. There are several reasons, but the main one is to discover what is happening inside our systems and then <em>optimize</em> them based on our findings.</p>
<p>What problem does telemetry solve for us? It gives us visibility into things that are important to us to measure and monitor.</p>
<p>Maybe we want to keep track of CPU utilization to see why our AWS bill is so high, or we need to see how many times a query is executed and how long it took to finish in our PostgreSQL database.</p>
<p>Measuring things and monitoring is not enough. If we don't use the collected data to make decisions and change or optimize the system measured then we might as well not measure anything at all.</p>
<p>By measuring our systems we get several benefits:</p>
<ul>
<li>we can look for parts of the system that are slow and can be improved.</li>
<li>we could discover parts of the system that are not used frequently enough and that maybe can be scaled down to reduce our infrastructure bill.</li>
<li>we could aggregate how many resources a customer is using every month and generate utilization graphs and other insights to make better use of our system. This would allow us to invite the customer to upgrade to a higher plan that includes this kind of data analysis.</li>
</ul>
<p>In summary, software telemetry allows us to measure and act upon the collected data of a running system for our own benefit.</p>
<h2 id="heading-telemetry-in-elixir">Telemetry in Elixir</h2>
<p>There are several ways to take measures from a running Elixir app.</p>
<p>For example, the following fragment would take the duration of a function execution:</p>
<pre><code class="lang-elixir">def some_function do
    start = System.monotonic_time()
    # do something that you want to measure
    stop = System.monotonic_time()
    record_measure("operation took: ", stop - start)
    # ...
end
</code></pre>
<p>In the strict sense, that <em>is</em> telemetry. But is not the best approach. It has several drawbacks:</p>
<ul>
<li>It is repetitive</li>
<li>You have to reimplement it in every app you want to use it</li>
<li>You have no way to disable measuring, for example, during unit tests</li>
<li>more things that I can't think right now</li>
</ul>
<h2 id="heading-elixir-telemetry-library">Elixir Telemetry library</h2>
<p>Given that telemetry is something that many people want, a <a target="_blank" href="https://hexdocs.pm/telemetry/readme.html">library</a> was created to address this need. It is called <code>telemetry</code>.</p>
<p>Advantages:</p>
<ul>
<li>provides a standard interface to measure and emit telemetry data</li>
<li>it is small and simple to use</li>
</ul>
<p>How does it work? It is very simple:</p>
<ul>
<li>you execute a measurement</li>
<li>you create a function that will be invoked when the measure is taken</li>
<li>you attach the function to the event measured</li>
</ul>
<h2 id="heading-using-telemetry">Using telemetry</h2>
<p>Let's try a simple example to see how easy is to start collecting telemetry data.</p>
<p>I am going to create a very simple Elixir for a fictional grocery store.
We want to measure each sale.</p>
<p>First, create a simple Elixir project:</p>
<pre><code class="lang-bash">mix new telemetry_intro
<span class="hljs-built_in">cd</span> telemetry_intro
</code></pre>
<p>Add telemetry to the list of dependencies in <code>mix.exs</code>:</p>
<pre><code class="lang-elixir">defp deps() do
  [
    {:telemetry, "~&gt; 1.0"}
  ]
end
</code></pre>
<p>Download the dependencies:</p>
<pre><code class="lang-bash">mix deps.get
</code></pre>
<p>Now our business logic module. Create a file named <code>lib/grocery/store.ex</code> and put this inside:</p>
<pre><code class="lang-elixir">defmodule TelemetryIntro.Grocery.Store do
  def sale(product, quantity, amount) do
    total = quantity * amount

    "Sold #{product}: #{quantity} units at #{amount} each. Total #{total}"
  end
end
</code></pre>
<p>As you see it is very simple. It just receives some parameters, does something, and then returns a value. Doesn't have any measure code yet.</p>
<p>Let's just try it with iex. In the shell, write:</p>
<pre><code class="lang-bash">iex -S mix
iex(1)&gt; TelemetryIntro.Grocery.Store.sale(<span class="hljs-string">"apple"</span>, 4, 0.5)
<span class="hljs-string">"Sold apple: 4 units at 0.5 each. Total 2.0"</span>
</code></pre>
<p>Nothing unexpected here. Let's add some measurements. Say we want to keep track of the total and also to track what product was sold.</p>
<p>Change the sale function to be like this:</p>
<pre><code class="lang-elixir">defmodule TelemetryIntro.Grocery.Store do
  def sale(product, quantity, amount) do
    total = quantity * amount

    :telemetry.execute(
      [:grocery, :store, :sale],
      %{total: total},
      %{product: product}
    )

    "Sold #{product}: #{quantity} units at #{amount} each. Total #{total}"
  end
end
</code></pre>
<p>Good. Now we are doing something. We are using the <code>execute</code> function of the <code>telemetry</code> module and we are passing three things:</p>
<ul>
<li>the <em>event name</em> as a list of keywords</li>
<li>the <em>measurement</em> we are interested in (sale total in our example)</li>
<li>additional <em>metadata</em> we might need (here I am just putting the product name)</li>
</ul>
<p>The metadata could be anything we want: our users' IP address, some tracking header from a web request, the user id, etc.</p>
<p>The measurement is what we are interested in. This is what we'll collect and analyze and then use to decide things. It can be anything, but it must be a measurement of something.</p>
<p>Finally is the event name. You can put anything here. Most people put a hierarchy of words to easily identify the events. What is important to keep in mind is that this event name will be used later to match which function will handle this event.</p>
<p>Let's see how the counterpart, the event handler, looks.</p>
<p>Create a file <code>lib/metrics/instrumenter.ex</code></p>
<pre><code class="lang-elixir">defmodule TelemetryIntro.Metrics.Instrumenter do
  require Logger

  def handle_event([:grocery, :store, :sale], measurements, metadata, _config) do
    Logger.info("[Sale telemetry: #{measurements.total}] total for #{metadata.product}")
  end
end
</code></pre>
<p>It is very simple. It has a function (the name doesn't matter but the convention is to name it <code>handle_event</code>) that has as the first parameter the keyword list we are going to match against. Then the measurements and metadata. The last parameter doesn't matter right now. Just ignore it.</p>
<p>This function is going to be called <em>every</em> time the <code>execute</code> function is called and the event name matches. That means that this function must avoid blocking execution for much time as it will be evaluated synchronously.</p>
<p>Finally, we do a lame thing and just pass the received info to <code>Logger.info</code>. Ideally, we'll send that telemetry data to some repository for further processing and analysis but that is outside of the scope of this article (I'll cover it in a future one, tho).</p>
<p>What you need to notice here is that we decoupled the emitting of the telemetry data with the handling of the data collected. Our app can use <code>:telemetry.execute</code> everywhere with the same standard interface and, as long as we have a corresponding function to handle that event, we'll be collecting that data.</p>
<p>Even more important, if we don't have an event handler for that event, our emitting code will <em>still</em> work. It won't even notice there is nothing collecting data.</p>
<p>We are only missing one thing. We need to specify which function will handle a given event. We use the <code>:telemetry.attach</code> function to do that.</p>
<p>Create a function called <code>setup</code> in the <code>Instrumenter</code> module:</p>
<pre><code class="lang-elixir">defmodule TelemetryIntro.Metrics.Instrumenter do
  require Logger

  def setup do
    events = [
      [:grocery, :store, :sale]
    ]

    :telemetry.attach_many("telemetry-intro-instrumenter", events, &amp;__MODULE__.handle_event/4, nil)
  end

  def handle_event([:grocery, :store, :sale], measurements, metadata, _config) do
    Logger.info("[Sale telemetry: #{measurements.total}] total for #{metadata.product}")
  end
end
</code></pre>
<p>As you can see, it is passing to <code>:telemetry.attach_many</code> a list of event names to monitor and the function that will handle all those event names.</p>
<p>By using pattern matching, the same <code>handle_event</code> function can handle all the necessary events in your app.</p>
<p>Now we need to call this <code>setup</code> function when the app starts.</p>
<p>Let's change the way our elixir app starts by modifying the <code>mix.exs</code> file and changing the <code>application</code> function inside it so that it is like this:</p>
<pre><code class="lang-elixir">  def application do
    [
      extra_applications: [:logger],
      mod: {TelemetryIntro, []}
    ]
  end
</code></pre>
<p>We added the <code>mod:</code> to specify a module that will be called when the application starts to execute some custom code. Our custom code will be the telemetry configuration.</p>
<p>Now replace the contents of the <code>lib/telemetry_intro.ex</code> module with this:</p>
<pre><code class="lang-elixir">defmodule TelemetryIntro do
  use Application

  def start(_type, _args) do
    TelemetryIntro.Metrics.Instrumenter.setup()

    children = []
    Supervisor.start_link(children, strategy: :one_for_one)
  end
end
</code></pre>
<p>This module implements the <code>start</code> callback of the <code>Application</code> behavior. This function's responsibility is to spawn and link a supervisor and return {:ok, pid} or {:ok, pid, state}. Here we just start a supervisor with no children. That will suffice for this example.</p>
<p>Let's try it. Start again the application with <code>iex</code>:</p>
<pre><code class="lang-bash">iex -S mix
iex(1)&gt; TelemetryIntro.Grocery.Store.sale(<span class="hljs-string">"apple"</span>, 4, 0.2)

00:18:01.867 [info]  [Sale telemetry: 0.8] total <span class="hljs-keyword">for</span> apple
<span class="hljs-string">"Sold apple: 4 units at 0.2 each. Total 0.8"</span>
</code></pre>
<p>Great, it works. The first output line is the result of <code>handle_event</code> call. The second one is the return value of the <code>sale</code> function.</p>
<p>Let's see what happens if we don't configure a handler for the event.</p>
<p>Comment the <code>TelemetryIntro.Metrics.Instrumenter.setup()</code> in <code>telemetry_intro.ex</code></p>
<pre><code class="lang-elixir">defmodule TelemetryIntro do
  use Application

  def start(_type, _args) do
    # TelemetryIntro.Metrics.Instrumenter.setup()

    children = []
    Supervisor.start_link(children, strategy: :one_for_one)
  end
end
</code></pre>
<p>And try again with <code>iex</code>:</p>
<pre><code class="lang-elixir">iex -S mix
iex(1)&gt; TelemetryIntro.Grocery.Store.sale("apple", 4, 0.2)
"Sold apple: 4 units at 0.2 each. Total 0.8"
</code></pre>
<p>As you can see, the code still works, even if there is no handler configured for that event. Our app still calls the <code>:telemetry.execute</code> function but nothing breaks when nothing is configured to handle that event. How cool is that?</p>
<p>We learned how to emit events and handle them. You can send this info to a third-party error monitoring service or ingest it to a big data store. From here, the sky is the limit.</p>
<h2 id="heading-summary">Summary</h2>
<p>In this article we learned:</p>
<ul>
<li>what is telemetry</li>
<li>how to add it to an Elixir project</li>
<li>how to measure and collect data</li>
<li>how telemetry works</li>
</ul>
<p>As we have seen, telemetry is a powerful tool to inspect the behavior of a running system.</p>
<p>As long as we use the collected data for making decisions to improve our system or its behavior, data collection through telemetry is an investment that every system must seriously consider making.</p>
<p>If you want to learn more about telemetry, you can read the full documentation <a target="_blank" href="https://hexdocs.pm/telemetry/readme.html">here</a></p>
<h2 id="heading-about">About</h2>
<p>I'm <a target="_blank" href="https://miguelcoba.com">Miguel Cobá</a>. I write about Elixir, Elm, Software Development, and eBook writing.</p>
<ul>
<li>Follow me on <a target="_blank" href="https://twitter.com/MiguelCoba_">Twitter</a></li>
<li>Subscribe to my <a target="_blank" href="https://newsletter.miguelcoba.com">newsletter</a></li>
<li>Read all my articles on my <a target="_blank" href="https://blog.miguelcoba.com">blog</a></li>
<li>Get my books:<ul>
<li><a target="_blank" href="https://store.miguelcoba.com/l/100elixirtips">100 Elixir Tips</a> [FREE]</li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/deployingelixir">Deploying Elixir</a> [FREE]</li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/advancedtopics">Deploying Elixir: Advanced Topics</a></li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/ebookwriting">eBook Writing Workflow for Developers</a></li>
</ul>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA["100 Elixir Tips" eBook]]></title><description><![CDATA[I wrote a book with 100 short Elixir samples

100 knowledge pills for you
100 curated examples created for you
100 ways to master Elixir
100 opportunities to increase your knowledge

IT IS FREE TO DOWNLOAD!!!

Download it now
About
I'm Miguel Cobá. I...]]></description><link>https://blog.miguelcoba.com/100-elixir-tips-ebook</link><guid isPermaLink="true">https://blog.miguelcoba.com/100-elixir-tips-ebook</guid><category><![CDATA[Elixir]]></category><category><![CDATA[Programming Tips]]></category><category><![CDATA[Beginner Developers]]></category><category><![CDATA[software development]]></category><category><![CDATA[Functional Programming]]></category><dc:creator><![CDATA[Miguel Cobá]]></dc:creator><pubDate>Wed, 05 Jan 2022 14:14:13 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1642118203614/kVedJ7DZH.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I wrote a book with 100 short Elixir samples</p>
<ul>
<li>100 knowledge pills for you</li>
<li>100 curated examples created for you</li>
<li>100 ways to master Elixir</li>
<li>100 opportunities to increase your knowledge</li>
</ul>
<p><strong>IT IS FREE TO DOWNLOAD!!!</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1642118099156/VbBpZEy4_.png" alt="100 Elixir Tips" /></p>
<h2 id="heading-download-it-nowhttpsstoremiguelcobacoml100elixirtips"><a target="_blank" href="https://store.miguelcoba.com/l/100elixirtips">Download it now</a></h2>
<h2 id="heading-about">About</h2>
<p>I'm <a target="_blank" href="https://miguelcoba.com">Miguel Cobá</a>. I write about Elixir, Elm, Software Development, and eBook writing.</p>
<ul>
<li>Follow me on <a target="_blank" href="https://twitter.com/MiguelCoba_">Twitter</a></li>
<li>Subscribe to my <a target="_blank" href="https://newsletter.miguelcoba.com">newsletter</a></li>
<li>Read all my articles on my <a target="_blank" href="https://blog.miguelcoba.com">blog</a></li>
<li>Get my books:<ul>
<li><a target="_blank" href="https://store.miguelcoba.com/l/100elixirtips">100 Elixir Tips</a> [FREE]</li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/deployingelixir">Deploying Elixir</a> [FREE]</li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/advancedtopics">Deploying Elixir: Advanced Topics</a></li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/ebookwriting">eBook Writing Workflow for Developers</a></li>
</ul>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[New Elixir Book: "100 Elixir Tips"]]></title><description><![CDATA[For the last three months, I have been publishing a daily Elixir tip on my Twitter account.
As the Twitter search interface isn't good I decided to compile them in an ebook so that you can download it and have it as a reference that is easy to search...]]></description><link>https://blog.miguelcoba.com/new-elixir-book-100-elixir-tips</link><guid isPermaLink="true">https://blog.miguelcoba.com/new-elixir-book-100-elixir-tips</guid><category><![CDATA[Elixir]]></category><category><![CDATA[books]]></category><category><![CDATA[Programming Tips]]></category><dc:creator><![CDATA[Miguel Cobá]]></dc:creator><pubDate>Mon, 27 Dec 2021 17:58:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/nGrfKmtwv24/upload/v1640627840774/sCaVs04n_.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>For the last three months, I have been publishing a daily Elixir tip on my Twitter account.</p>
<p>As the Twitter search interface isn't good I decided to compile them in an ebook so that you can download it and have it as a reference that is easy to search.</p>
<p>It contains:</p>
<ul>
<li>75 tips already published in my Twitter account</li>
<li>25 <strong>new, unpublished</strong> tips</li>
</ul>
<p>All of them were carefully curated, edited, and selected by me to show the expressive power and elegance of the Elixir programming language.</p>
<p>The best part: it is <strong>FREE</strong>!!!</p>
<p>You can get it in pre-order now and you'll receive it in your email inbox on January 5th, 2022.</p>
<h2 id="heading-pre-order-it-now-here">Pre-order it now here 👇🏼</h2>
<p><a target="_blank" href="https://store.miguelcoba.com/l/100elixirtips">100 Elixir Tips</a></p>
<h2 id="heading-about">About</h2>
<p>I'm <a target="_blank" href="https://miguelcoba.com">Miguel Cobá</a>. I write about Elixir, Elm, Software Development, and eBook writing.</p>
<ul>
<li>Follow me on <a target="_blank" href="https://twitter.com/MiguelCoba_">Twitter</a></li>
<li>Subscribe to my <a target="_blank" href="https://newsletter.miguelcoba.com">newsletter</a></li>
<li>Read all my articles on my <a target="_blank" href="https://blog.miguelcoba.com">blog</a></li>
<li>Get my books:<ul>
<li><a target="_blank" href="https://store.miguelcoba.com/l/100elixirtips">100 Elixir Tips</a> [FREE]</li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/deployingelixir">Deploying Elixir</a> [FREE]</li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/advancedtopics">Deploying Elixir: Advanced Topics</a></li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/ebookwriting">eBook Writing Workflow for Developers</a></li>
</ul>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Elixir Mix podcast]]></title><description><![CDATA[A couple of weeks ago I had the honor to be a guest in the "Elixir Mix" podcast. Sascha Wolf and Allen Wyma did an amazing job in driving the conversation and not letting me ramble endlessly too much.
We talked about deployment in the Elixir world.
I...]]></description><link>https://blog.miguelcoba.com/elixir-mix-podcast</link><guid isPermaLink="true">https://blog.miguelcoba.com/elixir-mix-podcast</guid><category><![CDATA[Elixir]]></category><category><![CDATA[deployment]]></category><category><![CDATA[podcast]]></category><dc:creator><![CDATA[Miguel Cobá]]></dc:creator><pubDate>Mon, 20 Dec 2021 18:11:03 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/LgFFFER09PE/upload/v1640023604430/jO3oj2Fp9.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A couple of weeks ago I had the honor to be a guest in the "Elixir Mix" podcast. Sascha Wolf and Allen Wyma did an amazing job in driving the conversation and not letting me ramble endlessly too much.</p>
<p>We talked about deployment in the Elixir world.</p>
<p>I was super nervous and really suffering an episode of imposter syndrome, but I think in the end it was not so bad.</p>
<p>You can listen to the discussion here</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://elixirmix.com/deploying-elixir-with-miguel-cob-emx-155">https://elixirmix.com/deploying-elixir-with-miguel-cob-emx-155</a></div>
<h2 id="heading-about">About</h2>
<p>I'm <a target="_blank" href="https://miguelcoba.com">Miguel Cobá</a>. I write about Elixir, Elm, Software Development, and eBook writing.</p>
<ul>
<li>Follow me on <a target="_blank" href="https://twitter.com/MiguelCoba_">Twitter</a></li>
<li>Subscribe to my <a target="_blank" href="https://newsletter.miguelcoba.com">newsletter</a></li>
<li>Read all my articles on my <a target="_blank" href="https://blog.miguelcoba.com">blog</a></li>
<li>Get my books:<ul>
<li><a target="_blank" href="https://store.miguelcoba.com/l/100elixirtips">100 Elixir Tips</a> [FREE]</li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/deployingelixir">Deploying Elixir</a> [FREE]</li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/advancedtopics">Deploying Elixir: Advanced Topics</a></li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/ebookwriting">eBook Writing Workflow for Developers</a></li>
</ul>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA["Deploying Elixir" eBook]]></title><description><![CDATA[I have compiled all the articles in the "Deploying Elixir" series in a handy eBook:

All together in a single place
Nicely formatted
Searchable
With improved wording
With better source code highlight

But I have also added a NEW, NEVER PUBLISHED BEFO...]]></description><link>https://blog.miguelcoba.com/deploying-elixir-ebook</link><guid isPermaLink="true">https://blog.miguelcoba.com/deploying-elixir-ebook</guid><category><![CDATA[Elixir]]></category><category><![CDATA[books]]></category><category><![CDATA[guide]]></category><category><![CDATA[Beginner Developers]]></category><category><![CDATA[Phoenix framework]]></category><dc:creator><![CDATA[Miguel Cobá]]></dc:creator><pubDate>Mon, 22 Nov 2021 14:02:05 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1637593235481/j1oUVeVA6.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I have compiled all the articles in the "Deploying Elixir" series in a handy eBook:</p>
<ul>
<li>All together in a single place</li>
<li>Nicely formatted</li>
<li>Searchable</li>
<li>With improved wording</li>
<li>With better source code highlight</li>
</ul>
<p>But I have also added a <em>NEW, NEVER PUBLISHED BEFORE, BONUS</em> chapter about deploying to heroku.</p>
<p>And the best part:</p>
<p><strong>IT IS FREE TO DOWNLOAD!!!</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1637577913039/xSbwDSSYL.png" alt="Deploying Elixir book cover" /></p>
<h1 id="heading-download-it-nowhttpsstoremiguelcobacomldeployingelixir"><a target="_blank" href="https://store.miguelcoba.com/l/deployingelixir">Download it now</a></h1>
<h2 id="heading-about">About</h2>
<p>I'm <a target="_blank" href="https://miguelcoba.com">Miguel Cobá</a>. I write about Elixir, Elm, Software Development, and eBook writing.</p>
<ul>
<li>Follow me on <a target="_blank" href="https://twitter.com/MiguelCoba_">Twitter</a></li>
<li>Subscribe to my <a target="_blank" href="https://newsletter.miguelcoba.com">newsletter</a></li>
<li>Read all my articles on my <a target="_blank" href="https://blog.miguelcoba.com">blog</a></li>
<li>Get my books:<ul>
<li><a target="_blank" href="https://store.miguelcoba.com/l/100elixirtips">100 Elixir Tips</a> [FREE]</li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/deployingelixir">Deploying Elixir</a> [FREE]</li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/advancedtopics">Deploying Elixir: Advanced Topics</a></li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/ebookwriting">eBook Writing Workflow for Developers</a></li>
</ul>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Deploying an Elixir Release to Gigalixir]]></title><description><![CDATA[I'll show you how to deploy a Phoenix 1.6 application, with Elixir 1.12 Release to gigalixir.com 
I'll use the  Elixir Release we did before.
Configure the app for Gigalixir
Edit the runtime.exs file and add the url: line to the Endpoint section:
  c...]]></description><link>https://blog.miguelcoba.com/deploying-an-elixir-release-to-gigalixir</link><guid isPermaLink="true">https://blog.miguelcoba.com/deploying-an-elixir-release-to-gigalixir</guid><category><![CDATA[Elixir]]></category><category><![CDATA[deployment]]></category><category><![CDATA[Beginner Developers]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Phoenix framework]]></category><dc:creator><![CDATA[Miguel Cobá]]></dc:creator><pubDate>Mon, 08 Nov 2021 08:18:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1636289342426/urFi-6fBS.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I'll show you how to deploy a Phoenix 1.6 application, with Elixir 1.12 Release to gigalixir.com </p>
<p>I'll use the  <a target="_blank" href="https://blog.miguelcoba.com/preparing-a-phoenix-16-app-for-deployment-with-elixir-releases">Elixir Release</a> we did before.</p>
<h2 id="heading-configure-the-app-for-gigalixir">Configure the app for Gigalixir</h2>
<p>Edit the <code>runtime.exs</code> file and add the <code>url:</code> line to the <code>Endpoint</code> section:</p>
<pre><code class="lang-elixir">  config :saturn, SaturnWeb.Endpoint,
    url: [host: System.get_env("APP_NAME") &lt;&gt; ".gigalixirapp.com", port: 443],
    http: [
      # Enable IPv6 and bind on all interfaces.
      # Set it to  {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
      # See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html
      # for details about using IPv6 vs IPv4 and loopback vs public addresses.
      ip: {0, 0, 0, 0, 0, 0, 0, 0},
      port: String.to_integer(System.get_env("PORT") || "4000")
    ],
    secret_key_base: secret_key_base
</code></pre>
<p>Add the config for the buildpacks that Gigalixir uses to compile and deploy the application. </p>
<p>Create a file named <code>elixir_buildpack.config</code> on the root of the project and put this:</p>
<pre><code><span class="hljs-attribute">erlang_version</span>=<span class="hljs-number">24</span>.<span class="hljs-number">1</span>.<span class="hljs-number">2</span>
<span class="hljs-attribute">elixir_version</span>=<span class="hljs-number">1</span>.<span class="hljs-number">12</span>.<span class="hljs-number">3</span>
</code></pre><p>Create a <code>phoenix_static_buildpack.config</code> file in the root and put this:</p>
<pre><code><span class="hljs-attribute">node_version</span>=<span class="hljs-number">14</span>.<span class="hljs-number">15</span>.<span class="hljs-number">4</span>
</code></pre><p>Create a file in <code>assets/package.json</code> and put this:</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"scripts"</span>: {
        <span class="hljs-attr">"deploy"</span>: <span class="hljs-string">"cd .. &amp;&amp; mix assets.deploy &amp;&amp; rm -f _build/esbuild"</span>
    }
}
</code></pre>
<p>This one is to use npm to use mix to use esbuild to compile our assets and after deploying them, delete the intermediate files!</p>
<p>Create a <code>.buildpacks</code> file and put this:</p>
<pre><code><span class="hljs-attribute">https</span>:<span class="hljs-comment">//github.com/HashNuke/heroku-buildpack-elixir</span>
<span class="hljs-attribute">https</span>:<span class="hljs-comment">//github.com/gjaldon/heroku-buildpack-phoenix-static</span>
<span class="hljs-attribute">https</span>:<span class="hljs-comment">//github.com/gigalixir/gigalixir-buildpack-releases.git</span>
</code></pre><p>Ok, enough changes. I am creating a branch named gigalixir-deployment and committing all these changes to it:</p>
<pre><code class="lang-bash">git checkout -b gigalixir-deployment
git add .
git commit -m <span class="hljs-string">"Deploying to Gigalixir"</span>
git push -u origin gigalixir-deployment
</code></pre>
<h2 id="heading-gigalixir-setup">Gigalixir setup</h2>
<h3 id="heading-install-the-gigalixir-cli">Install the gigalixir CLI</h3>
<pre><code class="lang-bash">brew tap gigalixir/brew
brew install gigalixir
</code></pre>
<p>Crete an account in Gigalixir and login</p>
<pre><code class="lang-bash">gigalixir signup
gigalixir login
</code></pre>
<h3 id="heading-create-a-gigalixir-application">Create a Gigalixir application:</h3>
<pre><code class="lang-bash"><span class="hljs-built_in">export</span> APP_NAME=$(gigalixir create)

Created app: harsh-some-cats.
Set git remote: gigalixir.
</code></pre>
<p>Verify your app is created:</p>
<pre><code>gigalixir apps

[<span class="hljs-meta">
  {
    <span class="hljs-meta-string">"cloud"</span>: <span class="hljs-meta-string">"gcp"</span>,
    <span class="hljs-meta-string">"region"</span>: <span class="hljs-meta-string">"v2018-us-central1"</span>,
    <span class="hljs-meta-string">"replicas"</span>: 0,
    <span class="hljs-meta-string">"size"</span>: 0.3,
    <span class="hljs-meta-string">"stack"</span>: <span class="hljs-meta-string">"gigalixir-20"</span>,
    <span class="hljs-meta-string">"unique_name"</span>: <span class="hljs-meta-string">"harsh-some-cats"</span>,
    <span class="hljs-meta-string">"version"</span>: 2
  }
</span>]
</code></pre><p>This command also creates a new remote in your git repo, pointing to gigalixir. Check it:</p>
<pre><code>git remote -v

gigalixir    https://git.gigalixir.com/harsh-<span class="hljs-keyword">some</span>-cats.git/ (<span class="hljs-keyword">fetch</span>)
gigalixir    https://git.gigalixir.com/harsh-<span class="hljs-keyword">some</span>-cats.git/ (push)
</code></pre><h3 id="heading-provision-a-database">Provision a database</h3>
<p>Let's create a database for our app. This is the free plan so is not suitable for production but it will be good for testing.</p>
<pre><code class="lang-bash">gigalixir pg:create --free
</code></pre>
<p>You can check if the database is running with this command:</p>
<pre><code class="lang-bash">gigalixir pg
</code></pre>
<p>Finally, let's see the environment variables that Gigalixir created for us:</p>
<pre><code class="lang-bash">gigalixir config
</code></pre>
<p>You should see something like this:</p>
<pre><code class="lang-bash">{
  <span class="hljs-string">"DATABASE_URL"</span>: <span class="hljs-string">"ecto://&lt;some user&gt;:&lt;some password&gt;@postgres-free-tier-v2020.gigalixir.com:5432/&lt;some database name&gt;"</span>,
  <span class="hljs-string">"POOL_SIZE"</span>: <span class="hljs-string">"2"</span>
}
</code></pre>
<p>As you see <code>DATABASE_URL</code> and <code>POOL_SIZE</code> are automatically created. Our app also needs the <code>SECRET_KEY_BASE</code>, <code>PORT</code> and <code>APP_NAME</code> but those will be automatically provided when the app starts, we don't need to worry about them.</p>
<h3 id="heading-deploy-the-application">Deploy the application</h3>
<p>To deploy to Gigalixir, we must push our master branch to the <code>gigalixir</code> remote. Gigalixir only builds the <code>master</code> branch and ignore others. We use <code>gigalixir-deployment</code> as our main branch, so we need to push our branch changes to the master branch on the gigalixir remote:</p>
<pre><code class="lang-bash">git push gigalixir gigalixir-deployment:master
</code></pre>
<p>This will start the building of the release on the gigalixir build servers.</p>
<p>You can see the status with <code>gigalixir ps</code>:</p>
<pre><code class="lang-bash">{
  <span class="hljs-string">"cloud"</span>: <span class="hljs-string">"gcp"</span>,
  <span class="hljs-string">"pods"</span>: [
    {
      <span class="hljs-string">"lastState"</span>: {},
      <span class="hljs-string">"name"</span>: <span class="hljs-string">"harsh-some-cats-77d577b7cd-2s9c2"</span>,
      <span class="hljs-string">"sha"</span>: <span class="hljs-string">"5dd02b3af6fc7958615b9ee2cbcd6af934845506"</span>,
      <span class="hljs-string">"status"</span>: <span class="hljs-string">"Healthy"</span>,
      <span class="hljs-string">"version"</span>: <span class="hljs-string">"4"</span>
    }
  ],
  <span class="hljs-string">"region"</span>: <span class="hljs-string">"v2018-us-central1"</span>,
  <span class="hljs-string">"replicas_desired"</span>: 1,
  <span class="hljs-string">"replicas_running"</span>: 1,
  <span class="hljs-string">"size"</span>: 0.3,
  <span class="hljs-string">"stack"</span>: <span class="hljs-string">"gigalixir-20"</span>,
  <span class="hljs-string">"unique_name"</span>: <span class="hljs-string">"harsh-some-cats"</span>
}
</code></pre>
<p>When the build is finished, the app is deployed and started and you'll see log lines like these:</p>
<pre><code class="lang-bash">remote: Creating release.
remote: Starting zero-downtime rolling deploy.
remote: Please <span class="hljs-built_in">wait</span> a minute <span class="hljs-keyword">for</span> the new instance(s) to roll out and pass health checks.
remote: For troubleshooting, See:      http://gigalixir.readthedocs.io/en/latest/main.html<span class="hljs-comment">#troubleshooting</span>
remote: For <span class="hljs-built_in">help</span>, contact:             <span class="hljs-built_in">help</span>@gigalixir.com
remote: Try hitting your app with:     curl https://harsh-some-cats.gigalixirapp.com/
remote: Check your app logs with:      gigalixir logs -a harsh-some-cats
remote: Check deploy status with:      gigalixir ps -a harsh-some-cats
remote: Updated property [core/account].
To https://git.gigalixir.com/harsh-some-cats.git/
 * [new branch]      gigalixir-deployment -&gt; master
</code></pre>
<p>You can open your app in your browser by running:</p>
<pre><code class="lang-bash">gigalixir open
</code></pre>
<p>And you'll see the familiar home screen:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635893264062/oswsJuio3.png" alt="Elixir release running on Gigalixir" /></p>
<h2 id="heading-opening-a-remote-console-and-running-migrations">Opening a remote console and running migrations</h2>
<p>First we need to add our ssh public key so that we can connect securely:</p>
<pre><code class="lang-bash">gigalixir account:ssh_keys:add <span class="hljs-string">"<span class="hljs-subst">$(cat ~/.ssh/id_rsa.pub)</span>"</span>
</code></pre>
<h3 id="heading-opening-a-remote-console">Opening a remote console</h3>
<pre><code class="lang-bash">gigalixir ps:remote_console

Erlang/OTP 24 [erts-12.1.2] [<span class="hljs-built_in">source</span>] [64-bit] [smp:8:1] [ds:8:1:10] [async-threads:1] [jit]

Interactive Elixir (1.12.3) - press Ctrl+C to <span class="hljs-built_in">exit</span> (<span class="hljs-built_in">type</span> h() ENTER <span class="hljs-keyword">for</span> <span class="hljs-built_in">help</span>)
iex(harsh-some-cats@10.56.18.206)1&gt; ls
.bashrc           .profile          .profile.d        bin               erts-12.1.2
lib               releases          saturn.tar.gz
</code></pre>
<h3 id="heading-running-migrations">Running migrations</h3>
<p>First, let's watch the logs:</p>
<pre><code class="lang-bash">gigalixir logs
</code></pre>
<p>Then, in another terminal, run:</p>
<pre><code class="lang-bash">gigalixir ps:migrate

Connection to v2018-us-central1.gcp.ssh.gigalixir.com closed.
</code></pre>
<p>You should see something like this:</p>
<pre><code class="lang-bash">2021-11-02T22:45:07.937626+00:00 harsh-some-cats[b<span class="hljs-string">'harsh-some-cats-77d577b7cd-2s9c2'</span>]: web.1  | 22:45:07.937 [info] Running SaturnWeb.Endpoint with cowboy 2.9.0 at :::4000 (http)
2021-11-02T22:45:07.938503+00:00 harsh-some-cats[b<span class="hljs-string">'harsh-some-cats-77d577b7cd-2s9c2'</span>]: web.1  | 22:45:07.938 [info] Access SaturnWeb.Endpoint at http://harsh-some-cats.gigalixirapp.com:443
2021-11-02T22:47:27.795085+00:00 harsh-some-cats[b<span class="hljs-string">'harsh-some-cats-77d577b7cd-2s9c2'</span>]: web.1  | 22:47:27.794 request_id=6ae142c1367400361510a08fb8000235 [info] GET /
2021-11-02T22:47:27.799510+00:00 harsh-some-cats[b<span class="hljs-string">'harsh-some-cats-77d577b7cd-2s9c2'</span>]: web.1  | 22:47:27.798 request_id=6ae142c1367400361510a08fb8000235 [info] Sent 200 <span class="hljs-keyword">in</span> 4ms
2021-11-02T22:48:38.509918+00:00 harsh-some-cats[b<span class="hljs-string">'harsh-some-cats-77d577b7cd-2s9c2'</span>]: web.1  | 22:48:38.508 request_id=bbc52891b40f8bb8ec1c6e96b766c8fd [info] GET /
2021-11-02T22:48:38.509940+00:00 harsh-some-cats[b<span class="hljs-string">'harsh-some-cats-77d577b7cd-2s9c2'</span>]: web.1  | 22:48:38.509 request_id=bbc52891b40f8bb8ec1c6e96b766c8fd [info] Sent 200 <span class="hljs-keyword">in</span> 1ms
2021-11-02T22:53:33.926744+00:00 harsh-some-cats[b<span class="hljs-string">'harsh-some-cats-77d577b7cd-2s9c2'</span>]: web.1  | 22:53:33.926 [info] Migrations already up
</code></pre>
<p>That's it.</p>
<h2 id="heading-source-code">Source code</h2>
<p>The  <a target="_blank" href="https://github.com/miguelcoba/saturn/tree/gigalixir-deployment">source code</a>  for the saturn project is open source under the MIT license. Use the <code>gigalixir-deployment</code> branch.</p>
<h2 id="heading-about">About</h2>
<p>I'm <a target="_blank" href="https://miguelcoba.com">Miguel Cobá</a>. I write about Elixir, Elm, Software Development, and eBook writing.</p>
<ul>
<li>Follow me on <a target="_blank" href="https://twitter.com/MiguelCoba_">Twitter</a></li>
<li>Subscribe to my <a target="_blank" href="https://newsletter.miguelcoba.com">newsletter</a></li>
<li>Read all my articles on my <a target="_blank" href="https://blog.miguelcoba.com">blog</a></li>
<li>Get my books:<ul>
<li><a target="_blank" href="https://store.miguelcoba.com/l/100elixirtips">100 Elixir Tips</a> [FREE]</li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/deployingelixir">Deploying Elixir</a> [FREE]</li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/advancedtopics">Deploying Elixir: Advanced Topics</a></li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/ebookwriting">eBook Writing Workflow for Developers</a></li>
</ul>
</li>
</ul>
<p>Photo by <a target="_blank" href="a href=&quot;https://unsplash.com/@wocintechchat?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText&quot;">Christina @ wocintechchat.com</a> on <a target="_blank" href="https://unsplash.com/s/photos/server?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a> </p>
]]></content:encoded></item><item><title><![CDATA[Deploying an Elixir Release using Docker on Render.com]]></title><description><![CDATA[I'm going to show you how to deploy our Elixir Release to render.com. We'll use our  Docker image.
Prepare Elixir Release for deploying to render.com
We only need to do a single change to our runtime.exs file. Add the url: line to the 
  config :satu...]]></description><link>https://blog.miguelcoba.com/deploying-an-elixir-release-using-docker-on-rendercom</link><guid isPermaLink="true">https://blog.miguelcoba.com/deploying-an-elixir-release-using-docker-on-rendercom</guid><category><![CDATA[Elixir]]></category><category><![CDATA[Docker]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Devops]]></category><category><![CDATA[Beginner Developers]]></category><dc:creator><![CDATA[Miguel Cobá]]></dc:creator><pubDate>Thu, 04 Nov 2021 11:40:13 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1636025734290/GruzETfSH.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I'm going to show you how to deploy our Elixir Release to render.com. We'll use our  <a target="_blank" href="https://blog.miguelcoba.com/deploying-a-phoenix-16-app-with-docker-and-elixir-releases">Docker image</a>.</p>
<h2 id="heading-prepare-elixir-release-for-deploying-to-rendercom">Prepare Elixir Release for deploying to render.com</h2>
<p>We only need to do a single change to our <code>runtime.exs</code> file. Add the <code>url:</code> line to the </p>
<pre><code class="lang-elixir">  config :saturn, SaturnWeb.Endpoint,
    url: [host: System.get_env("RENDER_EXTERNAL_HOSTNAME") || "localhost", port: 80],
    http: [
      # Enable IPv6 and bind on all interfaces.
      # Set it to  {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
      # See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html
      # for details about using IPv6 vs IPv4 and loopback vs public addresses.
      ip: {0, 0, 0, 0, 0, 0, 0, 0},
      port: String.to_integer(System.get_env("PORT") || "4000")
    ],
    secret_key_base: secret_key_base
</code></pre>
<p>I am creating a branch named <code>render-deployment</code> and committing all these changes to it:</p>
<pre><code class="lang-bash">git checkout -b render-deployment
git add .
git commit -m <span class="hljs-string">"Config for render.com"</span>
git push -u origin render-deployment
</code></pre>
<h2 id="heading-create-and-configure-your-rendercom-account">Create and configure your render.com account</h2>
<p>Create an account in render.com and log in. We are going to create two services, one for the database and one for the Elixir Release. We must be sure that both services are created in the <em>same</em> region so that they share the same private network and can communicate and establish connections using their assigned network names. If we fail to do this, you'll see errors in the logs saying that the database domain name is non-existant (:nxdomain error). </p>
<p>For this article, I'm going to put both in the <code>Oregon, USA</code> region. Be sure to use the same one.</p>
<h3 id="heading-create-the-database">Create the Database</h3>
<p>Create a database service by clicking the <code>New +</code> button on the dashboard:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635721631664/QMiDFgyms.png" alt="Create a new Database" /></p>
<p>Select one name for this service (I'm using Saturn DB) and ensure the <code>Oregon, USA</code> region is selected. Leave the other fields to their defaults and select the <code>Free</code> plan. Then click the <code>Create Database</code> to provision this service.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635721672198/FYbLozYcN.png" alt="Configure Database" /></p>
<p>In the next page you'll see the details of the database. We need to copy one value from here, the connection string that we'll use for the <code>DATABASE_URL</code> environment variable when we create the Elixir Release service.</p>
<p>Click the copy button on the <em>Internal Connection String</em> </p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635722517481/NFegZbe52.png" alt="Copy Internal Connection String" /></p>
<h3 id="heading-create-the-web-service">Create the Web Service</h3>
<p>Let's create a <em>Web Service</em> with the <code>New +</code> button:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635722671651/36ohPqXCD.png" alt="Create a Web Service" /></p>
<p>You'll need to connect your GitHub or GitLab account:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635722714452/LnP5PXnxr.png" alt="Connect your GitHub or GitLab account" /></p>
<p>and select the repository you want to deploy:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635723134713/egF-hgDn_.png" alt="Select repository to deploy" /></p>
<p>Let's configure it. Set a name for your service (I'm using Saturn here). You'll see that the Dockerfile was detected and the Environment is preselected to <code>Docker</code>. Ensure you're using the <code>Oregon, USA</code> region, otherwise both services won't be able to connect each other. Select the branch you want to deploy (in my case is <code>render-deployment</code>) and select the <em>Free</em> plan.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635723356345/S6AQZqazo.png" alt="Configure Web Service" /></p>
<p>We need to add a couple of environment variables. </p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635723552393/SfpUUpMfk.png" alt="Create environment variables" /></p>
<p>Create a <code>DATABASE_URL</code> and paste the value you copied from the <em>Internal Connection String</em> field in the Database service config page.</p>
<p>Then add another named <code>SECRET_KEY_BASE</code>. Run this command on the terminal and use the result as the value for it:</p>
<pre><code class="lang-bash">mix phx.gen.secret
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635723571735/wGYyB8-Ov.png" alt="Environment variables created" /></p>
<p>You can now create the Web Service. You'll see the status of the provisioning and when it is ready you'll see a green <code>Live</code> button and a line on the logs saying that the Endpoint is listening for requests:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635723628077/6J6yjGBZH.png" alt="Provisioning Web Service" /></p>
<p>If you click on the URL generated for your Web Service you'll see your Elixir Release app running on Render.com infrastructure:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635723775006/4lTzosniF.png" alt="Elixir Release running on render.com" /></p>
<h2 id="heading-running-migrations-and-connecting-to-the-running-instance">Running Migrations and connecting to the running instance</h2>
<p>This is something I couldn't do so far. The UI render provides has no way to connect to the running container. One way to run the migrations would be:</p>
<ol>
<li>Build the Elixir Release locally as shown <a target="_blank" href="https://blog.miguelcoba.com/preparing-a-phoenix-16-app-for-deployment-with-elixir-releases">here</a> </li>
<li>Copy the <em>External Connection String</em> from the database configuration page</li>
<li>Set a DATABASE_URL environment variable to that value</li>
<li>Run the <code>_build/prod/rel/saturn/bin/saturn eval "Saturn.Release.migrate"</code> command</li>
</ol>
<h2 id="heading-source-code">Source code</h2>
<p>The  <a target="_blank" href="https://github.com/miguelcoba/saturn/tree/render-deployment">source code</a>  for the saturn project is open source under the MIT license. Use the <code>render-deployment</code> branch.</p>
<h2 id="heading-about">About</h2>
<p>I'm <a target="_blank" href="https://miguelcoba.com">Miguel Cobá</a>. I write about Elixir, Elm, Software Development, and eBook writing.</p>
<ul>
<li>Follow me on <a target="_blank" href="https://twitter.com/MiguelCoba_">Twitter</a></li>
<li>Subscribe to my <a target="_blank" href="https://newsletter.miguelcoba.com">newsletter</a></li>
<li>Read all my articles on my <a target="_blank" href="https://blog.miguelcoba.com">blog</a></li>
<li>Get my books:<ul>
<li><a target="_blank" href="https://store.miguelcoba.com/l/100elixirtips">100 Elixir Tips</a> [FREE]</li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/deployingelixir">Deploying Elixir</a> [FREE]</li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/advancedtopics">Deploying Elixir: Advanced Topics</a></li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/ebookwriting">eBook Writing Workflow for Developers</a></li>
</ul>
</li>
</ul>
<p>Photo by  <a target="_blank" href="https://unsplash.com/@mueen_30?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Mueen Agherdien</a> on  <a target="_blank" href="https://unsplash.com/t/architecture?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></p>
]]></content:encoded></item><item><title><![CDATA[Deploying an Elixir Release using Docker on Fly.io]]></title><description><![CDATA[I'm going to show you how to deploy our Elixir Release to Fly.io. We'll use our  Docker image.
Prepare Elixir Release for deploying to Fly.io
Fly.io uses IPv6 in all their internal networks. So we need to configure our app to use IPv6 if we want to c...]]></description><link>https://blog.miguelcoba.com/deploying-an-elixir-release-using-docker-on-flyio</link><guid isPermaLink="true">https://blog.miguelcoba.com/deploying-an-elixir-release-using-docker-on-flyio</guid><category><![CDATA[Elixir]]></category><category><![CDATA[Phoenix framework]]></category><category><![CDATA[deployment]]></category><category><![CDATA[Docker]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Miguel Cobá]]></dc:creator><pubDate>Mon, 01 Nov 2021 10:36:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1635761654486/UZnO5G3As.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I'm going to show you how to deploy our Elixir Release to Fly.io. We'll use our  <a target="_blank" href="https://blog.miguelcoba.com/deploying-a-phoenix-16-app-with-docker-and-elixir-releases">Docker image</a>.</p>
<h2 id="heading-prepare-elixir-release-for-deploying-to-flyio">Prepare Elixir Release for deploying to Fly.io</h2>
<p>Fly.io uses IPv6 in all their internal networks. So we need to configure our app to use IPv6 if we want to connect the app to the database.</p>
<p>Run this command to generate, among others, the <code>rel/env.sh.eex</code> file:</p>
<pre><code class="lang-bash">mix release.init
</code></pre>
<p>This file runs just before starting our application. It configures environment variables dynamically. Set the contents of the file to this:</p>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/sh</span>

ip=$(grep fly-local-6pn /etc/hosts | cut -f 1)
<span class="hljs-built_in">export</span> RELEASE_DISTRIBUTION=name
<span class="hljs-built_in">export</span> RELEASE_NODE=<span class="hljs-variable">$FLY_APP_NAME</span>@<span class="hljs-variable">$ip</span>
<span class="hljs-built_in">export</span> ELIXIR_ERL_OPTIONS=<span class="hljs-string">"-proto_dist inet6_tcp"</span>
</code></pre>
<p>This file gets the IPv6 assigned by fly.io on startup and assigns it to a variable. Then it uses that variable, along with the <code>FLY_APP_NAME</code> environment variable that fly.io automatically provides, to set another environment variable <code>RELEASE_NODE</code>. This will be used as a unique name for the node that our app is running in. The last line configures the BEAM virtual machine to use IPv6.</p>
<p>Let's modify the <code>config/runtime.exs</code> file. </p>
<p>Change the <code>Saturn.Repo</code> config to:</p>
<pre><code class="lang-elixir">config :saturn, Saturn.Repo,
    # ssl: true,
    socket_options: [:inet6],
    url: database_url,
    pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
</code></pre>
<p>Change the <code>SaturnWeb.Endpoint</code> to</p>
<pre><code class="lang-elixir">  app_name =
    System.get_env("FLY_APP_NAME") ||
      raise "FLY_APP_NAME not available"

  config :saturn, SaturnWeb.Endpoint,
    url: [host: "#{app_name}.fly.dev", port: 80],
    http: [
      # Enable IPv6 and bind on all interfaces.
      # Set it to  {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
      # See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html
      # for details about using IPv6 vs IPv4 and loopback vs public addresses.
      ip: {0, 0, 0, 0, 0, 0, 0, 0},
      port: String.to_integer(System.get_env("PORT") || "4000")
    ],
    secret_key_base: secret_key_base
</code></pre>
<p>Add a <code>.dockerignore</code> file to the root of the project:</p>
<pre><code>assets<span class="hljs-operator">/</span>node_modules<span class="hljs-operator">/</span>
deps<span class="hljs-operator">/</span>
</code></pre><p>Modify the <code>Dockerfile</code> and change the line that copies the <code>runtime.exs</code> file to this:</p>
<pre><code class="lang-Dockerfile"># copy runtime configuration file
COPY rel rel
COPY config/runtime.exs config/
</code></pre>
<p>I am creating a branch named <code>fly-io-deployment</code> and committing all these changes to it:</p>
<pre><code class="lang-bash">git checkout -b fly-io-deployment
git add .
git commit -m <span class="hljs-string">"Deploying to fly.io"</span>
git push -u origin fly-io-deployment
</code></pre>
<h2 id="heading-create-and-configure-your-flyio-account">Create and configure your Fly.io account</h2>
<h3 id="heading-install-flyctl">Install flyctl</h3>
<pre><code class="lang-bash">brew install superfly/tap/flyctl
</code></pre>
<h3 id="heading-sign-up-to-flyio">Sign up to fly.io</h3>
<p>If you don't have a fly.io account, create one</p>
<pre><code class="lang-bash">flyctl auth signup
</code></pre>
<h3 id="heading-login-to-flyio">Login to fly.io</h3>
<p>If you already have a fly.io account, login</p>
<pre><code class="lang-bash">flyctl auth login
</code></pre>
<h2 id="heading-create-a-flyio-app">Create a Fly.io app</h2>
<p>Before launching the app ensure you have added a credit card to your organization by visiting https://fly.io/organizations/personal and adding one. Otherwise, the next command won't work.</p>
<p>Once you're ready, run this command:</p>
<pre><code class="lang-bash">fly launch
</code></pre>
<p>It will ask you some things to configure your app in fly.io. Leave the App name blank in order to get a random name for it. Pick a region close to where you live and make sure that you answer no to the question about deploying now.</p>
<p>You should see something similar to this:</p>
<pre><code class="lang-bash">fly launch
Creating app <span class="hljs-keyword">in</span> /Users/mcoba/Code/saturn
Scanning <span class="hljs-built_in">source</span> code
Detected a Dockerfile app
? App Name (leave blank to use an auto-generated name):
Automatically selected personal organization: Miguel Cobá
? Select region: mad (Madrid, Spain)
Created app damp-paper-3277 <span class="hljs-keyword">in</span> organization personal
Wrote config file fly.toml
? Would you like to deploy now? No
Your app is ready. Deploy with `flyctl deploy`
</code></pre>
<p>Open the <code>fly.toml</code> file that flyctl created in the root of the project. Change the <code>kill_signal</code> to:</p>
<pre><code class="lang-toml"><span class="hljs-attr">kill_signal</span> = <span class="hljs-string">"SIGTERM"</span>
</code></pre>
<p>and add a <code>[deploy]</code> section after <code>[env]</code></p>
<pre><code class="lang-toml"><span class="hljs-section">[env]</span>

<span class="hljs-section">[deploy]</span>
  <span class="hljs-attr">release_command</span> = <span class="hljs-string">"eval Saturn.Release.migrate"</span>
</code></pre>
<p>change the <code>internal_port</code> to:</p>
<pre><code class="lang-toml">  <span class="hljs-attr">internal_port</span> = <span class="hljs-number">4000</span>
</code></pre>
<h2 id="heading-set-secrets-on-flyio">Set secrets on Fly.io</h2>
<p>We need to create some secrets in Fly.io infrastructure to be used when the app starts.</p>
<pre><code class="lang-bash">fly secrets <span class="hljs-built_in">set</span> SECRET_KEY_BASE=$(mix phx.gen.secret)
</code></pre>
<h2 id="heading-create-database">Create database</h2>
<p>Create a database for the app. Aswer the questions leaving the app name blank to get a random name and ensure you select the smallest VM size.</p>
<pre><code class="lang-bash">fly postgres create
</code></pre>
<p>You should see something similar to this:</p>
<pre><code class="lang-bash">fly postgres create

? App Name:
Automatically selected personal organization: Miguel Cobá
? Select region: mad (Madrid, Spain)
? Select VM size: shared-cpu-1x - 256
? Volume size (GB): 10
Creating postgres cluster  <span class="hljs-keyword">in</span> organization personal
Postgres cluster still-sun-6781 created
  Username:    postgres
  Password:   &lt;some big password&gt;
  Hostname:    still-sun-6781.internal
  Proxy Port:  5432
  PG Port: 5433
Save your credentials <span class="hljs-keyword">in</span> a secure place, you won<span class="hljs-string">'t be able to see them again!

Monitoring Deployment

2 desired, 2 placed, 0 healthy, 0 unhealthy [health checks: 6 total, 1 passing,
2 desired, 2 placed, 0 healthy, 0 unhealthy [health checks: 6 total, 1 passing,
2 desired, 2 placed, 0 healthy, 0 unhealthy [health checks: 6 total, 1 passing,
2 desired, 2 placed, 0 healthy, 0 unhealthy [health checks: 6 total, 2 passing,
2 desired, 2 placed, 0 healthy, 0 unhealthy [health checks: 6 total, 3 passing,
2 desired, 2 placed, 0 healthy, 0 unhealthy [health checks: 6 total, 4 passing,
2 desired, 2 placed, 0 healthy, 0 unhealthy [health checks: 6 total, 5 passing,
2 desired, 2 placed, 2 healthy, 0 unhealthy [health checks: 6 total, 6 passing]
--&gt; v0 deployed successfully

Connect to postgres
Any app within the personal organization can connect to postgres using the above credentials and the hostname "still-sun-6781.internal."
For example: postgres://postgres:&lt;the big password&gt;@still-sun-6781.internal:5432

See the postgres docs for more information on next steps, managing postgres, connecting from outside fly:  https://fly.io/docs/reference/postgres/</span>
</code></pre>
<p>Take note of the generated database name, you'll need it in the next step. Mine is: <code>still-sun-6781</code>.</p>
<p>What remains is to connect the Elixir Release app to the PostgreSQL app. Run this command but use  your own database name. This will create a new postgres user and password to connect from the Elixir Release to the PostgreSQL database:</p>
<pre><code class="lang-bash">fly postgres attach --postgres-app still-sun-6781
</code></pre>
<p>You'll see something like this:</p>
<pre><code class="lang-bash">fly postgres attach --postgres-app still-sun-6781

Postgres cluster still-sun-6781 is now attached to damp-paper-3277
The following secret was added to damp-paper-3277:
  DATABASE_URL=postgres://&lt;some new user&gt;:&lt;some new password&gt;@still-sun-6781.internal:5432/damp_paper_3277?sslmode=<span class="hljs-built_in">disable</span>
</code></pre>
<p>As you can see, this automatically created a secret with the <code>DATABASE_URL</code> that we were missing.</p>
<h2 id="heading-deploy-to-flyio">Deploy to Fly.io</h2>
<p>Do the deployment:</p>
<pre><code class="lang-bash">fly deploy
</code></pre>
<p>This will start the Docker image building, push it to fly.io's registry and then will deploy a container based on that image and will provide the secrets we configure it to start it. After lots of output logs you should see something like this:</p>
<pre><code class="lang-bash">==&gt; Release <span class="hljs-built_in">command</span>
Command: <span class="hljs-built_in">eval</span> Saturn.Release.migrate
     Starting instance
     Configuring virtual machine
     Pulling container image
     Unpacking image
     Preparing kernel init
     Configuring firecracker
     Starting virtual machine
     Starting init (commit: 50ffe20)...
     Preparing to run: `bin/saturn <span class="hljs-built_in">eval</span> Saturn.Release.migrate` as elixir
     2021/10/29 23:19:47 listening on [fdaa:0:37f6:a7b:2656:f312:7c7b:2]:22 (DNS: [fdaa::3]:53)
     Reaped child process with pid: 561 and signal: SIGUSR1, core dumped? <span class="hljs-literal">false</span>
     23:19:50.604 [info] Migrations already up
     Main child exited normally with code: 0
     Reaped child process with pid: 563 and signal: SIGUSR1, core dumped? <span class="hljs-literal">false</span>
     Starting clean up.
Monitoring Deployment

1 desired, 1 placed, 1 healthy, 0 unhealthy [health checks: 1 total, 1 passing]
--&gt; v1 deployed successfully
</code></pre>
<p>As you see the deployment was executed correctly and it ran the migrations. Now let's visit the app.</p>
<pre><code class="lang-bash">fly open
</code></pre>
<p>A browser is opened and you should be presented with your app, running on Fly.io infrastructure:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635549662884/iPT6CGI1F.png" alt="App running on fly.io" /></p>
<h2 id="heading-bonus">Bonus</h2>
<h3 id="heading-connect-to-the-running-node-with-iex">Connect to the running node with IEx</h3>
<p>We need to configure a secure ssh tunnel to the container running in fly.io.</p>
<pre><code class="lang-bash">fly ssh establish
fly ssh issue
</code></pre>
<p>Answer with your email and select a place to save your private keys. If you already use ssh for other connections you can save it to the same $HOME/.ssh/ directory. I got this:</p>
<pre><code class="lang-bash">fly ssh establish
Automatically selected personal organization: Miguel Cobá
Establishing SSH CA cert <span class="hljs-keyword">for</span> organization personal
New organization root certificate:
ssh-ed25519-cert-v01@openssh.com &lt;some big value&gt;

fly ssh issue
? Email address <span class="hljs-keyword">for</span> user to issue cert:  miguel.coba@gmail.com

!!!! WARNING: We<span class="hljs-string">'re now prompting you to save an SSH private key and certificate       !!!!
!!!! (the private key in "id_whatever" and the certificate in "id_whatever-cert.pub"). !!!!
!!!! These SSH credentials are time-limited and handling them in files is clunky;      !!!!
!!!! consider running an SSH agent and running this command with --agent. Things       !!!!
!!!! should just sort of work like magic if you do.                                    !!!!
? Path to store private key:  ~/.ssh/id_fly_io
? Path to store private key:  /Users/mcoba/.ssh/.id_fly_io
Wrote 24-hour SSH credential to /Users/mcoba/.ssh/.id_fly_io, /Users/mcoba/.ssh/.id_fly_io-cert.pub</span>
</code></pre>
<p>You can now connect to the container with <code>fly ssh console</code> and connect to the erlang node with <code>app/bin/saturn remote</code>:</p>
<pre><code class="lang-bash">fly ssh console
Connecting to damp-paper-3277.internal... complete
/ <span class="hljs-comment"># cd ~</span>
/home/elixir <span class="hljs-comment"># ls</span>
app
/home/elixir <span class="hljs-comment"># app/bin/saturn remote</span>
Erlang/OTP 24 [erts-12.1.2] [<span class="hljs-built_in">source</span>] [64-bit] [smp:1:1] [ds:1:1:10] [async-threads:1] [jit:no-native-stack]

Interactive Elixir (1.12.3) - press Ctrl+C to <span class="hljs-built_in">exit</span> (<span class="hljs-built_in">type</span> h() ENTER <span class="hljs-keyword">for</span> <span class="hljs-built_in">help</span>)
</code></pre>
<p>That's it.</p>
<h2 id="heading-source-code">Source code</h2>
<p>The  <a target="_blank" href="https://github.com/miguelcoba/saturn/tree/fly-io-deployment">source code</a>  for the saturn project is open source under the MIT license. Use the <code>fly-io-deployment</code> branch.</p>
<h2 id="heading-about">About</h2>
<p>I'm <a target="_blank" href="https://miguelcoba.com">Miguel Cobá</a>. I write about Elixir, Elm, Software Development, and eBook writing.</p>
<ul>
<li>Follow me on <a target="_blank" href="https://twitter.com/MiguelCoba_">Twitter</a></li>
<li>Subscribe to my <a target="_blank" href="https://newsletter.miguelcoba.com">newsletter</a></li>
<li>Read all my articles on my <a target="_blank" href="https://blog.miguelcoba.com">blog</a></li>
<li>Get my books:<ul>
<li><a target="_blank" href="https://store.miguelcoba.com/l/100elixirtips">100 Elixir Tips</a> [FREE]</li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/deployingelixir">Deploying Elixir</a> [FREE]</li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/advancedtopics">Deploying Elixir: Advanced Topics</a></li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/ebookwriting">eBook Writing Workflow for Developers</a></li>
</ul>
</li>
</ul>
<p>Photo by  <a target="_blank" href="https://unsplash.com/@rokkon?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Jan Ranft</a>  on  <a target="_blank" href="https://unsplash.com/s/photos/potion?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a> </p>
]]></content:encoded></item><item><title><![CDATA[Deploying an Elixir Release using Docker on DigitalOcean]]></title><description><![CDATA[Last time we created a Docker image and ran a container based on it. Now we'll deploy our Elixir Release Docker image in DigitalOcean.
There are several approaches to this:

Let DigitalOcean access directly your GitHub or GitLab repository
Put the im...]]></description><link>https://blog.miguelcoba.com/deploying-an-elixir-release-using-docker-on-digitalocean</link><guid isPermaLink="true">https://blog.miguelcoba.com/deploying-an-elixir-release-using-docker-on-digitalocean</guid><category><![CDATA[Docker]]></category><category><![CDATA[DigitalOcean]]></category><category><![CDATA[Elixir]]></category><category><![CDATA[deployment]]></category><category><![CDATA[Devops]]></category><dc:creator><![CDATA[Miguel Cobá]]></dc:creator><pubDate>Mon, 18 Oct 2021 10:02:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1636443052879/gYYR7jiG4.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Last time <a target="_blank" href="https://blog.miguelcoba.com/deploying-a-phoenix-16-app-with-docker-and-elixir-releases">we created a Docker image</a> and ran a container based on it. Now we'll deploy our Elixir Release Docker image in DigitalOcean.</p>
<p>There are several approaches to this:</p>
<ul>
<li>Let DigitalOcean access directly your GitHub or GitLab repository</li>
<li>Put the image in Docker public registry where DigitalOcean can fetch it</li>
<li>Put the image in DigitalOcean own Container Registry</li>
</ul>
<p>In this article we'll use the first approach </p>
<h2 id="heading-create-a-repository">Create a repository</h2>
<p>I'll show how to do it in GitHub but should be similarly easy in GitLab</p>
<h3 id="heading-github">GitHub</h3>
<p>Create a new repository</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635364364288/xmCZddtx0.png" alt="Create new repository" /></p>
<p>push the code:</p>
<pre><code class="lang-bash">git remote add origin git@github.com:miguelcoba/saturn.git
git branch -M main
git push -u origin main
</code></pre>
<h3 id="heading-prepare-to-deploy-to-digitalocean">Prepare to deploy to DigitalOcean</h3>
<p>DigitalOcean requires some changes to our code in order to correctly connect to the DB from our Docker application.</p>
<p>So far we have been establishing a plain, unencrypted connections between the application and the database. This won't work for DigitalOcean as they strictly enforce SSL when connecting to the database.</p>
<p>In our case, that means that we need to enable SSL on the application side every time we open a connection to the database.</p>
<p>First thing is to configure the Repo to use ssl. Open <code>runtime.ex</code> and uncomment the <code>ssl: true</code> line on the <code>Repo</code> section:</p>
<pre><code class="lang-elixir">  config :saturn, Saturn.Repo,
    ssl: true,
    # socket_options: [:inet6],
    url: database_url,
    pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
</code></pre>
<p>This will enable SSL for all the connections in the pool that the repository uses to connect to the database.</p>
<p>We need to change the <code>Saturn.Release</code> module that we use to run the migrations, because that also opens a connection to the database and we need it to start the <code>ssl</code> application at the beginning.</p>
<p>Change the <code>load_app</code> function in the <code>Saturn.Release</code> module to be like this:</p>
<pre><code class="lang-elixir">  defp load_app do
    Application.ensure_all_started(:ssl)
    Application.load(@app)
  end
</code></pre>
<p>This will ensure the ssl application is started before trying to connect to the database to run the migrations.</p>
<p>We need to commit this changes to the repository. DigitalOcean works by deploying a specific branch from our repository and building the Docker image with what it finds in that branch. If you plan to use Docker as your production environment then is fine to commit this to the main branch and setup DigitalOcean to use the main branch for deployment.
For this example, I will use a different branch to commit these changes and configure DigitalOcean to use that branch. I'll use a branch named <code>digital-ocean-deployment</code></p>
<pre><code class="lang-bash">git checkout -b digital-ocean-deployment
git add .
git commit -m <span class="hljs-string">"Setup for deploying to DigitalOcean"</span>
git push -u origin digital-ocean-deployment
</code></pre>
<h2 id="heading-creating-an-app-in-digitalocean">Creating an App in DigitalOcean</h2>
<p>Let's do the deployment to DigitalOcean.</p>
<p>Go to your DigitalOcean dashboard and create a new App:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635364401918/AlwF3dqrW.png" alt="Create New App" /></p>
<p>Select GitHub on the "Choose Source" screen:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635364431638/B6lOjh7QX.png" alt="Choose GitHub as a source" /></p>
<p>The first time you do this, GitHub will require you to authorize DigitalOcean to access your repositories. Follow the instructions and give access to the repository you just created</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635364462895/2R3Ll_oBG.png" alt="Authorize DigitalOcean" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635364497138/_VheH383o.png" alt="Authorization granted" /></p>
<p>You'll get back to the "Choose source" and you'll see the list of authorized repositories there</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635364530678/qr2SwQoh6.png" alt="Image description" /></p>
<p>Select your repository and you'll see that the <code>main</code> branch is selected automatically</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635364562456/_xGYMrZ-4.png" alt="Repository selected" /></p>
<p>If you're using <code>main</code> that's fine. For this article, I'll change it to the <code>digital-ocean-deployment</code> branch:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635364607779/H_4OpSTJh.png" alt="Branch selected" /></p>
<p>Click Next to move to the "Configure your app" page:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635364651854/f2k_l6_z1q.png" alt="Configure app" /></p>
<p>We need to configure the environment variables that DigitalOcean will provide to our Docker container in runtime. We can omit the <code>PORT</code> environment variable as DigitalOcean automatically provides it. We have to create the <code>POOL_SIZE</code>, <code>DATABASE_URL</code>, and <code>SECRET_KEY_BASE</code>. Let's start with the last one. </p>
<p>Click on the Edit link in the "Environment Variables" section. Then, in a terminal execute this command:</p>
<pre><code class="lang-bash">mix phx.gen.secret
</code></pre>
<p>Add a variable named SECRET_KEY_BASE and put the value you got from the command in it. Check the "Encrypt" option.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635364685029/fELShFoQJ.png" alt="SECRET_KEY_BASE environment variable" /></p>
<p>Now create a <code>POOL_SIZE</code> with value 2 and a DATABASE_URL with value <code>${db.DATABASE_URL}</code>. These two don't need to be encrypted.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635364713369/mlg2mTtJA.png" alt="Environment variables" /></p>
<p>We need to create a database. Click on the "Add a Database" button:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635364776822/ksp1muN18.png" alt="Add a Database" /></p>
<p>Accept the default values for the database and now you're ready to create the App in DigitalOcean.</p>
<p>One thing to note: the DATABASE_URL environment variable is referring to this database we just created. It will be replaced with the correct value to connect to this database when the application starts. </p>
<p>Click next to proceed to the "Name your web service" page:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635364809034/6qYgDhO6Z.png" alt="Name your web service" /></p>
<p>Accept the defaults and click Next. You're now in the "Finalize and launch" page:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635364842781/9YM0uyuSw.png" alt="Finalize and launch" /></p>
<p>Click the "Launch Basic App" and you'll see your app being created.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635364876486/IUDHBLC-1.png" alt="App creation" /></p>
<p>DigitalOcean will now:</p>
<ol>
<li>Access your repository</li>
<li>Clone it,</li>
<li>Detect the <code>Dockerfile</code> in it</li>
<li>Build a Docker image from it</li>
<li>Push it to their own Container Registry</li>
<li>Provision the database</li>
<li>Set up environment variables</li>
<li>Provision a server to run the Docker container</li>
<li>Run the application</li>
</ol>
<p>You can check the progress by inspecting the logs:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635365839518/Kg9uqGhi3.png" alt="Realtime logs" /></p>
<p>At the end you'll have your app deployed:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635365869954/wDKPU1BGr.png" alt="Deployment successful" /></p>
<h2 id="heading-run-the-migrations">Run the migrations</h2>
<p>You can access the elixir container console and run the migrations. Go to the "Console" tab and there evaluate the following command:</p>
<pre><code class="lang-bash">bin/saturn <span class="hljs-built_in">eval</span> <span class="hljs-string">"Saturn.Release.migrate"</span>
</code></pre>
<p>You'll see something like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635365900774/hMZxs9rZi.png" alt="Run DB migrations" /></p>
<p>Finally, visit the application by clicking the "Live App" button:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635366819591/R0QTIEYG6.png" alt="Live App" /></p>
<p>You'll see your app running in DigitalOcean's infrastructure:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635366863255/Hi5Ow9fdT.png" alt="Image description" /></p>
<p>We are done!</p>
<h2 id="heading-source-code">Source code</h2>
<p>The  <a target="_blank" href="https://github.com/miguelcoba/saturn/tree/digital-ocean-deployment">source code</a>  for the saturn project is open source under the MIT license. Use the <code>digital-ocean-deployment</code> branch.</p>
<h2 id="heading-about">About</h2>
<p>I'm <a target="_blank" href="https://miguelcoba.com">Miguel Cobá</a>. I write about Elixir, Elm, Software Development, and eBook writing.</p>
<ul>
<li>Follow me on <a target="_blank" href="https://twitter.com/MiguelCoba_">Twitter</a></li>
<li>Subscribe to my <a target="_blank" href="https://newsletter.miguelcoba.com">newsletter</a></li>
<li>Read all my articles on my <a target="_blank" href="https://blog.miguelcoba.com">blog</a></li>
<li>Get my books:<ul>
<li><a target="_blank" href="https://store.miguelcoba.com/l/100elixirtips">100 Elixir Tips</a> [FREE]</li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/deployingelixir">Deploying Elixir</a> [FREE]</li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/advancedtopics">Deploying Elixir: Advanced Topics</a></li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/ebookwriting">eBook Writing Workflow for Developers</a></li>
</ul>
</li>
</ul>
<p>Photo by <a target="_blank" href="https://unsplash.com/@kmkr?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Kris-Mikael Krister</a> on <a target="_blank" href="https://unsplash.com/s/photos/datacenter-ocean?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></p>
]]></content:encoded></item><item><title><![CDATA[Deploying a Phoenix 1.6 app with Docker and Elixir Releases]]></title><description><![CDATA[I'll explain how to deploy a Phoenix 1.6 application with Docker and Elixir Releases
Prerequisites
Follow the previous guides to prepare your Phoenix 1.6 image to use Elixir Releases.
Build process
Docker builds images in steps, each step generating ...]]></description><link>https://blog.miguelcoba.com/deploying-a-phoenix-16-app-with-docker-and-elixir-releases</link><guid isPermaLink="true">https://blog.miguelcoba.com/deploying-a-phoenix-16-app-with-docker-and-elixir-releases</guid><category><![CDATA[Docker]]></category><category><![CDATA[Elixir]]></category><category><![CDATA[Phoenix framework]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[deployment]]></category><dc:creator><![CDATA[Miguel Cobá]]></dc:creator><pubDate>Mon, 11 Oct 2021 17:25:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1636442621208/56fsaoDy5k.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I'll explain how to deploy a Phoenix 1.6 application with Docker and Elixir Releases</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Follow the previous guides to prepare your Phoenix 1.6 image to <a target="_blank" href="https://blog.miguelcoba.com/preparing-a-phoenix-16-app-for-deployment-with-elixir-releases">use Elixir Releases</a>.</p>
<h2 id="heading-build-process">Build process</h2>
<p>Docker builds images in steps, each step generating an image layer as a result. If you organize your steps wisely, you can avoid rebuilding layers and the build process will be faster. For example, if one step downloads dependencies for the project and the next step compiles the code, the first time you build it, both steps will be executed and two layers will be generated. But if you later make a change in the source code and rebuild the Docker image, you'll see that the second step will be executed but the first one will be skipped. A new second layer will be generated too, to replace the old second one. The first step won't be executed, because no changes to the dependencies were made and therefore there is no need to regenerate its image layer. The net effect is that you get a faster build time the second time.</p>
<p>The Docker image we are going to create for the Phoenix app will take advantage of this.</p>
<p>There is an additional feature we'll use. Docker allows you group steps into build stages. One build stage can refer to previous build stages. We will use one stage to do build the elixir release and another stage to run the release. </p>
<h2 id="heading-docker-image-stage-for-building-the-release">Docker image stage for building the release</h2>
<p>First let's create a <code>Dockerfile</code> in the root of the project:  </p>
<pre><code class="lang-Docker">ARG MIX_ENV="prod"

# build stage
FROM hexpm/elixir:1.12.3-erlang-24.1.2-alpine-3.14.2 AS build

# install build dependencies
RUN apk add --no-cache build-base git python3 curl
</code></pre>
<p>The first line declares an <a target="_blank" href="https://docs.docker.com/engine/reference/builder/#arg">argument</a> that can be passed to the docker builder at build time: <code>MIX_ENV</code>. Similar to command line arguments, it allows us to change the behavior of the build. We are also giving it a default value of "prod", so that we don't have to specify it every time.</p>
<p>Second line starts a build stage specifying a base image to build upon. It names the build stage as <code>build</code>. The base image we use has the same versions we use in our project: Elixir 1.12.3 and Erlang 24.1.2. Alpine is a small linux distribution with fast startup time and small memory usage.</p>
<p>The <code>RUN</code> command installs some basic build tools into the Alpine Linux image, creating a docker image layer as a result.  </p>
<pre><code class="lang-Docker"># sets work dir
WORKDIR /app

# install hex + rebar
RUN mix local.hex --force &amp;&amp; \
    mix local.rebar --force
</code></pre>
<p>This creates a working directory for all the subsequent commands to work on. Then installs <code>hex</code> and <code>rebar</code>.<br />As we seldom change the build tools or the hex or rebar install commands, this image layer won't be regenerated often.  </p>
<pre><code class="lang-Docker">ARG MIX_ENV
ENV MIX_ENV="${MIX_ENV}"

# install mix dependencies
COPY mix.exs mix.lock ./
RUN mix deps.get --only $MIX_ENV
</code></pre>
<p>This new layer will contain the mix dependencies for the environment we choose. You can see we are setting the <code>MIX_ENV</code> environment variable to the value of the <code>MIX_ENV</code> variable. Although they have the same name, they are different things. One is the value we pass from outside to the docker when we start the build. The other is an environment variable that will exist during the image build process. </p>
<p>But there is a catch. The <code>MIX_ENV</code> argument we declared in the first line of the Docker file doesn't exist anymore after the FROM line. We can use its default value if we redeclare it after a FROM as we are doing here. The effect is that the docker variable <code>MIX_ENV</code> will have a value again in this build stage.</p>
<p>Note that if we change the mix.ex or mix.lock files, this step will generate a new layer. Otherwise it will remain unchanged.  </p>
<pre><code class="lang-Docker"># copy compile configuration files
RUN mkdir config
COPY config/config.exs config/$MIX_ENV.exs config/

# compile dependencies
RUN mix deps.compile
</code></pre>
<p>New layer. This one copies the compile time configuration files and then start the dependencies compilation. Again, if we don't change the config files content, this layer won't be regenerated.  </p>
<pre><code class="lang-Docker"># copy assets
COPY priv priv
COPY assets assets

# Compile assets
RUN mix assets.deploy
</code></pre>
<p>This step compiles the web assets. Again, no changes in priv/ or assets/ means no new layer is created here.</p>
<p>Now we can compile the project binaries.  </p>
<pre><code class="lang-Docker"># compile project
COPY lib lib
RUN mix compile
</code></pre>
<p>We already have a compiled binary, but Elixir Release allows to configure things for runtime.  </p>
<pre><code class="lang-Docker"># copy runtime configuration file
COPY config/runtime.exs config/

# assemble release
RUN mix release
</code></pre>
<p>So this step is generating a new layer that will only be rebuilt if there are changes in the runtime configuration. The output of this step is an assembled release.</p>
<p>At this point we have built our Elixir/Phoenix release. But we only need the output binaries that were generated into the <code>_build/prod/rel/saturn</code> directory. Everything else is of no use for us in the production server. We can discard anything else for running the release.</p>
<h2 id="heading-docker-image-stage-for-running-the-release">Docker image stage for running the release</h2>
<p>We will use a new image stage for running the release. The release, as the Elixir documentation says, has anything required to run the project, including the Erlang virtual machine. That means that the hosting image doesn't need to have it installed separately. That's an extra opportunity to shrink the image size.  </p>
<pre><code class="lang-Docker"># app stage
FROM alpine:3.14.2 AS app

ARG MIX_ENV

# install runtime dependencies
RUN apk add --no-cache libstdc++ openssl ncurses-libs
</code></pre>
<p>This starts a new build stage named <code>app</code>. It uses a plain Alpine Linux base image without Elixir in it. Next, it redeclares the <code>MIX_ENV</code> docker variable so that it can be used later.<br />Then installs the runtime dependencies and nothing else.  </p>
<pre><code class="lang-Docker">ENV USER="elixir"

WORKDIR "/home/${USER}/app"
</code></pre>
<p>For security reasons we will to run the release binary using a non-privileged user in the Linux Alpine image. We are going to create a user named <code>elixir</code> for that. We declare an environment variable <code>USER</code> and the work dir pointing to a directory inside that user home. This user doesn't exist in the alpine base image we are using. Let's create it:  </p>
<pre><code class="lang-Docker"># Create  unprivileged user to run the release
RUN \
  addgroup \
   -g 1000 \
   -S "${USER}" \
  &amp;&amp; adduser \
   -s /bin/sh \
   -u 1000 \
   -G "${USER}" \
   -h "/home/${USER}" \
   -D "${USER}" \
  &amp;&amp; su "${USER}"
</code></pre>
<p>With the user created we can copy the build stage binaries into this build stage:  </p>
<pre><code class="lang-Docker"># run as user
USER "${USER}"

# copy release executables
COPY --from=build --chown="${USER}":"${USER}" /app/_build/"${MIX_ENV}"/rel/saturn ./
</code></pre>
<p>As you can see, we are switching to the newly created user and copying the assembled release into the workdir and changing ownership of the files to the new user.  </p>
<pre><code class="lang-Docker">ENTRYPOINT ["bin/saturn"]

CMD ["start"]
</code></pre>
<p>The last thing is to tell docker to run the container as an executable by telling it which command to run in ENTRYPOINT. The CMD specify default parameters to pass to the command in ENTRYPOINT in case we don't set any when running the container.</p>
<p>The <code>Dockerfile</code> should be like this:</p>
<pre><code class="lang-Dockerfile">ARG MIX_ENV="prod"

# build stage
FROM hexpm/elixir:1.12.3-erlang-24.1.2-alpine-3.14.2 AS build

# install build dependencies
RUN apk add --no-cache build-base git python3 curl

# sets work dir
WORKDIR /app

# install hex + rebar
RUN mix local.hex --force &amp;&amp; \
    mix local.rebar --force

ARG MIX_ENV
ENV MIX_ENV="${MIX_ENV}"

# install mix dependencies
COPY mix.exs mix.lock ./
RUN mix deps.get --only $MIX_ENV

# copy compile configuration files
RUN mkdir config
COPY config/config.exs config/$MIX_ENV.exs config/

# compile dependencies
RUN mix deps.compile

# copy assets
COPY priv priv
COPY assets assets

# Compile assets
RUN mix assets.deploy

# compile project
COPY lib lib
RUN mix compile

# copy runtime configuration file
COPY config/runtime.exs config/

# assemble release
RUN mix release

# app stage
FROM alpine:3.14.2 AS app

ARG MIX_ENV

# install runtime dependencies
RUN apk add --no-cache libstdc++ openssl ncurses-libs

ENV USER="elixir"

WORKDIR "/home/${USER}/app"

# Create  unprivileged user to run the release
RUN \
    addgroup \
    -g 1000 \
    -S "${USER}" \
    &amp;&amp; adduser \
    -s /bin/sh \
    -u 1000 \
    -G "${USER}" \
    -h "/home/${USER}" \
    -D "${USER}" \
    &amp;&amp; su "${USER}"

# run as user
USER "${USER}"

# copy release executables
COPY --from=build --chown="${USER}":"${USER}" /app/_build/"${MIX_ENV}"/rel/saturn ./

ENTRYPOINT ["bin/saturn"]

CMD ["start"]
</code></pre>
<p>Now that we have a Dockerfile ready, let's build the image:</p>
<p>In the root of the project run this:  </p>
<pre><code class="lang-bash">docker image build -t elixir/saturn .
</code></pre>
<p>This will build the image following the Dockerfile commands and will tag it with the name <code>elixir/saturn</code>. You'll see a lot of messages while it is being built. Also, the first time you run it will take some time as it will download the alpine images we specified in the FROM commands but the following times it will use a local cache and it will be faster.</p>
<p>You can check your local images with:  </p>
<pre><code class="lang-bash">docker image list
REPOSITORY                      TAG            IMAGE ID       CREATED         SIZE
elixir/saturn                   latest         02d3d6d963f5   5 seconds ago   23.3MB
</code></pre>
<p>Now it is time to run a container based on this image we created. So far we have been using the PostgreSQL database installed locally, and we used a DATABASE_URL like this:  </p>
<pre><code class="lang-bash"><span class="hljs-built_in">export</span> DATABASE_URL=ecto://postgres:postgres@localhost/saturn_dev
</code></pre>
<p>But that is not going to work when we start the container for our app. The localhost address is going to point to the same linux instance the app is running in and that container has no PostgreSQL installed. We need to change the url to point to a database that is outside the docker image.</p>
<p>One option is to use the IP address our laptop has because that is accessible from the docker container. You could do something like this:  </p>
<pre><code class="lang-bash">ifconfig en0
en0: flags=8863&lt;UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST&gt; mtu 1500
    options=400&lt;CHANNEL_IO&gt;
    ether 6c:96:cf:dd:fd:3b
    inet6 fe80::c61:da63:14a3:73af%en0 prefixlen 64 secured scopeid 0x4
    inet 192.168.1.135 netmask 0xffffff00 broadcast 192.168.1.255
    nd6 options=201&lt;PERFORMNUD,DAD&gt;
    media: autoselect
    status: active
</code></pre>
<p>and use that IP address in the <code>DATABASE_URL</code>. But we'll also need to configure PostgreSQL <code>pg_hba.conf</code> to accept connection from hosts other than localhost or relax the security options, because the default install with <code>brew</code> in macOS is restricted to connections coming from localhost and the security is set to trust.</p>
<p>I am going to do it in other way: using docker containers. I'll start a docker container for PostgreSQL and a docker container for our Elixir/Phoenix app. And I'll connect them using a virtual network. Then I'll run the migrations from the elixir app container.</p>
<p>Let's create a the virtual network:  </p>
<pre><code class="lang-bash">docker network create saturn-network
</code></pre>
<p>And let's get a postgres docker image and boot it up binding it up to the virtual network I just created:  </p>
<pre><code class="lang-bash">docker run -d --network saturn-network --network-alias postgres-server -e POSTGRES_PASSWORD=supersecret postgres
</code></pre>
<p>This command downloads the latest postgres docker image, binds it to the network we created, sets a DNS alias for it, <code>postgres-server</code>, sets an environment variable for the default user and, as we didn't say otherwise, will use the default username <code>postgres</code>. It also runs in detached mode.</p>
<p>Check that is running:  </p>
<pre><code class="lang-bash">docker ps
CONTAINER ID   IMAGE      COMMAND                  CREATED          STATUS          PORTS      NAMES
8281f722c845   postgres   <span class="hljs-string">"docker-entrypoint.s…"</span>   45 seconds ago   Up 44 seconds   5432/tcp   kind_mahavira
</code></pre>
<p>Let's connect to it to create the database. You need to use the CONTAINER ID you got from the <code>docker ps</code> (change the id to your own container id)  </p>
<pre><code class="lang-bash">docker <span class="hljs-built_in">exec</span> -it 8281f722c845 psql -U postgres
</code></pre>
<p>Create the production database, and exit the container:  </p>
<pre><code class="lang-bash">docker <span class="hljs-built_in">exec</span> -it 8281f722c845 psql -U postgres
psql (14.0 (Debian 14.0-1.pgdg110+1))
Type <span class="hljs-string">"help"</span> <span class="hljs-keyword">for</span> <span class="hljs-built_in">help</span>.

postgres=<span class="hljs-comment"># CREATE DATABASE saturn_prod;</span>
CREATE DATABASE
postgres=<span class="hljs-comment"># \q</span>
</code></pre>
<p>Now lets set our environment variables to point to this docker postgres database.:  </p>
<pre><code class="lang-bash"><span class="hljs-built_in">export</span> DATABASE_URL=ecto://postgres:supersecret@postgres-server/saturn_prod
</code></pre>
<p>The other environment variables don't require change, but they need to be exported in the shell you're going to use to run the elixir app container. If you have opened a new terminal ensure you have export them all there.</p>
<p>Now we can start the elixir app:  </p>
<pre><code class="lang-bash">docker container run -dp <span class="hljs-variable">$PORT</span>:<span class="hljs-variable">$PORT</span> -e POOL_SIZE -e PORT -e DATABASE_URL -e SECRET_KEY_BASE --network saturn-network  --name saturn elixir/saturn
</code></pre>
<p>You can see that we are mapping the port used inside the docker image to start the elixir release to the same port number on the running container. We pass the other environment variables as they are currently exported in the terminal. Notice that this docker container will bind to the <code>saturn-network</code> too, otherwise won't be able to reach the <code>postgres-server</code> host.</p>
<p>Finally, let's run the migrations. We will send the command to the elixir app container and that will run the migrations over the virtual network in the postgres container.  </p>
<pre><code class="lang-bash">docker <span class="hljs-built_in">exec</span> -it saturn bin/saturn <span class="hljs-built_in">eval</span> <span class="hljs-string">"Saturn.Release.migrate"</span>
</code></pre>
<p>You should see something like this:  </p>
<pre><code class="lang-bash">12:14:32.135 [info] Migrations already up
</code></pre>
<p>You can now point to <a target="_blank" href="http://localhost:4001">http://localhost:4001</a> and you'll be accessing your app, deployed as an Elixir Release, inside a docker container that is connected through a virtual network to another container with postgres running in it.</p>
<p>How cool is that?</p>
<h2 id="heading-source-code">Source code</h2>
<p>The  <a target="_blank" href="https://github.com/miguelcoba/saturn">source code</a>  for the saturn project is open source under the MIT license. Use the <code>main</code> branch.</p>
<h2 id="heading-about">About</h2>
<p>I'm <a target="_blank" href="https://miguelcoba.com">Miguel Cobá</a>. I write about Elixir, Elm, Software Development, and eBook writing.</p>
<ul>
<li>Follow me on <a target="_blank" href="https://twitter.com/MiguelCoba_">Twitter</a></li>
<li>Subscribe to my <a target="_blank" href="https://newsletter.miguelcoba.com">newsletter</a></li>
<li>Read all my articles on my <a target="_blank" href="https://blog.miguelcoba.com">blog</a></li>
<li>Get my books:<ul>
<li><a target="_blank" href="https://store.miguelcoba.com/l/100elixirtips">100 Elixir Tips</a> [FREE]</li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/deployingelixir">Deploying Elixir</a> [FREE]</li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/advancedtopics">Deploying Elixir: Advanced Topics</a></li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/ebookwriting">eBook Writing Workflow for Developers</a></li>
</ul>
</li>
</ul>
<p>Photo by <a target="_blank" href="https://unsplash.com/@carrier_lost?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Ian Taylor</a> on <a target="_blank" href="https://unsplash.com/s/photos/docker?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></p>
]]></content:encoded></item><item><title><![CDATA[Preparing a Phoenix 1.6 app for deployment with Elixir Releases]]></title><description><![CDATA[Updates

20211009 added "Migrations support" section

I'm going to prepare a Phoenix application for deployment.
Prerequisites
Ensure to have your Phoenix 1.6 app running locally.
Runtime configuration
When deploying to production is better to inject...]]></description><link>https://blog.miguelcoba.com/preparing-a-phoenix-16-app-for-deployment-with-elixir-releases</link><guid isPermaLink="true">https://blog.miguelcoba.com/preparing-a-phoenix-16-app-for-deployment-with-elixir-releases</guid><category><![CDATA[Elixir]]></category><category><![CDATA[Phoenix framework]]></category><category><![CDATA[Beginner Developers]]></category><category><![CDATA[deployment]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Miguel Cobá]]></dc:creator><pubDate>Mon, 04 Oct 2021 10:03:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1636442409648/9IDR-OeJ9.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h4 id="heading-updates">Updates</h4>
<ul>
<li>20211009 added "Migrations support" section</li>
</ul>
<p>I'm going to prepare a Phoenix application for deployment.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Ensure to have your <a target="_blank" href="https://blog.miguelcoba.com/creating-a-phoenix-16-application-with-asdf">Phoenix 1.6 app</a> running locally.</p>
<h2 id="heading-runtime-configuration">Runtime configuration</h2>
<p>When deploying to production is better to inject runtime info to the application when it starts instead of having that info hardcoded in the source code. We pass info to the app to affect the way it works depending on the environment we are deploying the app to (e.g. staging, production).</p>
<p>Elixir 1.11 has introduced a way to inject this runtime info easily with the <code>config/runtime.exs</code> config file. If you open that file you'll see that it obtains some values from environment variables. The default environment variables to configure are <code>POOL_SIZE</code>, <code>PORT</code>, <code>DATABASE_URL</code> and <code>SECRET_KEY_BASE</code>. We need to specify a value for those envvars if we want our deployment to work correctly.</p>
<p>For now we are going to test it locally, in our laptop. In a deployment service, like Gigalixir or Fly.io, those envvars are going to be provided when the app starts. We are going to do that manually here:  </p>
<pre><code class="lang-bash"><span class="hljs-built_in">export</span> POOL_SIZE=2
<span class="hljs-built_in">export</span> PORT=4001
<span class="hljs-built_in">export</span> DATABASE_URL=ecto://postgres:postgres@localhost/saturn_dev
<span class="hljs-built_in">export</span> SECRET_KEY_BASE=$(mix phx.gen.secret)
</code></pre>
<p>I have my database named locally saturn_dev and the user and password the ones shown. You can see your own connection parameters in <code>config/dev.exs</code></p>
<h2 id="heading-build-in-production-mode">Build in production mode</h2>
<h3 id="heading-compile-elixir-code">Compile elixir code</h3>
<p>We can now get the production dependencies:  </p>
<pre><code class="lang-bash">mix deps.get --only prod
MIX_ENV=prod mix compile
</code></pre>
<h3 id="heading-compile-assets">Compile assets</h3>
<p>If the project has JS, CSS or other assets you can also compile them with the esbuild wrapper that phoenix now uses:  </p>
<pre><code class="lang-bash">MIX_ENV=prod mix assets.deploy
</code></pre>
<h3 id="heading-test-that-the-project-starts-in-prod-mode">Test that the project starts in prod mode</h3>
<p>By now, if you haven't closed the terminal, you'll have the previous envvars still defined. If you have closed the terminal, you need to set them again.  </p>
<pre><code class="lang-bash">MIX_ENV=prod mix phx.server
</code></pre>
<p>If you go to <a target="_blank" href="http://localhost:4001/">http://localhost:4001/</a> you'll see the homepage of the app, but this time it is using the configuration that the <code>config/runtime.exs</code> read from the terminal when it started instead of using the <code>config/dev.exs</code> configuration. One thing you'll notice is that the LiveDashboard link is gone. This works only in dev mode.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635361326294/BuOZUxzMY.png" alt="Phoenix App running in prod mode" /></p>
<h2 id="heading-generate-a-release">Generate a release</h2>
<p>We need to do an extra step before building the release using Elixir Releases. Open <code>config/runtime.exs</code> and uncomment the following line, in the section titled "Using releases"  </p>
<pre><code class="lang-elixir">config :saturn, SaturnWeb.Endpoint, server: true
</code></pre>
<p>This direct the app to start the webserver when running the release executable. When we used <code>mix phx.server</code> this was done for us. Now we need to explicitly enable it.</p>
<p>After saving those changes we can now generate the release:  </p>
<pre><code class="lang-bash">MIX_ENV=prod mix release
</code></pre>
<h2 id="heading-run-the-release">Run the release</h2>
<p>We can now run the release executable generated by the mix release task:  </p>
<pre><code class="lang-bash">_build/prod/rel/saturn/bin/saturn start
</code></pre>
<p>If you go again to <a target="_blank" href="http://localhost:4001/">http://localhost:4001/</a> you'll see the app running, but this time from the self-contained bundle that the Elixir Releases generated for us. </p>
<h2 id="heading-migrations-support">Migrations support</h2>
<p>There is one more thing before finishing. Right now we are using a database that was created by a <code>mix ecto.create</code> command. But the release we just generated has no support for running mix in production. There is no <code>mix</code> command anywhere inside the <code>_build/prod/rel</code> folder. So how are we going to create the database and to run the phoenix migrations? Good question. We need a workaround that is embedded in the application itself.</p>
<p>Create a file in <code>lib/saturn/release.ex</code> and put this content there:  </p>
<pre><code class="lang-Elixir">defmodule Saturn.Release do
  @app :saturn

  def migrate do
    load_app()

    for repo &lt;- repos() do
      {:ok, _, _} = Ecto.Migrator.with_repo(repo, &amp;Ecto.Migrator.run(&amp;1, :up, all: true))
    end
  end

  def rollback(repo, version) do
    load_app()
    {:ok, _, _} = Ecto.Migrator.with_repo(repo, &amp;Ecto.Migrator.run(&amp;1, :down, to: version))
  end

  defp repos do
    Application.fetch_env!(@app, :ecto_repos)
  end

  defp load_app do
    Application.load(@app)
  end
end
</code></pre>
<p>Save the file and regenerate the release:  </p>
<pre><code class="lang-bash">MIX_ENV=prod mix release
</code></pre>
<p>Let's create a production database in our local postgres to simulate the production database in our production environment. Login to PostgreSQL locally create the database:  </p>
<pre><code class="lang-bash">psql -U postgres -h localhost 
psql (13.4)
Type <span class="hljs-string">"help"</span> <span class="hljs-keyword">for</span> <span class="hljs-built_in">help</span>.

postgres=<span class="hljs-comment"># CREATE DATABASE saturn_prod;</span>
CREATE DATABASE
</code></pre>
<p>You need to change the <code>DATABASE_URL</code> to point to the new database:  </p>
<pre><code class="lang-bash"><span class="hljs-built_in">export</span> DATABASE_URL=ecto://postgres:postgres@localhost/saturn_prod
</code></pre>
<p>And now you can run the migrations:  </p>
<pre><code class="lang-bash">_build/prod/rel/saturn/bin/saturn <span class="hljs-built_in">eval</span> <span class="hljs-string">"Saturn.Release.migrate"</span>
</code></pre>
<p>and you should see something like this:  </p>
<pre><code class="lang-bash">23:41:17.647 [info] Migrations already up
</code></pre>
<p>Now you can start the app and it will point to the <code>saturn_prod</code> database we just created:  </p>
<pre><code class="lang-bash">_build/prod/rel/saturn/bin/saturn start
</code></pre>
<p>Go again to <a target="_blank" href="http://localhost:4001">http://localhost:4001</a> and the app will work normally, but now the database is fully migrated.</p>
<p>That's it.</p>
<p>You can put the contents of the <code>_build/prod/rel/saturn</code> folder in your production server (as long as same architecture that the computer you used to assemble the release) and start it. You don't need anything else installed because this folder includes all the dependencies and binaries required to run the application. As long as you set the environment variables with correct values for production, this should work flawless.</p>
<p>You could do that manually, for example, by copying this folder to a DigitalOcean droplet or any other VPS provider, but there are better ways to do that.</p>
<p>I'll show you how to deploy to <a target="_blank" href="https://gigalixir.com/">Gigalixir</a> in a future post.</p>
<p>Cheers</p>
<h2 id="heading-about">About</h2>
<p>I'm <a target="_blank" href="https://miguelcoba.com">Miguel Cobá</a>. I write about Elixir, Elm, Software Development, and eBook writing.</p>
<ul>
<li>Follow me on <a target="_blank" href="https://twitter.com/MiguelCoba_">Twitter</a></li>
<li>Subscribe to my <a target="_blank" href="https://newsletter.miguelcoba.com">newsletter</a></li>
<li>Read all my articles on my <a target="_blank" href="https://blog.miguelcoba.com">blog</a></li>
<li>Get my books:<ul>
<li><a target="_blank" href="https://store.miguelcoba.com/l/100elixirtips">100 Elixir Tips</a> [FREE]</li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/deployingelixir">Deploying Elixir</a> [FREE]</li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/advancedtopics">Deploying Elixir: Advanced Topics</a></li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/ebookwriting">eBook Writing Workflow for Developers</a></li>
</ul>
</li>
</ul>
<p>Photo by <a target="_blank" href="https://unsplash.com/@yamnez?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Gautier Salles</a> on <a target="_blank" href="https://unsplash.com/s/photos/phoenix?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></p>
]]></content:encoded></item><item><title><![CDATA[Creating a Phoenix 1.6 application with asdf]]></title><description><![CDATA[The easiest way to create a new Phoenix Framework application:
Install asdf
Install asdf
Install plugins
asdf plugin-add erlang
asdf plugin-add elixir
Install dependencies
asdf install erlang 24.1.2
asdf global erlang 24.1.2
asdf install elixir 1.12....]]></description><link>https://blog.miguelcoba.com/creating-a-phoenix-16-application-with-asdf</link><guid isPermaLink="true">https://blog.miguelcoba.com/creating-a-phoenix-16-application-with-asdf</guid><category><![CDATA[Elixir]]></category><category><![CDATA[Phoenix framework]]></category><category><![CDATA[Beginner Developers]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[backend]]></category><dc:creator><![CDATA[Miguel Cobá]]></dc:creator><pubDate>Mon, 20 Sep 2021 15:16:48 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1636442071077/kKQShI77P.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The easiest way to create a new <a target="_blank" href="https://www.phoenixframework.org">Phoenix Framework</a> application:</p>
<h2 id="heading-install-asdf">Install asdf</h2>
<p><a target="_blank" href="https://asdf-vm.com">Install asdf</a></p>
<h2 id="heading-install-plugins">Install plugins</h2>
<pre><code>asdf plugin-<span class="hljs-keyword">add</span> erlang
asdf plugin-<span class="hljs-keyword">add</span> elixir
</code></pre><h2 id="heading-install-dependencies">Install dependencies</h2>
<pre><code><span class="hljs-attribute">asdf</span> install erlang <span class="hljs-number">24</span>.<span class="hljs-number">1</span>.<span class="hljs-number">2</span>
<span class="hljs-attribute">asdf</span> global erlang <span class="hljs-number">24</span>.<span class="hljs-number">1</span>.<span class="hljs-number">2</span>
<span class="hljs-attribute">asdf</span> install elixir <span class="hljs-number">1</span>.<span class="hljs-number">12</span>.<span class="hljs-number">3</span>-otp-<span class="hljs-number">24</span>
<span class="hljs-attribute">asdf</span> global elixir <span class="hljs-number">1</span>.<span class="hljs-number">12</span>.<span class="hljs-number">3</span>-otp-<span class="hljs-number">24</span>
</code></pre><h2 id="heading-install-phoenix">Install Phoenix</h2>
<pre><code>mix local.rebar <span class="hljs-operator">-</span><span class="hljs-operator">-</span>force
mix local.hex <span class="hljs-operator">-</span><span class="hljs-operator">-</span>force
mix archive.install hex phx_new <span class="hljs-number">1.6</span><span class="hljs-number">.0</span> <span class="hljs-operator">-</span><span class="hljs-operator">-</span>force
</code></pre><h2 id="heading-create-the-phoenix-application">Create the Phoenix application</h2>
<pre><code>mix phx.new saturn <span class="hljs-operator">-</span><span class="hljs-operator">-</span>install
</code></pre><h2 id="heading-create-db-and-run-application">Create DB and run application</h2>
<pre><code>cd saturn
mix ecto.<span class="hljs-keyword">create</span>
mix phx.<span class="hljs-keyword">server</span>
</code></pre><h2 id="heading-visit-your-phoenix-livedashboard">Visit your Phoenix LiveDashboard</h2>
<p>Open <a target="_blank" href="http://localhost:4000/dashboard">http://localhost:4000/dashboard</a></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635361156078/BU89Vd52h.png" alt="Phoenix LiveDashboard" /></p>
<p>Done.</p>
<h2 id="heading-about">About</h2>
<p>I'm <a target="_blank" href="https://miguelcoba.com">Miguel Cobá</a>. I write about Elixir, Elm, Software Development, and eBook writing.</p>
<ul>
<li>Follow me on <a target="_blank" href="https://twitter.com/MiguelCoba_">Twitter</a></li>
<li>Subscribe to my <a target="_blank" href="https://newsletter.miguelcoba.com">newsletter</a></li>
<li>Read all my articles on my <a target="_blank" href="https://blog.miguelcoba.com">blog</a></li>
<li>Get my books:<ul>
<li><a target="_blank" href="https://store.miguelcoba.com/l/100elixirtips">100 Elixir Tips</a> [FREE]</li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/deployingelixir">Deploying Elixir</a> [FREE]</li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/advancedtopics">Deploying Elixir: Advanced Topics</a></li>
<li><a target="_blank" href="https://store.miguelcoba.com/l/ebookwriting">eBook Writing Workflow for Developers</a></li>
</ul>
</li>
</ul>
<p>Photo by <a target="_blank" href="https://unsplash.com/@kjkempt17?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Kyle Kempt</a> on <a target="_blank" href="https://unsplash.com/s/photos/phoenix?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a> </p>
]]></content:encoded></item></channel></rss>