slouchpie

slouchpie

Using :dns_cluster with docker-compose locally (it can be done)

Warmest greetings, comrades.

I recently started using :dns_cluster (GitHub - phoenixframework/dns_cluster: Simple DNS clustering for distributed Elixir nodes · GitHub) in a couple of production projects which are deployed on Fly. These all suffer from various “distributed” problems (not because of dns_cluster, just because of normal distributed problems). I have been trying to emulate the setup locally with minimal setup for new devs. I did not want to use a different clustering lib in dev (e.g. libcluster) because I think these kind of differences between prod and dev make debugging almost impossible.

So I have a working project using docker-compose where I have 2 instances of the same Phoenix application with static IP addresses on a custom docker network and with node long-names of form app_name@ip_address. This mimics the standard long-names seen on fly.io machines.

I thought about writing a blog post about it but time is very precious these days. I am already overloaded. So I am posting this here, hopefully with adequate tags and keywords to be found by someone searching.

Broad strokes what to do:

2 services in docker-compose.override.yaml

name: myapp
services:
  myapp1: &myapp_mapping
    ports:
      - "4000:4000"
    env_file: ../docker/myapp/dev.env
    environment:
      IP_V4_ADDRESS: 192.0.1.11
    volumes:
      - ../assets:/app/assets
      - ../config:/app/config:ro
      - ../lib:/app/lib:ro
      - ../priv:/app/priv
      - ../seeds:/app/seeds:ro
      - ../test:/app/test:ro
      - ../mix.exs:/app/mix.exs:ro
      - ../mix.lock:/app/mix.lock:ro
      - ../.iex.exs:/app/.iex.exs:ro
    networks:
      erlcluster:
        ipv4_address: 192.0.1.11

  myapp2:
    <<: *myapp_mapping
    ports:
      - "4001:4000"
    environment:
      IP_V4_ADDRESS: 192.0.1.12
    networks:
      erlcluster:
        ipv4_address: 192.0.1.12

networks:
  erlcluster:
    ipam:
      driver: default
      config:
        - subnet: "192.0.1.0/24"

Note a custom network is created using IP range reserved for documentation and testing purposes. The volumes is a trick to get hot-reloading.

Use the ipv4_address in your docker entrypoint, e.g.

iex --name myapp@$1 --cookie $2 -S mix phx.server

the cookie is in the dev.env file.

Note both nodes have same shortname (“myapp”) but different hostnames (the ipv4 address).

So that is it in broad strokes.

The next trick is to write a custom DNS resolver module for DNSCluster, like this:

defmodule MyApp.DevDNSClusterResolver do
  @moduledoc false

  require Record

  Record.defrecord(:hostent, Record.extract(:hostent, from_lib: "kernel/include/inet.hrl"))

  def basename(node_name) when is_atom(node_name) do
    [basename, _] = String.split(to_string(node_name), "@")
    basename
  end

  def connect_node(node_name) when is_atom(node_name), do: Node.connect(node_name)

  def list_nodes, do: Node.list(:visible)

  def lookup(query, type) when is_binary(query) and type in [:a, :aaaa] do
    query
    |> String.split()
    |> Enum.reduce([], fn query, acc ->
      case :inet_res.getbyname(~c"#{query}", type) do
        {:ok, hostent(h_addr_list: addr_list)} -> addr_list ++ acc
        {:error, _} -> acc
      end
    end)
  end
end

This is almost identical to the normal DNSCluster.Resolver module except for the lookup function which does String.split. This hack allows us to specify multiple hosts to check for IP addresses. Note we differ from fly.io deployment here because we don’t have an overlay network (they have, e.g. myapp.internal or something like that). That’s why we need multiple calls to :inet_res.getbyname.

This is actually a good thing because we can choose which server will serve our requests since they map to different ports. Server 1 is at localhost:4000, server 2 is at localhost:4001.

Finally, you need to have this env var in, e.g dev.env:

DNS_CLUSTER_QUERY="myapp1 myapp2"

As mentioned, this will be split by our custom resolver and both IP v4 addresses are obtained.

So that’s how I got it to work. I am testing using :pogo for global singletons, regional singletons, etc. and it is nice to have 2 clustered apps by default.
It is especially comforting to my mind that they are clustering using the same lib as prod. Like I said, I can choose which server to manually test on (ports 4000 vs 4001) and I can attach to whatever running server I want.

This is not a neat write-up but there is enough info for anyone else who wants this kind of local setup for dns_cluster clustering with docker-compose. I am happy to provide more info.

Most Liked

mxgrn

mxgrn

From my further digging into it (there’s a part in a talk by Chris McCord on dns_cluster, btw), dns_cluster is a simpler dependency, but both solve the same problem, so, I don’t see why use both. The libcluster lib appears more extensible. E.g., someone wrote Postgres node discovery strategy for it, which is pretty cool.

LostKobrakai

LostKobrakai

No you don’t. Libcluster has a strategy for a list of static nodes matching what OTP does iirc.

epmd does port mapping. It has nothing to do with hostname resolution. It just deal with multiple erlang nodes on a single host and mapping them to ports as well as informing other nodes what the selected ports are.

It does. OTP added the ability to disable epmd in favor of a hardcoded port, but that’s not a default. You need to opt into that.

Where Next?

Popular in Guides/Tuts Top

thetechnologyvault
One of our team members just published this getting started guide for Elixir/Phoenix devs to use the Nanobox platform: https://content.n...
New
dgamidov
Hi folks, Just a short instruction. Maybe it will help somebody. Install boostrap with deps and Sass: cd assets npm install jquery ...
New
niku
I write an article Parameterized testing with ExUnit.The key concept is using ExUnit.Case.register_test/4 such as ExUnit.start() defmod...
New
anuragg
Hi everyone, I’m the founder of Render and we just released a guide to deploying Phoenix apps with Mix releases. Most of it is generali...
New
Jskalc
Sorry if it’s a common knowledge, but it’s something I just learned and wanted to share. I’ve seen that mind-blowing demo of hot-reload ...
New
mudasobwa
The post covering how to generate nifty types to use in @spec in compile time with macros.
New
zazaian
I recently generated the boilerplate for a new mix app using the Phoenix 1.3.1 phx.new generator and realized that the structure of the w...
New
ob1
Recently we partitioned a table with 28 million rows by its ULID to improve query speeds and reduce query timeouts. To do this I ended up...
New
KoviRobi
Hi, I’ve written the following to debug function calls, not sure if it’s useful for anyone else, and if so should I put it somewhere? i...
New
dogweather
I just finished a long process of configuring and debugging a Docker Compose-based dev environment. Several late-night hours! I think I’v...
New

Other popular topics Top

senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
johnnyicon
Hi all, I’ve just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I’m trying to use Postgres...
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
New
KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 36128 110
New
komlanvi
Hi everyone, I was playing with phoenix liveView but I run into an issue. I have a form and want to validate each input text when the te...
New
hariharasudhan94
Lets say I have map like this fetching from my database %{"_id" =&gt; #BSON.ObjectId&lt;58eb1a7a9ad169198c3dXXXX&gt;, "email" =&gt; ...
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New

We're in Beta

About us Mission Statement