<?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[Stuff I learnt]]></title><description><![CDATA[Stuff I learnt]]></description><link>https://ambrosemungai.com</link><generator>RSS for Node</generator><lastBuildDate>Wed, 15 Apr 2026 17:39:26 GMT</lastBuildDate><atom:link href="https://ambrosemungai.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Deploying Phoenix App to Production Using Docker Compose]]></title><description><![CDATA[In this article, we’ll break down a real-world docker-compose.yml file used to deploy a Phoenix web application with Docker Compose, Traefik v2.9 as a reverse proxy, and environment-based configuration. This setup is designed for production with HTTP...]]></description><link>https://ambrosemungai.com/deploying-phoenix-app-docker-compose</link><guid isPermaLink="true">https://ambrosemungai.com/deploying-phoenix-app-docker-compose</guid><category><![CDATA[Elixir]]></category><category><![CDATA[Docker]]></category><category><![CDATA[Docker compose]]></category><category><![CDATA[Traefik]]></category><category><![CDATA[Let's Encrypt]]></category><category><![CDATA[deployment]]></category><dc:creator><![CDATA[Ambrose Mungai]]></dc:creator><pubDate>Mon, 26 May 2025 20:35:44 GMT</pubDate><content:encoded><![CDATA[<p>In this article, we’ll break down a real-world <code>docker-compose.yml</code> file used to deploy a Phoenix web application with Docker Compose, Traefik v2.9 as a reverse proxy, and environment-based configuration. This setup is designed for production with HTTPS, automated certificates via Let’s Encrypt, and health checks.</p>
<p>Instead of configuring the Phoenix application to terminate SSL connections, we use a proxy, <a target="_blank" href="https://traefik.io/traefik/">Traefik</a>, to handle all SSL connections. This enables us to use <a target="_blank" href="https://letsencrypt.org">Let's Encrypt</a> for TLS certificates. Using Traefik to manage this allows automatic renewal of certificates.</p>
<h2 id="heading-health-check">Health Check</h2>
<p>To prepare the application for production deployment, we need to add a health check. The health check in your Docker Compose file ensures that the Phoenix application inside the main container is running and ready to receive traffic before other services (like Traefik) start routing requests to it.</p>
<p>The health check helps to:</p>
<ul>
<li>Prevent routing to broken apps: If the health check fails, Traefik (and other dependent services) will not route traffic to the container.</li>
<li>Support zero-downtime deployments: It ensures the app is fully up before switching traffic, which is essential for rolling updates.</li>
<li>Improve observability: You can monitor container health in Docker dashboards or orchestration tools like Docker Swarm or Kubernetes.</li>
</ul>
<pre><code class="lang-elixir"><span class="hljs-class"><span class="hljs-keyword">defmodule</span> <span class="hljs-title">MyappWeb.Healthcheck</span></span> <span class="hljs-keyword">do</span>
  <span class="hljs-variable">@moduledoc</span> <span class="hljs-string">"""
  A plug for health check, bypasses TLS rewrites.
  """</span>

  <span class="hljs-variable">@behaviour</span> Plug

  <span class="hljs-keyword">import</span> Plug.Conn

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">init</span></span>(opts), <span class="hljs-symbol">do:</span> opts

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">call</span></span>(%{<span class="hljs-symbol">request_path:</span> <span class="hljs-string">"/health"</span>} = conn, _) <span class="hljs-keyword">do</span>
    conn
    |&gt; send_resp(<span class="hljs-number">200</span>, <span class="hljs-string">""</span>)
    |&gt; halt()
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">call</span></span>(conn, _), <span class="hljs-symbol">do:</span> conn
<span class="hljs-keyword">end</span>
</code></pre>
<p>Add the plug to the <code>MyappWeb.Endpoint</code> just after the socket definition:</p>
<pre><code class="lang-elixir">  plug MyappWeb.Healthcheck
</code></pre>
<h2 id="heading-mix-release">Mix Release</h2>
<p>This article assumes you have configured your application as discussed in the previous blog post: <a target="_blank" href="https://ambrosemungai.com/improving-your-elixir-configuration">Improving Your Elixir Configuration</a>.</p>
<p>Elixir applications are prepared for deployment by running the <code>MIX_ENV=prod mix release</code> command. This command assembles a self-contained release that can be packaged and deployed, provided the target runs the same operating system distribution and version as the machine that ran the <code>mix release</code> command. The release directory includes the Erlang VM, Elixir, all code, and dependencies, which can then be deployed to the production machine.</p>
<p>Phoenix applications come with a Dockerfile to build Docker images. You can generate the Dockerfile by running:</p>
<pre><code class="lang-bash">mix phx.gen.release --docker
</code></pre>
<h2 id="heading-environmental-variables">Environmental variables</h2>
<p>To configure the deployed application we will use an <code>.env</code> file that will be in the same folder as the <code>compose.yml</code>. </p>
<h2 id="heading-docker-compose-file">Docker Compose File</h2>
<p>This Compose file defines two primary services:</p>
<ul>
<li><code>traefik</code>: Acts as the HTTPS reverse proxy and certificate manager.</li>
<li><code>main</code>: A Phoenix application container that listens on port defined in  <code>.env</code>.</li>
</ul>
<p>It also defines:</p>
<ul>
<li>A shared volume for storing SSL certificate data.</li>
<li>A private network (<code>myapp_network</code>) connecting the services.</li>
</ul>
<h3 id="heading-traefik-service-reverse-proxy">Traefik Service (Reverse Proxy)</h3>
<pre><code class="lang-yaml"><span class="hljs-attr">services:</span>
  <span class="hljs-attr">traefik:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">traefik:v2.9</span>
</code></pre>
<p>Traefik is configured as a standalone container that proxies HTTP(S) traffic to the Phoenix app.</p>
<p>Key configuration:</p>
<ul>
<li>Networks: Connects to <code>myapp_network</code> to route traffic to the Phoenix app.</li>
<li>Volumes:<ul>
<li><code>/var/run/docker.sock</code>: Allows Traefik to discover running containers and their labels.</li>
<li><code>production_traefik:/etc/traefik/acme</code>: Stores Let’s Encrypt TLS certificates.</li>
</ul>
</li>
<li>Ports:<ul>
<li><code>80</code> for HTTP.</li>
<li><code>443</code> for HTTPS.</li>
</ul>
</li>
<li>Command Flags:<ul>
<li>Sets up HTTP → HTTPS redirection.</li>
<li>Enables automatic TLS via Let’s Encrypt.</li>
<li>Uses Docker as a provider for service discovery.</li>
</ul>
</li>
</ul>
<h3 id="heading-lets-encrypt">Let's Encrypt</h3>
<pre><code class="lang-yaml"><span class="hljs-string">--certificatesResolvers.letsencrypt.acme.email=support@myapp.com</span>
<span class="hljs-string">--certificatesResolvers.letsencrypt.acme.storage=/etc/traefik/acme/acme.json</span>
</code></pre>
<p>Traefik automatically generates and renews TLS certificates using the provided email and stores them in the mounted volume.</p>
<h3 id="heading-phoenix-app-service-main">Phoenix App Service (main)</h3>
<p>Build the Phoenix application using the Dockerfile in the current directory:</p>
<pre><code class="lang-yaml">  <span class="hljs-attr">main:</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">main_phoenix_app</span>
    <span class="hljs-attr">build:</span>
      <span class="hljs-attr">context:</span> <span class="hljs-string">.</span>
      <span class="hljs-attr">dockerfile:</span> <span class="hljs-string">Dockerfile</span>
</code></pre>
<h2 id="heading-traefik-labels">Traefik Labels</h2>
<p>These labels instruct Traefik how to route traffic to the Phoenix app:</p>
<ul>
<li>Routing Rule: Matches both the root domain and <code>www.</code> version using the <code>${ENDPOINT_URL_HOST}</code> environment variable.</li>
<li>Middlewares: Handle CSRF headers, redirect <code>http://www.*</code> to <code>https://</code>, and inject forwarded HTTPS headers.</li>
<li>TLS Settings: Enable HTTPS and link to the Let’s Encrypt resolver.</li>
</ul>
<h2 id="heading-compose-file">Compose File</h2>
<pre><code class="lang-yaml"><span class="hljs-attr">volumes:</span>
  <span class="hljs-attr">production_traefik:</span> {}

