Erlang distribution over Unix domain sockets via the :socket module. Lets Elixir and Erlang nodes connect to each other without opening TCP listeners — useful for releases that need a local remote shell but should not expose distribution over the network.

Filesystem-backed sockets work on any Unix-like platform. Linux additionally supports abstract namespace sockets that live in kernel state rather than on disk.

Requirements

OTP 26 or newer. Erlang distribution protocol version 6 only.

Installation

def deps do
  [{:uds_dist, "~> 1.0"}]
end

Configuration

Every node — the listening release and any client connecting in — needs the same -proto_dist and EPMD-bypass flags:

-proto_dist uds
-no_epmd

Path resolution is driven by :socket_dir in the :uds_dist application environment. The part of the node name before @ becomes the socket file's name within that directory.

# config/runtime.exs
config :uds_dist, socket_dir: "/run/myapp"

A node named myapp@host then listens at /run/myapp/myapp.sock. Anything after @ is ignored — all traffic is local, so the host part is conventional only.

If socket_dir is omitted the default is ".", meaning sockets are created relative to the BEAM's working directory. Convenient for ad-hoc testing; not recommended for releases.

Abstract namespace sockets (Linux only)

A socket_dir value beginning with @ selects the Linux abstract namespace. Abstract sockets have no filesystem entry, no permission bits, and are cleaned up by the kernel when their owner exits.

config :uds_dist, socket_dir: "@myapp"

A node named myapp@host then listens at the abstract path \0myapp/myapp.

Configuring an abstract socket_dir on a non-Linux platform raises at listen/1 or setup/5 time. There is no automatic fallback.

Listen backlog

The kernel listen backlog defaults to 5 and can be overridden:

config :uds_dist, backlog: 128

The value is read at listen/1 time, so setting it in config/runtime.exs of a release is sufficient.

Release integration

rel/vm.args.eex:

-proto_dist uds
-no_epmd

rel/remote.vm.args.eex — used by bin/<rel> remote:

-proto_dist uds
-no_epmd
-dist_listen false

-dist_listen false tells net_kernel to call address/0 instead of listen/1, so the remote shell does not create its own socket file.

Set socket_dir in config/runtime.exs:

import Config
config :uds_dist, socket_dir: "/run/#{System.get_env("RELEASE_NAME", "myapp")}"

Make sure the directory exists with the right ownership before the release boots. rel/env.sh.eex is a good place:

mkdir -p "/run/${RELEASE_NAME}"

After deployment bin/<rel> remote opens a shell via the UDS instead of TCP, with no other changes needed.

Local development (iex -S mix)

The BEAM processes -proto_dist before Mix has set up the dependency code path, so net_kernel cannot find uds_dist if you start distribution at boot from a Mix project. Pass -pa explicitly to fix it:

iex \
  --erl "-pa _build/dev/lib/uds_dist/ebin -proto_dist uds -no_epmd" \
  --sname server -S mix

Set the socket directory for the dev environment so both nodes can find each other:

# config/config.exs
import Config

if Mix.env() == :dev do
  config :uds_dist, socket_dir: Path.join(System.tmp_dir!(), "uds_dist_dev")
end

Start a second node the same way with a different --sname and run Node.connect/1 from either side. Releases do not need this workaround — the boot script populates the code path before -proto_dist is consulted.

How it works

uds_dist implements the seven callbacks an Erlang distribution module must export (listen/1, accept/1, accept_connection/5, setup/5, close/1, select/1, address/0) against the :socket NIF rather than gen_tcp. EPMD is bypassed entirely: setup/5 derives the target's socket path from the node name plus the configured socket_dir, so no registry is needed.

Post-handshake each connection has three processes:

  • Output handler — sole writer, also handles distribution ticks
  • Input handler — greedy reader; pulls available bytes from the kernel buffer and parses length-prefixed frames out of the accumulator
  • Connection supervisor — supplied by dist_util

Length prefixes are hand-rolled in Erlang since :socket has no {packet, N} mode: 2 bytes during handshake, 4 bytes after.

The implementation is modelled on OTP's lib/kernel/examples/erl_uds_dist example. Notable differences from that reference: :socket instead of gen_tcp, distribution protocol version 6 only, abstract namespace support, and socket-path resolution driven by application config rather than by the bare node name.

License

MIT. See LICENSE.