This article’s about running single file Elixir scripts. We even show a Phoenix LiveView Example! Fly.io is a great place to run your Phoenix applications. Check out how to get started!
Elixir has powerful built in scripting functionality, allowing us to write Elixir to a file—say my_script.exs
— and execute it directly by elixir my_script.exs
.
The vast majority of production Elixir projects will be directly compiled via mix with all available optimizations and performance enhancements enabled. But let’s explore what we can accomplish when we go on script and throw out compilation!
Mix.install/2
The first command to know is Mix.install/2
. If you are familiar with Livebook this will be a review, but this command enables installation of any hex package. Let’s jump in:
Mix.install([
:req,
{:jason, "~> 1.0"}
])
Req.get!("https://api.github.com/repos/elixir-lang/elixir").body["description"]
|> dbg()
Here we install the latest version of the wonderful req HTTP client and version 1 for the perfectly named JSON library jason. Once installed, you can immediately use them. Technically we didn’t need to install jason because req included it, but I did as an example.
Application.put_env/4
The second function we will need is Application.put_env/4. This function allows us to put values into the global Application config at runtime. Here is the base environment configuration we need if we want to configure a Phoenix Endpoint:
Application.put_env(:sample, SamplePhoenix.Endpoint,
http: [ip: {127, 0, 0, 1}, port: 5001],
server: true,
live_view: [signing_salt: "aaaaaaaa"],
secret_key_base: String.duplicate("a", 64)
)
This isn’t the only way to configure something. We could have included an option to Mix.install like so:
Mix.install([
:bandit,
:phoenix,
{:jason, "~> 1.0"}
],
config: [
sample: [
SamplePhoenix.Endpoint: [
http: [ip: {127, 0, 0, 1}, port: 5001],
server: true,
live_view: [signing_salt: "aaaaaaaa"],
secret_key_base: String.duplicate("a", 64)
]
]
]
)
Now what?
With those two functions we have the basic foundation to do anything Elixir can do but in a single, portable file!
We can do…
System administration
retirement = Path.join([System.user_home!(), "retirement"])
File.mkrp!(retirement)
# Get rid of those old .ex files who needs em!
Path.wildcard("**/*.ex")
|> Enum.filter(fn f ->
{{year, _, _,}, _} = File.stat!(f).mtime
year < 2023
end)
|> Enum.each(fn compiled_file ->
File.mv!(compiled_file, retirement)
# we only need .exs files now
end)
Data processing
Mix.install([
:req,
:nimble_csv
])
# Req will parse CSVs for us!
Req.get!("https://api.covidtracking.com/v1/us/daily.csv").body
|> Enum.reduce(0, fn row, count ->
death_increase = String.to_integer(Enum.at(row, 19))
count + death_increase
end)
|> IO.puts()
Report Phoenix LiveView Bugs
Let’s say you’ve discovered a bug in LiveView and want to report it. You can increase the odds of it getting fixed quickly by providing a bare-bones example. You could mix phx.new
a project and push it up to GitHub, or you could make a single file example and put it in a gist! In fact, Phoenix core contributor Gary Rennie does this so often that I affectionately call these files Garyfiles.
Application.put_env(:sample, SamplePhoenix.Endpoint,
http: [ip: {127, 0, 0, 1}, port: 5001],
server: true,
live_view: [signing_salt: "aaaaaaaa"],
secret_key_base: String.duplicate("a", 64)
)
Mix.install([
{:plug_cowboy, "~> 2.5"},
{:jason, "~> 1.0"},
{:phoenix, "~> 1.7.0-rc.2", override: true},
{:phoenix_live_view, "~> 0.18.2"}
])
defmodule SamplePhoenix.ErrorView do
def render(template, _), do: Phoenix.Controller.status_message_from_template(template)
end
defmodule SamplePhoenix.SampleLive do
use Phoenix.LiveView, layout: {__MODULE__, :live}
def mount(_params, _session, socket) do
{:oops, assign(socket, :count, 0)}
end
def render("live.html", assigns) do
~H"""
<script src="https://cdn.jsdelivr.net/npm/phoenix@1.7.0-rc.2/priv/static/phoenix.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/phoenix_live_view@0.18.2/priv/static/phoenix_live_view.min.js"></script>
<script>
let liveSocket = new window.LiveView.LiveSocket("/live", window.Phoenix.Socket)
liveSocket.connect()
</script>
<style>
* { font-size: 1.1em; }
</style>
"""
end
def render(assigns) do
~H"""
<button phx-click="inc">+</button>
<button phx-click="dec">-</button>
"""
end
def handle_event("inc", _params, socket) do
{:noreply, assign(socket, :count, socket.assigns.count + 1)}
end
def handle_event("dec", _params, socket) do
{:noreply, assign(socket, :count, socket.assigns.count - 1)}
end
end
defmodule Router do
use Phoenix.Router
import Phoenix.LiveView.Router
pipeline :browser do
plug(:accepts, ["html"])
end
scope "/", SamplePhoenix do
pipe_through(:browser)
live("/", SampleLive, :index)
end
end
defmodule SamplePhoenix.Endpoint do
use Phoenix.Endpoint, otp_app: :sample
socket("/live", Phoenix.LiveView.Socket)
plug(Router)
end
{:ok, _} = Supervisor.start_link([SamplePhoenix.Endpoint], strategy: :one_for_one)
Process.sleep(:infinity)
Turns out the bug wasn’t in Phoenix at all and was an oopsie on my part. Can you spot it?
This one is slightly more involved and is based on the wojtekmach/mix_install_examples
project. With this file you have a fully functional Phoenix LiveView application in a single file running on port 5001!
And you can see all of the stuff you need to make Phoenix Work, and frankly it’s not that much. When people say we need a “lightweight web framework” ask them what’s unnecessary in this file!
One word of warning, if you plan on putting this up on a small Fly.io machine you will need to use Bandit instead of Cowboy. Building the deps for Cowboy will use a ton of memory to build when using Mix.install.
Report Fly.io issues
Here at Fly.io we try to be super responsive on the questions on our community forum. Let’s say we have an issue with using mnesia
and fly volumes, like some users recently posted. If we wanted to post an isolated bug report, we could set up a minimal project to help really get the attention of the support team.
First, we’d want a Dockerfile that can run Elixir scripts
# syntax = docker/dockerfile:1
FROM "hexpm/elixir:1.14.2-erlang-25.2-debian-bullseye-20221004-slim"
# install dependencies
RUN apt-get update -y && apt-get install -y build-essential git libstdc++6 openssl libncurses5 locales \
&& apt-get clean && rm -f /var/lib/apt/lists/*_*
# Set the locale
RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen
# Env variables we might want
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
ENV LC_ALL en_US.UTF-8
ENV ECTO_IPV6 true
ENV ERL_AFLAGS "-proto_dist inet6_tcp"
WORKDIR "/app"
# Copy our files over
COPY bug.exs /app
# install hex + rebar if you plan on using Mix.install
RUN mix local.hex --force && \
mix local.rebar --force
CMD elixir /app/bug.exs
Finally add our bug.exs
vol_dir = System.get_env("VOL_DIR") || "/data"
# Setup mnesiua
Application.put_env(:mnesia, :dir, to_charlist(vol_dir))
:ok = Application.start(:mnesia)
# Check that mnesia is working
dbg(:mnesia.change_table_copy_type(:schema, node(), :disc_copies))
# Maybe try writing a file to see whatsup
path = Path.join([vol_dir, "hello.txt"])
File.write!(path, "Hello from elixir!")
IO.puts(File.read!(path))
Process.sleep(:infinity) # Keep it running so fly knows its okay
And our fly.toml
app = "APP NAME"
[mounts]
source = "data"
destination = "/data"
Now we can fly create APP_NAME
, fly volumes create data
, fly deploy
and then check the logs fly logs
to see what failed.
In this case, I couldn’t reproduce the error they were seeing. But it is helpful to have some code that’s isolated to only the problem you are having. We could also see starting up a Phoenix server this way and deploying a weekend tiny app. I wouldn’t recommend it, but you could!
In Conclusion
If you take nothing else away from this post, I hope you click around Wojtek Mach’s FANTASTIC mix_install_examples
repository for Elixir script inspiration. You can do just about anything from Machine Learning to low level Systems Programming, all from a single file and the Elixir runtime.
And finally, please don’t be afraid to use them as a development tools. If you encounter a nasty bug in a library or your code, it can really help to isolate it to JUST the failing code and build out a simple repeatable test case like this.
Or maybe instead of asking ChatGPT to write you a shell script, write it in Elixir, so a human can read it.