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

malloryerik
Hi, this is for people who, like me, have had some friction using .html.heex templates in VSCode. The solution seems to be, in a hyphena...
New
wfgilman
I’m writing up this quick “How to” because what I thought was going to be an easy implementation of a Plug to validate a webhook request ...
New
WolfDan
So my main OS is Windows, I do must of my work with it, Elixir and vscode elixirls works just fine when you’re working only with elixir, ...
New
dkuhlman
For those of you who might be interested in using ZeroMQ in Elixir, I converted the Erlang examples in the ZeroMQ “zguide” to Elixir. I ...
New
berts-4865
Here is a quick guide to uploading a file from the browser to DO spaces. It is crude, but will hopefully save sometime time and frustrat...
New
f0rest8
Hi, TLDR: form attribute set on the input fields and button submit. I just wanted to share a solution I discovered when making live inl...
New
drapermd
So here is the code I came up with to generically generate an array param that will be stored on a jsonb property in ecto. It only handl...
New
rogach
I’ve spent some time understanding how to do hot code reloading with releases built using mix release, and here I’d like to detail the st...
New
njwest
In the process of developing a Phx-based multiplayer experience, I found myself with so many browser tabs open with Elixir gaming resourc...
New
marcin
This post is intended to be a guide for others, I was running a remote debugger for the first time and appreciate feedback on how I could...
New

Other popular topics Top

sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
malloryerik
Hi, this is for people who, like me, have had some friction using .html.heex templates in VSCode. The solution seems to be, in a hyphena...
New
msaraiva
Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative...
564 43622 214
New
JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1144 53690 245
New
gausby
I asked this very same question on twitter and got some interesting feedback, but I thought it would be a good question to ask here as we...
1207 39297 209
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 records...
New
marick
I had some trouble figuring out how to make many-to-many associations work. Once I got it working, I wrote a blog post. Because I’m a nov...
New
Qqwy
Update: How to use the Blogs &amp; Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3271 126479 1222
New

We're in Beta

About us Mission Statement