<span class="hljs-attr">services:</span>
  <span class="hljs-attr">traefik:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">traefik:v2.9</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">traefik</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">myapp_network</span>
    <span class="hljs-attr">depends_on:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">main</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">production_traefik:/etc/traefik/acme</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">/var/run/docker.sock:/var/run/docker.sock</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">'0.0.0.0:80:80'</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">'0.0.0.0:443:443'</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span>
    <span class="hljs-attr">labels:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"application=traefik"</span>
    <span class="hljs-attr">command:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">--log.level=INFO</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">--entryPoints.web.address=:80</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">--entryPoints.web.http.redirections.entryPoint.to=web-secure</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">--entryPoints.web.http.redirections.entryPoint.scheme=https</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">--entryPoints.web-secure.address=:443</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">--certificatesResolvers.letsencrypt.acme.email=support@myapp.com</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">--certificatesResolvers.letsencrypt.acme.storage=/etc/traefik/acme/acme.json</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">--certificatesResolvers.letsencrypt.acme.httpChallenge.entryPoint=web</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">--providers.docker=true</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">--providers.docker.exposedByDefault=false</span>

  <span class="hljs-attr">main:</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">main_phoenix_app</span>
    <span class="hljs-attr">build:</span>
      <span class="hljs-attr">context:</span> <span class="hljs-string">.</span>
      <span class="hljs-attr">dockerfile:</span> <span class="hljs-string">Dockerfile</span>
    <span class="hljs-attr">env_file:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">.env</span>
    <span class="hljs-attr">expose:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">${PORT}</span>
    <span class="hljs-attr">labels:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"application=main_phoenix_app"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.enable=true"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.http.middlewares.csrf.headers.hostsproxyheaders=X-CSRFToken"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.http.middlewares.redirect-https-www.redirectregex.regex=^https?://www\\.(.+)"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.http.middlewares.redirect-https-www.redirectregex.replacement=https://$${1}"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.http.middlewares.redirect-https-www.redirectregex.permanent=true"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.http.middlewares.forwarded-headers.headers.customrequestheaders.X-Forwarded-Proto=https"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.http.routers.main.rule=(Host(`${ENDPOINT_URL_HOST}`) || Host(`www.${ENDPOINT_URL_HOST}`)) &amp;&amp; PathPrefix(`/`)"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.http.routers.main.priority=1"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.http.routers.main.entryPoints=web-secure"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.http.routers.main.middlewares=redirect-https-www,csrf,forwarded-headers"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.http.routers.main.tls.certResolver=letsencrypt"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.http.routers.main.tls=true"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"traefik.http.services.main.loadBalancer.server.port=${PORT}"</span>
    <span class="hljs-attr">healthcheck:</span>
      <span class="hljs-attr">test:</span> [<span class="hljs-string">"CMD"</span>, <span class="hljs-string">"curl"</span>, <span class="hljs-string">"-f"</span>, <span class="hljs-string">"http://localhost:${PORT}/health"</span>]
      <span class="hljs-attr">interval:</span> <span class="hljs-string">30s</span>
      <span class="hljs-attr">timeout:</span> <span class="hljs-string">10s</span>
      <span class="hljs-attr">retries:</span> <span class="hljs-number">3</span>
    <span class="hljs-attr">networks:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">myapp_network</span>
    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span>

<span class="hljs-attr">networks:</span>
  <span class="hljs-attr">myapp_network:</span>
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this article, we demonstrated how to deploy a Phoenix application to production using Docker Compose and Traefik as a reverse proxy. By offloading SSL termination to Traefik and automating certificate management with Let’s Encrypt, you simplify your deployment and ensure secure HTTPS connections. Adding a health check improves reliability and supports zero-downtime deployments. This setup provides a robust foundation for deploying Phoenix apps in production environments.</p>
<p>To see the code changes, check out this <a target="_blank" href="https://github.com/walu-lila/blog_demo/pull/3">PR</a></p>
]]></content:encoded></item><item><title><![CDATA[Improving your elixir configuration]]></title><description><![CDATA[Updates
2025-05-20

Fix bugs in config
Add example .env file
Add GitHub link to PR

Main article
Configuration in the Elixir ecosystem has undergone multiple changes over the years. Configuration scripts are used to configure elixir applications duri...]]></description><link>https://ambrosemungai.com/improving-your-elixir-configuration</link><guid isPermaLink="true">https://ambrosemungai.com/improving-your-elixir-configuration</guid><category><![CDATA[Elixir]]></category><category><![CDATA[Phoenix framework]]></category><category><![CDATA[configuration management]]></category><category><![CDATA[Devops]]></category><dc:creator><![CDATA[Ambrose Mungai]]></dc:creator><pubDate>Fri, 23 May 2025 10:48:54 GMT</pubDate><content:encoded><![CDATA[<h3 id="heading-updates">Updates</h3>
<p>2025-05-20</p>
<ul>
<li>Fix bugs in config</li>
<li>Add example <code>.env</code> file</li>
<li>Add GitHub link to PR</li>
</ul>
<h2 id="heading-main-article">Main article</h2>
<p>Configuration in the Elixir ecosystem has undergone multiple changes over the years. Configuration scripts are used to configure elixir applications during the compile phase depending on the environment in which it is running ie: </p>
<ul>
<li><code>config/test.exs</code> for test environments</li>
<li><code>config/dev.exs</code> for development environments</li>
<li><code>config/prod.exs</code> for prodution environments.</li>
</ul>
<p>Overtime this resulted in problems since a new file needed to be added for every new environment, eg: staging environment, requiring compilation of the code again. This would not work in CI/CD environments since the same binary that was tested is expected to be used in production.</p>
<p>In Elixir 1.11 support for using <code>config/runtime.exs</code> which allowed for runtime configuration in all environments. The current structure of the config folder is as follows:</p>
<pre><code>config
├── config.exs
├── dev.exs
├── prod.exs
├── runtime.exs
└── test.exs
</code></pre><h2 id="heading-current-config-file-structures">Current config file structures</h2>
<p>If we look deeper into each of the configuration files with all the comments removed we can see that there is a lot of duplication.</p>
<h3 id="heading-config-file">Config file</h3>
<pre><code class="lang-elixir"><span class="hljs-keyword">import</span> Config

config <span class="hljs-symbol">:myapp</span>, <span class="hljs-symbol">:scopes</span>,
  <span class="hljs-symbol">user:</span> [
    <span class="hljs-symbol">default:</span> <span class="hljs-keyword">true</span>,
    <span class="hljs-symbol">module:</span> Myapp.Accounts.Scope,
    <span class="hljs-symbol">assign_key:</span> <span class="hljs-symbol">:current_scope</span>,
    <span class="hljs-symbol">access_path:</span> [<span class="hljs-symbol">:user</span>, <span class="hljs-symbol">:id</span>],
    <span class="hljs-symbol">schema_key:</span> <span class="hljs-symbol">:user_id</span>,
    <span class="hljs-symbol">schema_type:</span> <span class="hljs-symbol">:id</span>,
    <span class="hljs-symbol">schema_table:</span> <span class="hljs-symbol">:users</span>,
    <span class="hljs-symbol">test_data_fixture:</span> Myapp.AccountsFixtures,
    <span class="hljs-symbol">test_login_helper:</span> <span class="hljs-symbol">:register_and_log_in_user</span>
  ]

config <span class="hljs-symbol">:myapp</span>,
  <span class="hljs-symbol">ecto_repos:</span> [Myapp.Repo],
  <span class="hljs-symbol">generators:</span> [<span class="hljs-symbol">timestamp_type:</span> <span class="hljs-symbol">:utc_datetime</span>]

