TL;DR
Fusion lets you run Elixir code on any server with SSH access and Elixir installed. No deployment, no release building. Connect via SSH, push your modules automatically, execute remotely. This is Part 1 of a 3-part series.
What Fusion Does
I wrote Fusion to answer a question: what if you could run Elixir code on a remote server with nothing but SSH access?
No deployment pipeline. No pre-installed application. Just SSH and an Elixir installation on the other end. Fusion connects to the remote machine, bootstraps a BEAM node, and ships your compiled bytecode over. Your local code runs remotely.
Set Up the Project
Scaffold a new Elixir project:
mix new fusion_demo
cd fusion_demo
Add Fusion to mix.exs:
defp deps do
[
{:fusion, github: "elpddev/fusion"}
]
end
Fetch dependencies:
mix deps.get
Prerequisites
If using password-based SSH auth, install sshpass on your local machine. Fusion uses it to pass passwords to the ssh command:
# Arch Linux
sudo pacman -S sshpass
# Ubuntu/Debian
sudo apt-get install sshpass
# macOS
brew install sshpass
Alternatively, use SSH key auth and skip this step.
Set Up a Remote Target
We need a machine with Elixir and SSH. A Docker container works perfectly for local testing.
Create Dockerfile:
FROM elixir:1.19-otp-28
RUN apt-get update && apt-get install -y openssh-server && \
mkdir -p /var/run/sshd && \
echo 'root:fusion' | chpasswd && \
sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config && \
sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config
EXPOSE 22
CMD ["/usr/sbin/sshd", "-D"]
Match your local Elixir version. Fusion pushes compiled BEAM bytecode, not source code. If the remote runs a different Elixir version, structs like Regex may have a different internal layout, causing function_clause errors at runtime. Check your version with elixir --version and use the matching Docker image.
Create docker-compose.yml:
services:
remote:
build: .
ports:
- "2222:22"
Start it:
docker compose up -d
You now have an Elixir-capable SSH server on port 2222.
Connect and Run
Start your local node as distributed. Fusion needs this to use Erlang distribution:
iex --sname myapp@localhost -S mix
Inside iex, connect to the remote:
target = %Fusion.Target{
host: "localhost",
port: 2222,
username: "root",
auth: {:password, "fusion"}
}
{:ok, manager} = Fusion.NodeManager.start_link(target)
{:ok, remote_node} = Fusion.NodeManager.connect(manager)
That’s it. You have a live connection. Run something:
{:ok, {hostname, _}} = Fusion.run(remote_node, System, :cmd, ["hostname", []])
IO.puts("Remote hostname: #{hostname}")
Run More Commands
# What OS is the remote running?
{:ok, {uname, _}} = Fusion.run(remote_node, System, :cmd, ["uname", ["-a"]])
# How much memory?
{:ok, {meminfo, _}} = Fusion.run(remote_node, System, :cmd, ["cat", ["/proc/meminfo"]])
# What's the Elixir version?
{:ok, version} = Fusion.run(remote_node, System, :version, [])
Every call goes through SSH tunnels and executes on the remote BEAM node. The results come back to your local iex session.
Push Your Own Module
Running stdlib functions is useful. Running your own code is the point.
Create lib/remote_health.ex:
defmodule RemoteHealth do
def check do
%{
hostname: hostname(),
elixir_version: System.version(),
otp_release: System.otp_release(),
memory: memory_mb(),
uptime: uptime(),
beam_processes: length(Process.list())
}
end
defp hostname do
{name, _} = System.cmd("hostname", [])
String.trim(name)
end
defp memory_mb do
{meminfo, _} = System.cmd("cat", ["/proc/meminfo"])
meminfo
|> String.split("\n")
|> Enum.find(&String.starts_with?(&1, "MemTotal"))
|> String.split(~r/\s+/)
|> Enum.at(1)
|> String.to_integer()
|> div(1024)
end
defp uptime do
{uptime_str, _} = System.cmd("cat", ["/proc/uptime"])
uptime_str
|> String.split(" ")
|> hd()
|> String.to_float()
|> trunc()
end
end
Recompile and run it remotely:
recompile()
{:ok, health} = Fusion.run(remote_node, RemoteHealth, :check, [])
IO.inspect(health, label: "Remote health")
Output:
Remote health: %{
hostname: "a1b2c3d4e5f6",
elixir_version: "1.18.4",
otp_release: "28",
memory: 16024,
uptime: 3421,
beam_processes: 58
}
You didn’t install RemoteHealth on the remote. Fusion pushed the compiled bytecode automatically before executing.
Interactive Exploration
This is where it gets interesting. In your iex session, you can run anything:
# Anonymous functions work too - Fusion pushes the defining module
{:ok, files} = Fusion.run_fun(remote_node, fn ->
File.ls!("/etc")
|> Enum.filter(&String.ends_with?(&1, ".conf"))
end)
# Check what Elixir modules are available on the remote
{:ok, loaded} = Fusion.run(remote_node, :code, :all_loaded, [])
IO.puts("#{length(loaded)} modules loaded on remote")
# Run Elixir code that uses multiple modules
{:ok, result} = Fusion.run_fun(remote_node, fn ->
System.cmd("df", ["-h"])
|> elem(0)
|> String.split("\n")
|> Enum.map(&String.split(&1, ~r/\s+/))
|> Enum.reject(&(length(&1) < 6))
|> Enum.map(fn [fs, size, used, avail, pct | _] ->
%{filesystem: fs, size: size, used: used, available: avail, use_percent: pct}
end)
end)
It feels like SSH, but you’re writing Elixir. Pattern matching, pipes, structs - all available on the remote.
Disconnect
When you’re done:
Fusion.NodeManager.disconnect(manager)
docker compose down
What’s Next
This works. But how?
Fusion sets up three SSH tunnels to trick the remote BEAM node into joining your local Erlang cluster. Then it reads your compiled BEAM files to figure out which modules to ship.
Part 2 covers the tunnel architecture - how Erlang distribution works and how Fusion bends it over SSH.