Remote task runner using Erlang distribution over SSH. Zero dependencies.
Fusion connects to remote servers via SSH, sets up port tunnels for Erlang distribution, bootstraps a remote BEAM node, and lets you run Elixir code on it. Think Ansible/Chef but for Elixir - push modules and execute functions on remote machines without pre-installing your application.
- Elixir ~> 1.18 / OTP 28+
- Remote server with Elixir/Erlang installed
- SSH access (key-based or password via
sshpass)
Add fusion to your list of dependencies in mix.exs:
def deps do
[
{:fusion, github: "elpddev/fusion"}
]
endYour local BEAM must be started as a distributed node:
iex --sname myapp@localhost -S mixThen connect and run code remotely:
# Define the target
target = %Fusion.Target{
host: "10.0.1.5",
port: 22,
username: "deploy",
auth: {:key, "~/.ssh/id_ed25519"}
}
# Connect (sets up tunnels, bootstraps remote BEAM, joins cluster)
{:ok, manager} = Fusion.NodeManager.start_link(target)
{:ok, remote_node} = Fusion.NodeManager.connect(manager)
# Run code remotely (MFA form)
{:ok, 3} = Fusion.run(remote_node, Kernel, :+, [1, 2])
# Run system commands on the remote
{:ok, {hostname, 0}} = Fusion.run(remote_node, System, :cmd, ["hostname", []])
# Push and run your own modules (dependencies are resolved automatically)
{:ok, result} = Fusion.run(remote_node, MyApp.Worker, :process, [data])
# Disconnect and clean up
Fusion.NodeManager.disconnect(manager)When you run MyApp.Worker remotely, Fusion automatically pushes all project modules that Worker references (struct usage, function calls, etc.). You don't need to manually track the dependency chain.
# This pushes MyApp.Worker AND any project modules it depends on
{:ok, result} = Fusion.run(remote_node, MyApp.Worker, :do_work, [])
# You can also push explicitly
Fusion.TaskRunner.push_module(remote_node, MyApp.Worker)
Fusion.TaskRunner.push_modules(remote_node, [MyApp.Config, MyApp.Utils])Standard library modules (Kernel, Enum, String, etc.) are already on the remote and don't need pushing.
Fusion creates 3 SSH tunnels between local and remote:
Local Machine Remote Server
───────────── ─────────────
┌─── Reverse ────┐
Local node port ◄┘ tunnel #1 └── Remote can reach local node
┌─── Forward ────┐
localhost:port ──┘ tunnel #2 └► Remote node's dist port
┌─── Reverse ────┐
Local EPMD ◄─┘ tunnel #3 └── Remote registers with local EPMD
(port 4369)
Starts Elixir on the remote via SSH with carefully configured flags:
ERL_EPMD_PORT=<tunneled>- routes EPMD registration through tunnel #3 back to local EPMD--sname worker@localhost- uses@localhostbecause all traffic goes through localhost-bound tunnels--cookie <local_cookie>- matches the local cluster's cookie--erl "-kernel inet_dist_listen_min/max <port>"- pins distribution port to match tunnel #2
Since the remote registered with the local EPMD, Node.connect/1 works as if the remote node were local. All distribution traffic is routed through the SSH tunnels.
Module bytecode is transferred via Erlang distribution:
- Read
.beambinary locally with:code.get_object_code/1 - Parse BEAM atoms table to find non-stdlib dependencies
- Push each dependency recursively (bottom-up)
- Load on remote with
:code.load_binary/3 - Execute via
:erpc.call/4
# Unit tests (no external dependencies)
mix test
# Docker integration tests (requires Docker)
cd test/docker && ./run.sh start
elixir --sname fusion_test@localhost -S mix test --include external
# Stop the test container
cd test/docker && ./run.sh stop- Tier 1 (Unit) - Doctests and pure logic tests. No network, no SSH.
- Tier 2 (Integration) - Tests against localhost SSH. Skips gracefully if not configured.
- Tier 3 (External) - End-to-end tests against a Docker container with SSH + Elixir. Requires
./run.sh start.
Fusion (public API)
├── TaskRunner - Remote code execution + module pushing + dependency resolution
├── NodeManager - GenServer: tunnel setup, BEAM bootstrap, connection lifecycle
├── Target - SSH connection configuration struct
├── TunnelSupervisor - DynamicSupervisor for tunnel processes
├── Net - Port generation, EPMD utilities
├── Connector - SSH connection GenServer
├── SshPortTunnel - SSH port tunnel process wrapper
├── PortRelay - Port relay process wrapper
├── UdpTunnel - UDP tunnel process wrapper
└── Utilities
├── Ssh - SSH command string generation
├── Exec - OS process execution (Port/System.cmd)
├── Erl - Erlang CLI command builder
└── Bash/Socat/Netcat/Netstat/Telnet - CLI tool wrappers
MIT