config <span class="hljs-symbol">:myapp</span>, MyappWeb.Endpoint,
  <span class="hljs-symbol">url:</span> [<span class="hljs-symbol">host:</span> <span class="hljs-string">"localhost"</span>],
  <span class="hljs-symbol">adapter:</span> Bandit.PhoenixAdapter,
  <span class="hljs-symbol">render_errors:</span> [
    <span class="hljs-symbol">formats:</span> [<span class="hljs-symbol">html:</span> MyappWeb.ErrorHTML, <span class="hljs-symbol">json:</span> MyappWeb.ErrorJSON],
    <span class="hljs-symbol">layout:</span> <span class="hljs-keyword">false</span>
  ],
  <span class="hljs-symbol">pubsub_server:</span> Myapp.PubSub,
  <span class="hljs-symbol">live_view:</span> [<span class="hljs-symbol">signing_salt:</span> <span class="hljs-string">"GkZs+o4a"</span>]

config <span class="hljs-symbol">:myapp</span>, Myapp.Mailer, <span class="hljs-symbol">adapter:</span> Swoosh.Adapters.Local

config <span class="hljs-symbol">:esbuild</span>,
  <span class="hljs-symbol">version:</span> <span class="hljs-string">"0.17.11"</span>,
  <span class="hljs-symbol">myapp:</span> [
    <span class="hljs-symbol">args:</span>
      <span class="hljs-string">~w(js/app.js --bundle --target=es2022 --outdir=../priv/static/assets/js --external:/fonts/* --external:/images/*)</span>,
    <span class="hljs-symbol">cd:</span> Path.expand(<span class="hljs-string">"../assets"</span>, __DIR__),
    <span class="hljs-symbol">env:</span> %{<span class="hljs-string">"NODE_PATH"</span> =&gt; Path.expand(<span class="hljs-string">"../deps"</span>, __DIR__)}
  ]

config <span class="hljs-symbol">:tailwind</span>,
  <span class="hljs-symbol">version:</span> <span class="hljs-string">"4.0.9"</span>,
  <span class="hljs-symbol">myapp:</span> [
    <span class="hljs-symbol">args:</span> <span class="hljs-string">~w(
      --input=assets/css/app.css
      --output=priv/static/assets/css/app.css
    )</span>,
    <span class="hljs-symbol">cd:</span> Path.expand(<span class="hljs-string">".."</span>, __DIR__)
  ]

config <span class="hljs-symbol">:logger</span>, <span class="hljs-symbol">:default_formatter</span>,
  <span class="hljs-symbol">format:</span> <span class="hljs-string">"$time $metadata[$level] $message\n"</span>,
  <span class="hljs-symbol">metadata:</span> [<span class="hljs-symbol">:request_id</span>]

config <span class="hljs-symbol">:phoenix</span>, <span class="hljs-symbol">:json_library</span>, Jason

import_config <span class="hljs-string">"<span class="hljs-subst">#{config_env()}</span>.exs"</span>
</code></pre>
<p>The last line <code>import_config "#{config_env()}.exs"</code> reads the config file associated with the current environment.</p>
<h3 id="heading-test-config-file">Test config file</h3>
<pre><code class="lang-elixir"><span class="hljs-keyword">import</span> Config

config <span class="hljs-symbol">:bcrypt_elixir</span>, <span class="hljs-symbol">:log_rounds</span>, <span class="hljs-number">1</span>

config <span class="hljs-symbol">:myapp</span>, Myapp.Repo,
  <span class="hljs-symbol">username:</span> <span class="hljs-string">"postgres"</span>,
  <span class="hljs-symbol">password:</span> <span class="hljs-string">"postgres"</span>,
  <span class="hljs-symbol">hostname:</span> <span class="hljs-string">"localhost"</span>,
  <span class="hljs-symbol">database:</span> <span class="hljs-string">"myapp_test<span class="hljs-subst">#{System.get_env(<span class="hljs-string">"MIX_TEST_PARTITION"</span>)}</span>"</span>,
  <span class="hljs-symbol">pool:</span> Ecto.Adapters.SQL.Sandbox,
  <span class="hljs-symbol">pool_size:</span> System.schedulers_online() * <span class="hljs-number">2</span>

config <span class="hljs-symbol">:myapp</span>, MyappWeb.Endpoint,
  <span class="hljs-symbol">http:</span> [<span class="hljs-symbol">ip:</span> {<span class="hljs-number">127</span>, 0, 0, <span class="hljs-number">1</span>}, <span class="hljs-symbol">port:</span> <span class="hljs-number">4002</span>],
  <span class="hljs-symbol">secret_key_base:</span> <span class="hljs-string">"6tvc42QacLkrYkrabIP+Xhj0LFaiF7bCNp4kMY5VT41zVVvFHywZB0Tq/F5PmjSJ"</span>,
  <span class="hljs-symbol">server:</span> <span class="hljs-keyword">false</span>

config <span class="hljs-symbol">:myapp</span>, Myapp.Mailer, <span class="hljs-symbol">adapter:</span> Swoosh.Adapters.Test

config <span class="hljs-symbol">:swoosh</span>, <span class="hljs-symbol">:api_client</span>, <span class="hljs-keyword">false</span>

config <span class="hljs-symbol">:logger</span>, <span class="hljs-symbol">level:</span> <span class="hljs-symbol">:warning</span>

config <span class="hljs-symbol">:phoenix</span>, <span class="hljs-symbol">:plug_init_mode</span>, <span class="hljs-symbol">:runtime</span>

config <span class="hljs-symbol">:phoenix_live_view</span>, <span class="hljs-symbol">enable_expensive_runtime_checks:</span> <span class="hljs-keyword">true</span>
</code></pre>
<h3 id="heading-dev-config-file">Dev config file</h3>
<pre><code class="lang-elixir"><span class="hljs-keyword">import</span> Config

config <span class="hljs-symbol">:myapp</span>, Myapp.Repo,
  <span class="hljs-symbol">username:</span> <span class="hljs-string">"postgres"</span>,
  <span class="hljs-symbol">password:</span> <span class="hljs-string">"postgres"</span>,
  <span class="hljs-symbol">hostname:</span> <span class="hljs-string">"localhost"</span>,
  <span class="hljs-symbol">database:</span> <span class="hljs-string">"myapp_dev"</span>,
  <span class="hljs-symbol">stacktrace:</span> <span class="hljs-keyword">true</span>,
  <span class="hljs-symbol">show_sensitive_data_on_connection_error:</span> <span class="hljs-keyword">true</span>,
  <span class="hljs-symbol">pool_size:</span> <span class="hljs-number">10</span>

config <span class="hljs-symbol">:myapp</span>, MyappWeb.Endpoint,
  <span class="hljs-symbol">http:</span> [<span class="hljs-symbol">ip:</span> {<span class="hljs-number">127</span>, 0, 0, <span class="hljs-number">1</span>}, <span class="hljs-symbol">port:</span> <span class="hljs-number">4000</span>],
  <span class="hljs-symbol">check_origin:</span> <span class="hljs-keyword">false</span>,
  <span class="hljs-symbol">code_reloader:</span> <span class="hljs-keyword">true</span>,
  <span class="hljs-symbol">debug_errors:</span> <span class="hljs-keyword">true</span>,
  <span class="hljs-symbol">secret_key_base:</span> <span class="hljs-string">"2UQSRkWSJcN3LSaNF6ZclG2Zv8ZpuEuGXDIiFSkE8z8a1AczuxJDDTGc3trNoQQu"</span>,
  <span class="hljs-symbol">watchers:</span> [
    <span class="hljs-symbol">esbuild:</span> {Esbuild, <span class="hljs-symbol">:install_and_run</span>, [<span class="hljs-symbol">:myapp</span>, <span class="hljs-string">~w(--sourcemap=inline --watch)</span>]},
    <span class="hljs-symbol">tailwind:</span> {Tailwind, <span class="hljs-symbol">:install_and_run</span>, [<span class="hljs-symbol">:myapp</span>, <span class="hljs-string">~w(--watch)</span>]}
  ]

config <span class="hljs-symbol">:myapp</span>, MyappWeb.Endpoint,
  <span class="hljs-symbol">live_reload:</span> [
    <span class="hljs-symbol">web_console_logger:</span> <span class="hljs-keyword">true</span>,
    <span class="hljs-symbol">patterns:</span> [
      <span class="hljs-string">~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$"</span>,
      <span class="hljs-string">~r"priv/gettext/.*(po)$"</span>,
      <span class="hljs-string">~r"lib/myapp_web/(controllers|live|components)/.*(ex|heex)$"</span>
    ]
  ]

config <span class="hljs-symbol">:myapp</span>, <span class="hljs-symbol">dev_routes:</span> <span class="hljs-keyword">true</span>

config <span class="hljs-symbol">:logger</span>, <span class="hljs-symbol">:default_formatter</span>, <span class="hljs-symbol">format:</span> <span class="hljs-string">"[$level] $message\n"</span>

config <span class="hljs-symbol">:phoenix</span>, <span class="hljs-symbol">:stacktrace_depth</span>, <span class="hljs-number">20</span>

config <span class="hljs-symbol">:phoenix</span>, <span class="hljs-symbol">:plug_init_mode</span>, <span class="hljs-symbol">:runtime</span>

config <span class="hljs-symbol">:phoenix_live_view</span>,
  <span class="hljs-symbol">debug_heex_annotations:</span> <span class="hljs-keyword">true</span>,
  <span class="hljs-symbol">enable_expensive_runtime_checks:</span> <span class="hljs-keyword">true</span>

config <span class="hljs-symbol">:swoosh</span>, <span class="hljs-symbol">:api_client</span>, <span class="hljs-keyword">false</span>
</code></pre>
<h3 id="heading-prod-config-file">Prod config file</h3>
<pre><code class="lang-elixir"><span class="hljs-keyword">import</span> Config

config <span class="hljs-symbol">:myapp</span>, MyappWeb.Endpoint, <span class="hljs-symbol">cache_static_manifest:</span> <span class="hljs-string">"priv/static/cache_manifest.json"</span>

config <span class="hljs-symbol">:swoosh</span>, <span class="hljs-symbol">api_client:</span> Swoosh.ApiClient.Req

config <span class="hljs-symbol">:swoosh</span>, <span class="hljs-symbol">local:</span> <span class="hljs-keyword">false</span>

config <span class="hljs-symbol">:logger</span>, <span class="hljs-symbol">level:</span> <span class="hljs-symbol">:info</span>
</code></pre>
<h3 id="heading-runtime-config-file">Runtime config file</h3>
<pre><code class="lang-elixir"><span class="hljs-keyword">import</span> Config

if System.get_env(<span class="hljs-string">"PHX_SERVER"</span>) <span class="hljs-keyword">do</span>
  config <span class="hljs-symbol">:myapp</span>, MyappWeb.Endpoint, <span class="hljs-symbol">server:</span> <span class="hljs-keyword">true</span>
<span class="hljs-keyword">end</span>

if config_env() == <span class="hljs-symbol">:prod</span> <span class="hljs-keyword">do</span>
  database_url =
    System.get_env(<span class="hljs-string">"DATABASE_URL"</span>) ||
      raise <span class="hljs-string">"""
      environment variable DATABASE_URL is missing.
      For example: ecto://USER:PASS@HOST/DATABASE
      """</span>

  maybe_ipv6 = if System.get_env(<span class="hljs-string">"ECTO_IPV6"</span>) <span class="hljs-keyword">in</span> <span class="hljs-string">~w(true 1)</span>, <span class="hljs-symbol">do:</span> [<span class="hljs-symbol">:inet6</span>], <span class="hljs-symbol">else:</span> []

  config <span class="hljs-symbol">:myapp</span>, Myapp.Repo,
    <span class="hljs-comment"># ssl: true,</span>
    <span class="hljs-symbol">url:</span> database_url,
    <span class="hljs-symbol">pool_size:</span> String.to_integer(System.get_env(<span class="hljs-string">"POOL_SIZE"</span>) || <span class="hljs-string">"10"</span>),
    <span class="hljs-symbol">socket_options:</span> maybe_ipv6

  secret_key_base =
    System.get_env(<span class="hljs-string">"SECRET_KEY_BASE"</span>) ||
      raise <span class="hljs-string">"""
      environment variable SECRET_KEY_BASE is missing.
      You can generate one by calling: mix phx.gen.secret
      """</span>

  host = System.get_env(<span class="hljs-string">"PHX_HOST"</span>) || <span class="hljs-string">"example.com"</span>
  port = String.to_integer(System.get_env(<span class="hljs-string">"PORT"</span>) || <span class="hljs-string">"4000"</span>)

  config <span class="hljs-symbol">:myapp</span>, <span class="hljs-symbol">:dns_cluster_query</span>, System.get_env(<span class="hljs-string">"DNS_CLUSTER_QUERY"</span>)

  config <span class="hljs-symbol">:myapp</span>, MyappWeb.Endpoint,
    <span class="hljs-symbol">url:</span> [<span class="hljs-symbol">host:</span> host, <span class="hljs-symbol">port:</span> <span class="hljs-number">443</span>, <span class="hljs-symbol">scheme:</span> <span class="hljs-string">"https"</span>],
    <span class="hljs-symbol">http:</span> [
      <span class="hljs-symbol">ip:</span> {0, 0, 0, 0, 0, 0, 0, 0},
      <span class="hljs-symbol">port:</span> port
    ],
    <span class="hljs-symbol">secret_key_base:</span> secret_key_base
<span class="hljs-keyword">end</span>
</code></pre>
<p>If we look at the above code we can see alot of duplication of application configuration.
Adding new configuration needs updates to 3 files. Also, the <code>config/runtime.exs</code> reads environment variables in production mode. There is no reason that we cannot use environmental variables to configure all the environments instead of  production only.</p>
<h2 id="heading-configuration-improvements">Configuration improvements</h2>
<p>In order to improve the configuration we can combine the configuration into 2 files <code>config/config.exs</code> and <code>config/runtime.exs</code> only. To do this we have to follow the following rules:</p>
<ol>
<li>Config that does not change depending on an environment should all be in the <code>config/config.exs</code> file</li>
<li>Config that varies by environment but is hardcoded should also be in the <code>config/config.exs</code> file inside <code>if</code> function blocks eg:<pre><code class="lang-elixir">if config_env() == <span class="hljs-symbol">:test</span> <span class="hljs-keyword">do</span>
 config <span class="hljs-symbol">:swoosh</span>, <span class="hljs-symbol">:api_client</span>, <span class="hljs-keyword">false</span>
<span class="hljs-keyword">end</span>
</code></pre>
</li>
<li>All other configuration should be stored in the <code>config/runtime.exs</code> file and should parse operating system environment variables.</li>
</ol>
<p>The new config folder should look as:</p>
<pre><code>config
├── config.exs
└── runtime.exs
</code></pre><h3 id="heading-new-configexs-file">New <code>config.exs</code> file</h3>
<pre><code class="lang-elixir"><span class="hljs-keyword">import</span> Config

config <span class="hljs-symbol">:myapp</span>, <span class="hljs-symbol">:scopes</span>,
  <span class="hljs-symbol">user:</span> [
    <span class="hljs-symbol">default:</span> <span class="hljs-keyword">true</span>,
    <span class="hljs-symbol">module:</span> Myapp.Accounts.Scope,
    <span class="hljs-symbol">assign_key:</span> <span class="hljs-symbol">:current_scope</span>,
    <span class="hljs-symbol">access_path:</span> [<span class="hljs-symbol">:user</span>, <span class="hljs-symbol">:id</span>],
    <span class="hljs-symbol">schema_key:</span> <span class="hljs-symbol">:user_id</span>,
    <span class="hljs-symbol">schema_type:</span> <span class="hljs-symbol">:id</span>,
    <span class="hljs-symbol">schema_table:</span> <span class="hljs-symbol">:users</span>,
    <span class="hljs-symbol">test_data_fixture:</span> Myapp.AccountsFixtures,
    <span class="hljs-symbol">test_login_helper:</span> <span class="hljs-symbol">:register_and_log_in_user</span>
  ]

config <span class="hljs-symbol">:myapp</span>,
  <span class="hljs-symbol">ecto_repos:</span> [Myapp.Repo],
  <span class="hljs-symbol">generators:</span> [<span class="hljs-symbol">timestamp_type:</span> <span class="hljs-symbol">:utc_datetime</span>]

config <span class="hljs-symbol">:myapp</span>, MyappWeb.Endpoint,
  <span class="hljs-symbol">adapter:</span> Bandit.PhoenixAdapter,
  <span class="hljs-symbol">render_errors:</span> [
    <span class="hljs-symbol">formats:</span> [<span class="hljs-symbol">html:</span> MyappWeb.ErrorHTML, <span class="hljs-symbol">json:</span> MyappWeb.ErrorJSON],
    <span class="hljs-symbol">layout:</span> <span class="hljs-keyword">false</span>
  ],
  <span class="hljs-symbol">pubsub_server:</span> Myapp.PubSub,
  <span class="hljs-symbol">live_view:</span> [<span class="hljs-symbol">signing_salt:</span> <span class="hljs-string">"GkZs+o4a"</span>],
  <span class="hljs-symbol">code_reloader:</span> config_env() == <span class="hljs-symbol">:dev</span>,
  <span class="hljs-symbol">debug_errors:</span> config_env() == <span class="hljs-symbol">:dev</span>,
  <span class="hljs-symbol">check_origin:</span> config_env() == <span class="hljs-symbol">:prod</span>

config <span class="hljs-symbol">:esbuild</span>,
  <span class="hljs-symbol">version:</span> <span class="hljs-string">"0.17.11"</span>,
  <span class="hljs-symbol">myapp:</span> [
    <span class="hljs-symbol">args:</span>
      <span class="hljs-string">~w(js/app.js --bundle --target=es2022 --outdir=../priv/static/assets/js --external:/fonts/* --external:/images/*)</span>,
    <span class="hljs-symbol">cd:</span> Path.expand(<span class="hljs-string">"../assets"</span>, __DIR__),
    <span class="hljs-symbol">env:</span> %{<span class="hljs-string">"NODE_PATH"</span> =&gt; Path.expand(<span class="hljs-string">"../deps"</span>, __DIR__)}
  ]

config <span class="hljs-symbol">:tailwind</span>,
  <span class="hljs-symbol">version:</span> <span class="hljs-string">"4.0.9"</span>,
  <span class="hljs-symbol">myapp:</span> [
    <span class="hljs-symbol">args:</span> <span class="hljs-string">~w(
      --input=assets/css/app.css
      --output=priv/static/assets/css/app.css
    )</span>,
    <span class="hljs-symbol">cd:</span> Path.expand(<span class="hljs-string">".."</span>, __DIR__)
  ]

config <span class="hljs-symbol">:logger</span>, <span class="hljs-symbol">:default_formatter</span>,
  <span class="hljs-symbol">format:</span> <span class="hljs-string">"$time $metadata[$level] $message\n"</span>,
  <span class="hljs-symbol">metadata:</span> [<span class="hljs-symbol">:request_id</span>]

config <span class="hljs-symbol">:phoenix</span>, <span class="hljs-symbol">:json_library</span>, Jason

if config_env() == <span class="hljs-symbol">:test</span> <span class="hljs-keyword">do</span>
    config <span class="hljs-symbol">:bcrypt_elixir</span>, <span class="hljs-symbol">:log_rounds</span>, <span class="hljs-number">1</span>

    config <span class="hljs-symbol">:myapp</span>, Myapp.Repo,
        <span class="hljs-symbol">pool:</span> Ecto.Adapters.SQL.Sandbox,
        <span class="hljs-symbol">pool_size:</span> System.schedulers_online() * <span class="hljs-number">2</span>

    config <span class="hljs-symbol">:logger</span>, <span class="hljs-symbol">level:</span> <span class="hljs-symbol">:warning</span>

    config <span class="hljs-symbol">:phoenix</span>, <span class="hljs-symbol">:plug_init_mode</span>, <span class="hljs-symbol">:runtime</span>

    config <span class="hljs-symbol">:phoenix_live_view</span>, <span class="hljs-symbol">enable_expensive_runtime_checks:</span> <span class="hljs-keyword">true</span>
<span class="hljs-keyword">end</span>

if config_env() == <span class="hljs-symbol">:dev</span> <span class="hljs-keyword">do</span>
config <span class="hljs-symbol">:myapp</span>, MyappWeb.Endpoint,
  <span class="hljs-symbol">watchers:</span> [
    <span class="hljs-symbol">esbuild:</span> {Esbuild, <span class="hljs-symbol">:install_and_run</span>, [<span class="hljs-symbol">:myapp</span>, <span class="hljs-string">~w(--sourcemap=inline --watch)</span>]},
    <span class="hljs-symbol">tailwind:</span> {Tailwind, <span class="hljs-symbol">:install_and_run</span>, [<span class="hljs-symbol">:myapp</span>, <span class="hljs-string">~w(--watch)</span>]}
  ],
  <span class="hljs-symbol">live_reload:</span> [
    <span class="hljs-symbol">web_console_logger:</span> <span class="hljs-keyword">true</span>,
    <span class="hljs-symbol">patterns:</span> [
      <span class="hljs-string">~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$"</span>,
      <span class="hljs-string">~r"priv/gettext/.*(po)$"</span>,
      <span class="hljs-string">~r"lib/myapp_web/(controllers|live|components)/.*(ex|heex)$"</span>
    ]
  ]

config <span class="hljs-symbol">:myapp</span>, <span class="hljs-symbol">dev_routes:</span> <span class="hljs-keyword">true</span>

config <span class="hljs-symbol">:logger</span>, <span class="hljs-symbol">:default_formatter</span>, <span class="hljs-symbol">format:</span> <span class="hljs-string">"[$level] $message\n"</span>

config <span class="hljs-symbol">:phoenix</span>, <span class="hljs-symbol">:stacktrace_depth</span>, <span class="hljs-number">20</span>

config <span class="hljs-symbol">:phoenix</span>, <span class="hljs-symbol">:plug_init_mode</span>, <span class="hljs-symbol">:runtime</span>

config <span class="hljs-symbol">:phoenix_live_view</span>,
  <span class="hljs-symbol">debug_heex_annotations:</span> <span class="hljs-keyword">true</span>,
  <span class="hljs-symbol">enable_expensive_runtime_checks:</span> <span class="hljs-keyword">true</span>

<span class="hljs-keyword">end</span>

if config_env() == <span class="hljs-symbol">:prod</span> <span class="hljs-keyword">do</span>
    config <span class="hljs-symbol">:logger</span>, <span class="hljs-symbol">level:</span> <span class="hljs-symbol">:info</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h3 id="heading-new-runtimeexs-config-file">New <code>runtime.exs</code> config file</h3>
<pre><code class="lang-elixir"><span class="hljs-keyword">import</span> Config

database_username =
  System.get_env(<span class="hljs-string">"DATABASE_USERNAME"</span>) ||
    raise <span class="hljs-string">"""
    environment variable DATABASE_USERNAME is missing.
    For example: postgress
    """</span>

database_password =
  System.get_env(<span class="hljs-string">"DATABASE_PASSWORD"</span>) ||
    raise <span class="hljs-string">"""
    environment variable DATABASE_PASSWORD is missing.
    For example: postgress
    """</span>

database_hostname =
  System.get_env(<span class="hljs-string">"DATABASE_HOSTNAME"</span>) ||
    raise <span class="hljs-string">"""
    environment variable DATABASE_HOSTNAME is missing.
    For example: localhost
    """</span>

database_database =
  System.get_env(<span class="hljs-string">"DATABASE_DATABASE"</span>) ||
    raise <span class="hljs-string">"""
    environment variable DATABASE_DATABASE is missing.
    For example: myapp
    """</span>

secret_key_base =
  System.get_env(<span class="hljs-string">"SECRET_KEY_BASE"</span>) ||
    raise <span class="hljs-string">"""
    environment variable SECRET_KEY_BASE is missing.
    You can generate one by calling: mix phx.gen.secret
    """</span>

endpoint_url_host =
  System.get_env(<span class="hljs-string">"ENDPOINT_URL_HOST"</span>) ||
    raise <span class="hljs-string">"""
    environment variable ENDPOINT_URL_HOST is missing.
    For example: "example.com"
    """</span>

endpoint_url_port =
  System.get_env(<span class="hljs-string">"ENDPOINT_URL_PORT"</span>) ||
    raise <span class="hljs-string">"""
    environment variable ENDPOINT_URL_PORT is missing.
    For example: "80"
    """</span>

endpoint_http_port =
  System.get_env(<span class="hljs-string">"PORT"</span>) ||
    raise <span class="hljs-string">"""
    environment variable PORT is missing.
    For example: "4000"
    """</span>

config <span class="hljs-symbol">:myapp</span>, Myapp.Repo,
  <span class="hljs-symbol">username:</span> database_username,
  <span class="hljs-symbol">password:</span> database_password,
  <span class="hljs-symbol">hostname:</span> database_hostname,
  <span class="hljs-symbol">database:</span> database_database,
  <span class="hljs-symbol">stacktrace:</span> config_env() == <span class="hljs-symbol">:dev</span>,
  <span class="hljs-symbol">show_sensitive_data_on_connection_error:</span> config_env() == <span class="hljs-symbol">:dev</span>,
  <span class="hljs-symbol">pool_size:</span> String.to_integer(System.get_env(<span class="hljs-string">"POOL_SIZE"</span>) || <span class="hljs-string">"10"</span>),
  <span class="hljs-symbol">socket_options:</span> if(System.get_env(<span class="hljs-string">"ECTO_IPV6"</span>) <span class="hljs-keyword">in</span> <span class="hljs-string">~w(true 1)</span>, <span class="hljs-symbol">do:</span> [<span class="hljs-symbol">:inet6</span>], <span class="hljs-symbol">else:</span> []),

if config_env() == <span class="hljs-symbol">:test</span> <span class="hljs-keyword">do</span>
  config <span class="hljs-symbol">:myapp</span>, Myapp.Repo,
    <span class="hljs-symbol">database:</span> <span class="hljs-string">"<span class="hljs-subst">#{database_database}</span><span class="hljs-subst">#{System.get_env(<span class="hljs-string">"MIX_TEST_PARTITION"</span>)}</span>"</span>,
    <span class="hljs-symbol">pool:</span> Ecto.Adapters.SQL.Sandbox,
    <span class="hljs-symbol">pool_size:</span> System.schedulers_online() * <span class="hljs-number">2</span>
<span class="hljs-keyword">end</span>

config <span class="hljs-symbol">:myapp</span>, MyappWeb.Endpoint,
  <span class="hljs-symbol">url:</span> [
    <span class="hljs-symbol">host:</span> endpoint_url_host,
    <span class="hljs-symbol">port:</span> String.to_integer(endpoint_url_port),
    <span class="hljs-symbol">scheme:</span> if(config_env() == <span class="hljs-symbol">:prod</span>, <span class="hljs-symbol">do:</span> <span class="hljs-string">"https"</span>, <span class="hljs-symbol">else:</span> <span class="hljs-string">"http"</span>)
  ],
  <span class="hljs-symbol">http:</span> [
    <span class="hljs-symbol">port:</span> String.to_integer(endpoint_http_port),
    <span class="hljs-symbol">ip:</span> if(config_env() == <span class="hljs-symbol">:prod</span>, <span class="hljs-symbol">do:</span> {0, 0, 0, 0, 0, 0, 0, 0}, <span class="hljs-symbol">else:</span> {<span class="hljs-number">127</span>, 0, 0, <span class="hljs-number">1</span>})
  ],
  <span class="hljs-symbol">secret_key_base:</span> secret_key_base,
  <span class="hljs-symbol">server:</span> String.to_existing_atom(System.get_env(<span class="hljs-string">"PHX_SERVER"</span>, <span class="hljs-string">"false"</span>))

swooch_adapter =
  <span class="hljs-keyword">case</span> config_env() <span class="hljs-keyword">do</span>
    <span class="hljs-symbol">:dev</span> -&gt; Swoosh.Adapters.Local
    <span class="hljs-comment"># In test we don't send emails.</span>
    <span class="hljs-symbol">:test</span> -&gt; Swoosh.Adapters.Test
    <span class="hljs-symbol">:prod</span> -&gt; Swoosh.Adapters.MailGun
  <span class="hljs-keyword">end</span>

config <span class="hljs-symbol">:myapp</span>, Myapp.Mailer,
  <span class="hljs-symbol">adapter:</span> Swoosh.Adapters.Test,
  <span class="hljs-symbol">api_client:</span> <span class="hljs-keyword">false</span>,
  <span class="hljs-symbol">local:</span> <span class="hljs-keyword">true</span>

if config_env() == <span class="hljs-symbol">:dev</span> <span class="hljs-keyword">do</span>
  config <span class="hljs-symbol">:myapp</span>, Myapp.Mailer, <span class="hljs-symbol">adapter:</span> Swoosh.Adapters.Local
<span class="hljs-keyword">end</span>

if config_env() == <span class="hljs-symbol">:prod</span> <span class="hljs-keyword">do</span>
  config <span class="hljs-symbol">:myapp</span>, Myapp.Mailer,
    <span class="hljs-symbol">api_key:</span> System.get_env(<span class="hljs-string">"MAILGUN_API_KEY"</span>),
    <span class="hljs-symbol">domain:</span> System.get_env(<span class="hljs-string">"MAILGUN_DOMAIN"</span>),
    <span class="hljs-symbol">local:</span> <span class="hljs-keyword">false</span>,
    <span class="hljs-symbol">api_client:</span> Swoosh.ApiClient.Req

  config <span class="hljs-symbol">:myapp</span>, <span class="hljs-symbol">:dns_cluster_query</span>, System.get_env(<span class="hljs-string">"DNS_CLUSTER_QUERY"</span>)
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-setting-environmental-variables">Setting environmental variables</h2>
<p>The configuration has been changed to use environmental variables, this is not a problem in production where it is usually recommended to use in the <a target="_blank" href="https://12factor.net/config">12 factor</a>. However in development and testing this can be a problem since it is not easy to set the environmental variables in the operation system. To simply this we will update the config to make use <code>.env</code> files.</p>
<p>To do this we will need to add an external dependency <a target="_blank" href="https://hexdocs.pm/dotenvy">Dotenvy</a>:</p>
<pre><code class="lang-elixir"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">deps</span></span> <span class="hljs-keyword">do</span>
  [
    {<span class="hljs-symbol">:dotenvy</span>, <span class="hljs-string">"~&gt; 1.0.0"</span>}
  ]
<span class="hljs-keyword">end</span>
</code></pre>
<p>Then run <code>mix deps.get</code></p>
<p>The <code>config/runtime.exs</code> file is updated to include the following block at the beginning:</p>
<pre><code class="lang-elixir"><span class="hljs-comment"># config/runtime.exs</span>
<span class="hljs-keyword">import</span> Config
<span class="hljs-keyword">import</span> Dotenvy

env_dir_prefix = System.get_env(<span class="hljs-string">"RELEASE_ROOT"</span>) || Path.expand(<span class="hljs-string">"./envs"</span>)

source!([
  Path.absname(<span class="hljs-string">".env"</span>, env_dir_prefix),
  Path.absname(<span class="hljs-string">".<span class="hljs-subst">#{config_env()}</span>.env"</span>, env_dir_prefix),
  Path.absname(<span class="hljs-string">".<span class="hljs-subst">#{config_env()}</span>.overrides.env"</span>, env_dir_prefix),
  System.get_env()
])
</code></pre>
<p>The above example would include the <code>envs/.env</code> file for shared/default configuration values and then leverage environment-specific <code>.env</code> files (e.g. <code>envs/.dev.env</code>) to provide environment-specific values. An <code>envs/.{MIX_ENV}.overrides.env</code> file would be referenced in the <code>.gitignore</code> file to allow developers to override any values a file that is not under version control. System environment variables are given final say over the values via <code>System.get_env()</code>.</p>
<p><code>Dotenvy.env!/2</code> is used to read the given <code>variable</code> and convert its value to a given type. This function will raise an error if the given variable does not exist. </p>
<p>Updated <code>config/runtime.exs</code> file:</p>
<pre><code class="lang-elixir">config <span class="hljs-symbol">:myapp</span>, Myapp.Repo,
  <span class="hljs-symbol">username:</span> env!(<span class="hljs-string">"DATABASE_USERNAME"</span>),
  <span class="hljs-symbol">password:</span> env!(<span class="hljs-string">"DATABASE_PASSWORD"</span>),
  <span class="hljs-symbol">hostname:</span> env!(<span class="hljs-string">"DATABASE_HOSTNAME"</span>),
  <span class="hljs-symbol">database:</span> env!(<span class="hljs-string">"DATABASE_DATABASE"</span>),
  <span class="hljs-symbol">stacktrace:</span> config_env() == <span class="hljs-symbol">:dev</span>,
  <span class="hljs-symbol">show_sensitive_data_on_connection_error:</span> config_env() == <span class="hljs-symbol">:dev</span>,
  <span class="hljs-symbol">pool_size:</span> env!(<span class="hljs-string">"POOL_SIZE"</span>, <span class="hljs-symbol">:integer</span>, <span class="hljs-number">10</span>),
  <span class="hljs-symbol">socket_options:</span> if(env!(<span class="hljs-string">"ECTO_IPV6"</span>) <span class="hljs-keyword">in</span> <span class="hljs-string">~w(true 1)</span>, <span class="hljs-symbol">do:</span> [<span class="hljs-symbol">:inet6</span>], <span class="hljs-symbol">else:</span> [])

if config_env() == <span class="hljs-symbol">:test</span> <span class="hljs-keyword">do</span>
  config <span class="hljs-symbol">:myapp</span>, Myapp.Repo,
    <span class="hljs-symbol">database:</span> <span class="hljs-string">"<span class="hljs-subst">#{env!(<span class="hljs-string">"DATABASE_DATABASE"</span>)}</span><span class="hljs-subst">#{env!(<span class="hljs-string">"MIX_TEST_PARTITION"</span>, <span class="hljs-symbol">:integer</span>, <span class="hljs-number">1</span>)}</span>"</span>,
    <span class="hljs-symbol">pool:</span> Ecto.Adapters.SQL.Sandbox,
    <span class="hljs-symbol">pool_size:</span> System.schedulers_online() * <span class="hljs-number">2</span>
<span class="hljs-keyword">end</span>

config <span class="hljs-symbol">:myapp</span>, MyappWeb.Endpoint,
  <span class="hljs-symbol">url:</span> [
    <span class="hljs-symbol">host:</span> env!(<span class="hljs-string">"ENDPOINT_URL_HOST"</span>),
    <span class="hljs-symbol">port:</span> env!(<span class="hljs-string">"ENDPOINT_URL_PORT"</span>, <span class="hljs-symbol">:integer</span>),
    <span class="hljs-symbol">scheme:</span> if(config_env() == <span class="hljs-symbol">:prod</span>, <span class="hljs-symbol">do:</span> <span class="hljs-string">"https"</span>, <span class="hljs-symbol">else:</span> <span class="hljs-string">"http"</span>)
  ],
  <span class="hljs-symbol">http:</span> [
    <span class="hljs-symbol">port:</span> env!(<span class="hljs-string">"PORT"</span>, <span class="hljs-symbol">:integer</span>),
    <span class="hljs-symbol">ip:</span> if(config_env() == <span class="hljs-symbol">:prod</span>, <span class="hljs-symbol">do:</span> {0, 0, 0, 0, 0, 0, 0, 0}, <span class="hljs-symbol">else:</span> {<span class="hljs-number">127</span>, 0, 0, <span class="hljs-number">1</span>})
  ],
  <span class="hljs-symbol">secret_key_base:</span> env!(<span class="hljs-string">"SECRET_KEY_BASE"</span>),
  <span class="hljs-symbol">server:</span> env!(<span class="hljs-string">"PHX_SERVER"</span>, <span class="hljs-symbol">:boolean</span>, <span class="hljs-keyword">false</span>)


config <span class="hljs-symbol">:myapp</span>, Myapp.Mailer,
  <span class="hljs-symbol">adapter:</span> Swoosh.Adapters.Test,
  <span class="hljs-symbol">api_client:</span> <span class="hljs-keyword">false</span>,
  <span class="hljs-symbol">local:</span> <span class="hljs-keyword">true</span>

if config_env() == <span class="hljs-symbol">:dev</span> <span class="hljs-keyword">do</span>
  config <span class="hljs-symbol">:myapp</span>, Myapp.Mailer, <span class="hljs-symbol">adapter:</span> Swoosh.Adapters.Local
<span class="hljs-keyword">end</span>

if config_env() == <span class="hljs-symbol">:prod</span> <span class="hljs-keyword">do</span>
  config <span class="hljs-symbol">:myapp</span>, Myapp.Mailer,
    <span class="hljs-symbol">adapter:</span> Swoosh.Adapters.MailGun,
    <span class="hljs-symbol">api_key:</span> env!(<span class="hljs-string">"MAILGUN_API_KEY"</span>),
    <span class="hljs-symbol">domain:</span> env!(<span class="hljs-string">"MAILGUN_DOMAIN"</span>),
    <span class="hljs-symbol">local:</span> <span class="hljs-keyword">false</span>,
    <span class="hljs-symbol">api_client:</span> Swoosh.ApiClient.Req

  config <span class="hljs-symbol">:myapp</span>, <span class="hljs-symbol">:dns_cluster_query</span>, env!(<span class="hljs-string">"DNS_CLUSTER_QUERY"</span>)
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-example-env-file">Example <code>.env</code> file</h2>
<pre><code class="lang-env"># Database Configuration
DATABASE_USERNAME=postgres
DATABASE_PASSWORD=postgres
DATABASE_HOSTNAME=localhost
DATABASE_DATABASE=myapp_dev
POOL_SIZE=10
ECTO_IPV6=false

# Phoenix Endpoint
PHX_SERVER=false
ENDPOINT_URL_HOST=localhost
ENDPOINT_URL_PORT=80
PORT=4000
SECRET_KEY_BASE=your_secret_key_here
PHX_SERVER=true

# Mailer (for production)
MAILGUN_API_KEY=your-mailgun-api-key
MAILGUN_DOMAIN=your-mailgun-domain.com

# Optional clustering support
DNS_CLUSTER_QUERY=myapp.local

# Testing (optional for test partitioning)
MIX_TEST_PARTITION=1
</code></pre>
<h1 id="heading-conclusion">Conclusion</h1>
<p>By consolidating configuration into just two files, using runtime variables, and leveraging .env support with Dotenvy, your Elixir app becomes:</p>
<ul>
<li>Easier to manage across environments</li>
<li>CI/CD-friendly and immutable-build ready</li>
<li>Aligned with 12-factor and containerized deployment standards</li>
</ul>
<p>To view the code here is the <a target="_blank" href="https://github.com/walu-lila/blog_demo/pull/2">PR for the code changes</a></p>
]]></content:encoded></item><item><title><![CDATA[Adding material symbols to a Phoenix project]]></title><description><![CDATA[Using Material Symbols with Phoenix and Tailwind CSS
Material Symbols is the variable font version of Google’s Material Icons. Unlike traditional icon fonts, variable fonts allow a single file to contain multiple stylistic variations—significantly re...]]></description><link>https://ambrosemungai.com/adding-material-symbols-to-a-phoenix-project</link><guid isPermaLink="true">https://ambrosemungai.com/adding-material-symbols-to-a-phoenix-project</guid><category><![CDATA[Tailwind CSS]]></category><category><![CDATA[Phoenix framework]]></category><category><![CDATA[material ui]]></category><category><![CDATA[Elixir]]></category><dc:creator><![CDATA[Ambrose Mungai]]></dc:creator><pubDate>Tue, 20 May 2025 12:48:30 GMT</pubDate><content:encoded><![CDATA[<h1 id="heading-using-material-symbols-with-phoenix-and-tailwind-css">Using Material Symbols with Phoenix and Tailwind CSS</h1>
<p><a target="_blank" href="https://fonts.google.com/icons">Material Symbols</a> is the variable font version of Google’s Material Icons. Unlike traditional icon fonts, variable fonts allow a single file to contain multiple stylistic variations—significantly reducing the number of assets you need to manage.</p>
<p>Material Symbols replaces the now-deprecated Material Icons fonts and is actively maintained with regular updates.</p>
<h2 id="heading-why-use-material-symbols-in-phoenix">Why Use Material Symbols in Phoenix?</h2>
<p>By default, new Phoenix projects include a set of built-in components, including an <code>icon/1</code> component that renders <a target="_blank" href="https://heroicons.com">Heroicons</a>. While Heroicons are elegant and often sufficient, Material Symbols offer a far larger collection. They're widely adopted by UI designers, which means many mockups and design systems use them by default.</p>
<p>To preserve visual fidelity with the design, substituting icons between sets (like Heroicons and Material Symbols) is not ideal. Having Material Symbols available directly in your Phoenix app makes it easier to match designs precisely.</p>
<h2 id="heading-material-symbols-overview">Material Symbols Overview</h2>
<p>Material Symbols are based on 24px icons and come in three styles:</p>
<ul>
<li><strong>Outlined</strong></li>
<li><strong>Rounded</strong></li>
<li><strong>Sharp</strong></li>
</ul>
<p>These are variable fonts with four axes of customization:</p>
<ul>
<li><strong>Optical Size</strong>: Default is <code>24</code></li>
<li><strong>Weight</strong>: Default is <code>400</code> (Regular)</li>
<li><strong>Grade</strong>: Default is <code>0</code></li>
<li><strong>Fill</strong>: Default is <code>0</code></li>
</ul>
<p>For simplicity, we'll use the default values and only include the <code>400</code> weight icons.</p>
<h2 id="heading-installing-material-symbols-via-dependency">Installing Material Symbols via Dependency</h2>
<p>To avoid manually downloading and maintaining icon files, we can pull them directly from GitHub using a sparse checkout in <code>mix.exs</code>. This setup fetches only the necessary <code>svg/400</code> folder, avoids compiling the dependency, and reduces disk usage by limiting Git history.</p>
<p>Add the following to your <code>mix.exs</code> dependencies:</p>
<pre><code class="lang-elixir">{<span class="hljs-symbol">:material_icons</span>,
  <span class="hljs-symbol">github:</span> <span class="hljs-string">"marella/material-symbols"</span>,
  <span class="hljs-symbol">sparse:</span> <span class="hljs-string">"svg/400"</span>,
  <span class="hljs-symbol">app:</span> <span class="hljs-keyword">false</span>,
  <span class="hljs-symbol">compile:</span> <span class="hljs-keyword">false</span>,
  <span class="hljs-symbol">depth:</span> <span class="hljs-number">1</span>
}
</code></pre>
<p>Then run:</p>
<pre><code class="lang-bash">mix deps.get
</code></pre>
<h2 id="heading-extending-the-icon1-component">Extending the icon/1 Component</h2>
<p>Phoenix comes with a built-in <code>icon/1</code> component for Heroicons. We’ll add support for Material Symbols by introducing a new clause that matches icon names with a <code>material-</code> prefix.</p>
<p>In core_components.ex, add this function:</p>
<pre><code class="lang-elixir"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">icon</span></span>(%{<span class="hljs-symbol">name:</span> <span class="hljs-string">"material-"</span> &lt;&gt; _} = assigns) <span class="hljs-keyword">do</span>
  <span class="hljs-string">~H"""
  &lt;span class={[@name, @class]} aria-hidden="</span><span class="hljs-keyword">true</span><span class="hljs-string">" {@rest} /&gt;
  "</span><span class="hljs-string">""</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>This allows you to use Material Symbols like so:</p>
<pre><code class="lang-elixir">&lt;.icon name=<span class="hljs-string">"material-home"</span> /&gt;
</code></pre>
<h2 id="heading-configuring-tailwind-to-recognize-material-symbols">Configuring Tailwind to Recognize Material Symbols</h2>
<p>Next, we need Tailwind to generate CSS classes for the Material Symbols. We’ll scan the SVG files in the deps/material_icons directory and create utility classes based on their filenames.</p>
<p>Update your tailwind.config.js with the following plugin:</p>
<pre><code class="lang-js">plugin(<span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">{ matchComponents, theme }</span>) </span>{
  <span class="hljs-keyword">const</span> path = <span class="hljs-built_in">require</span>(<span class="hljs-string">"path"</span>);
  <span class="hljs-keyword">const</span> fs = <span class="hljs-built_in">require</span>(<span class="hljs-string">"fs"</span>);

  <span class="hljs-keyword">const</span> iconsDir = path.join(__dirname, <span class="hljs-string">"../deps/material_icons/svg/400"</span>);
  <span class="hljs-keyword">const</span> values = {};

  <span class="hljs-keyword">const</span> styles = [
    [<span class="hljs-string">""</span>, <span class="hljs-string">"outlined"</span>],
    [<span class="hljs-string">"_rounded"</span>, <span class="hljs-string">"rounded"</span>],
    [<span class="hljs-string">"_sharp"</span>, <span class="hljs-string">"sharp"</span>],
  ];

  styles.forEach(<span class="hljs-function">(<span class="hljs-params">[suffix, dir]</span>) =&gt;</span> {
    fs.readdirSync(path.join(iconsDir, dir)).forEach(<span class="hljs-function"><span class="hljs-params">file</span> =&gt;</span> {
      <span class="hljs-keyword">let</span> name = path.basename(file, <span class="hljs-string">".svg"</span>) + suffix;
      name = name.replaceAll(<span class="hljs-string">"_"</span>, <span class="hljs-string">"-"</span>);

      values[name] = {
        name,
        <span class="hljs-attr">fullPath</span>: path.join(iconsDir, dir, file),
      };
    });
  });

  matchComponents({
    <span class="hljs-string">"material"</span>: <span class="hljs-function">(<span class="hljs-params">{ name, fullPath }</span>) =&gt;</span> {
      <span class="hljs-keyword">let</span> content = fs.readFileSync(fullPath, <span class="hljs-string">"utf8"</span>).replace(<span class="hljs-regexp">/\r?\n|\r/g</span>, <span class="hljs-string">""</span>);
      content = content.replace(<span class="hljs-string">' width="48" height="48"'</span>, <span class="hljs-string">""</span>);

      <span class="hljs-keyword">const</span> size = theme(<span class="hljs-string">"spacing.6"</span>);

      <span class="hljs-keyword">return</span> {
        [<span class="hljs-string">`--material-<span class="hljs-subst">${name}</span>`</span>]: <span class="hljs-string">`url('data:image/svg+xml;utf8,<span class="hljs-subst">${content}</span>')`</span>,
        <span class="hljs-string">"-webkit-mask"</span>: <span class="hljs-string">`var(--material-<span class="hljs-subst">${name}</span>)`</span>,
        <span class="hljs-string">"mask"</span>: <span class="hljs-string">`var(--material-<span class="hljs-subst">${name}</span>)`</span>,
        <span class="hljs-string">"mask-repeat"</span>: <span class="hljs-string">"no-repeat"</span>,
        <span class="hljs-string">"background-color"</span>: <span class="hljs-string">"currentColor"</span>,
        <span class="hljs-string">"vertical-align"</span>: <span class="hljs-string">"middle"</span>,
        <span class="hljs-string">"display"</span>: <span class="hljs-string">"inline-block"</span>,
        <span class="hljs-string">"width"</span>: size,
        <span class="hljs-string">"height"</span>: size,
      };
    }
  }, { values });
})
</code></pre>
<p>With this setup, Tailwind will generate classes like material-home based on your icons. You can then use them as utility classes in your components.</p>
<h2 id="heading-keeping-icons-up-to-date">Keeping Icons Up to Date</h2>
<p>Since the Material Symbols repository is frequently updated via GitHub Actions, your app can stay current with new icons by simply running:</p>
<pre><code class="lang-bash">mix deps.update material_icons
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Integrating Material Symbols with Phoenix and Tailwind gives you access to a vast and regularly updated icon set that aligns with modern UI design standards. By automating the dependency, icon rendering, and Tailwind class generation, you streamline both development and design fidelity—ensuring your front end looks just like the mockups.</p>
]]></content:encoded></item></channel></rss>