rogach

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 steps needed, in hopes that it will help someone.

So, in order to make hot-reloadable release you need:

  1. Write an appup file (the Erlang’s Appup cookbook is a great resource for that: Appup Cookbook — Erlang System Documentation v29.0.2).
  2. Tweak the release generated by mix release, by copying the .rel & .appup files into expected places and generating the relup file. This can be done by hand, but I’ve wrote a simple function that can do that right inside the mix release task:
def project do
  [
    releases: [
      app_name: [
        include_executables_for: [:unix],
        steps: [:assemble, &release_fixup_step/1, :tar],
      ],
    ],
    ...
  ]
end

defp release_fixup_step(release) do
  releases_dir = Path.join(release.path, "releases")
  rel_file = Path.join([releases_dir, release.version, "#{release.name}.rel"])
  :ok = :release_handler.create_RELEASES(releases_dir, rel_file, [])

  # Yes, we have three copies of a same file in the same place.
  # This is necessary to appease the release_handler.
  File.cp!(rel_file, Path.join([releases_dir, release.version, "#{release.name}-#{release.version}.rel"]))
  File.cp!(rel_file, Path.join([releases_dir, "#{release.name}-#{release.version}.rel"]))

  # Copy appup file into the correct location and generate the relup.
  appup_file = Path.join("appups", "#{release.version}.appup")
  if File.exists?(Path.join("appups", "#{release.version}.appup")) do
    File.cp!(
      appup_file,
      Path.join([release.path, "lib", "#{release.name}-#{release.version}", "ebin", "#{release.name}.appup"])
    )

    {:ok, appup} = :file.consult(appup_file)
    [{appup_release_version, [{version_up_from, _}], [{version_down_to, _}]}] = appup

    if appup_release_version != to_charlist(release.version) do
      raise "Unexpected version in appup: #{appup_release_version} (expected #{release.version})"
    end

    if version_up_from != version_down_to do
      raise "Unexpected versions in appup instructions: #{version_up_from} != #{version_down_to}"
    end

    :systools.make_relup(
      ~c"#{release.name}-#{release.version}",
      [~c"#{release.name}-#{version_up_from}"],
      [~c"#{release.name}-#{version_down_to}"],
      path: [to_charlist(Path.join(release.path, "releases/*")), to_charlist(Path.join(release.path, "lib/*/ebin"))],
      outdir: to_charlist(release.version_path)
    )
  end

  release
end

Now, after running mix release you will get a release archive in _build/<env>/app_name-<version>.tar.gz.

  1. Copy this archive into the releases/ directory of your running release.
  2. Connect to the Iex shell of the running release.
  3. Carefully run the commands:
:release_handler.unpack_release(~c"app_name-<version>")
:release_handler.install_release(~c"<version>")
:release_handler.make_permanent(~c"<version>")

Showing Posts 1 to 3

defman

defman

I’d like to explain a bit more about “appeasing the release_handler” because I’ve spent some time this night on this (I was interested for why we need 3 copies of the rel file).

The rel file in _build/.../rel/releases/.../ (/releases/.../ in the tarball) is required by :systoos.make_relup.
path: [Dir] in the call is where make_relup searches for rel, app and appup files to generate the relup.
mix release already creates a rel file named release.name in that folder, but make_relup will search for release.name-release.version (Name.rel and Name2.rel in make_relup(Name, [Name2], [Name2]): systools — OTP 29.1 (sasl 4.4)).
So it finds rel files in releases/*/.../ and app and appup files in lib/*/ebin/, for example:

:systools.make_relup(
  ~c"my_project-1.2.0",
  [~c"my_project-1.1.0"],
  [~c"my_project-1.1.0"],
  path: [~c".../_build/dev/rel/my_project/releases/*"]
)
# Will fail with: 
# my_project: No valid version ("1.1.0") of .app file found. Found file ".../_build/dev/lib/my_project/ebin/my_project.app" with version "1.2.0"

:systools.make_relup(
  ~c"my_project-1.2.0",
  [~c"my_project-1.1.0"],
  [~c"my_project-1.1.0"],
  path: [~c".../_build/dev/rel/my_project/lib/*/ebin"]
)
# Will fail with: File not found: "my_project-1.2.0.rel"

The rel file in _build/.../rel/releases/ (/releases/ in the tarball) is required by :release_handler.unpack_release. Without it, unpacking will fail:

:release_handler.unpack_release(~c"my_project-1.2.0")
{:error,
 {:enoent,
  ~c".../_build/dev/rel/my_project/releases/elixir_learning-1.2.0.rel"}}

This happens because unpack_release tries to extract releases/{release-name}.rel from the tarball:

Then it tries to check the rel file:

and check_rel calls consult(..., false) which is defined as

You can go as much as:

  1. Rename the rel file generated by :assemble from releases/#{release.version}/#{release.name}.erl to releases/#{release.version}/#{release.name}-{release.version}.erl for :systools.make_relup
  2. Generate the relup file
  3. Move the erl to releases/ for :release_handler.unpack_release
  4. Create tarball
  5. Move the erl from releases/ back to releases/#{release.version} (this is needed for your next release, make_relup searches for previous release rel file in releases/*/ :slight_smile: )

Here’s my final solution:

  def project do
    [
      ...
      releases: [
        app_name: [
          include_executables_for: [:unix],
          steps: [:assemble, &release_fixup_step/1, :tar, &move_rel/1]
        ]
      ]
    ]
  end

  defp release_fixup_step(release) do
    releases_dir = Path.join(release.path, "releases")
    rel_name = "#{release.name}-#{release.version}.rel"
    rel_file = Path.join([release.version_path, rel_name])
    # Step 1: rename non-versioned rel to versioned rel for :systools.make_relup
    File.rename!(Path.join([release.version_path, "#{release.name}.rel"]), rel_file)
    :ok = :release_handler.create_RELEASES(releases_dir, rel_file, [])

    appup_file = Path.join("appups", "#{release.version}.appup")

    if File.exists?(Path.join("appups", "#{release.version}.appup")) do
      File.cp!(
        appup_file,
        Path.join([
          release.path,
          "lib",
          "#{release.name}-#{release.version}",
          "ebin",
          "#{release.name}.appup"
        ])
      )

      {:ok, appup} = :file.consult(appup_file)
      [{appup_release_version, [{version_up_from, _}], [{version_down_to, _}]}] = appup

      if appup_release_version != to_charlist(release.version) do
        raise "Unexpected version in appup: #{appup_release_version} (expected #{release.version})"
      end

      if version_up_from != version_down_to do
        raise "Unexpected versions in appup instructions: #{version_up_from} != #{version_down_to}"
      end

      :systools.make_relup(
        ~c"#{release.name}-#{release.version}",
        [~c"#{release.name}-#{version_up_from}"],
        [~c"#{release.name}-#{version_down_to}"],
        path: [
          to_charlist(Path.join(release.path, "releases/*")),
          to_charlist(Path.join(release.path, "lib/*/ebin"))
        ],
        outdir: to_charlist(release.version_path)
      )
    end

    # Step 2: move rel from releases/#release.version/ to releases/ for :release_handler.unpack_release
    File.rename!(rel_file, Path.join([releases_dir, rel_name]))

    release
  end

  defp move_rel(release) do
    # Step 3: move rel back from releases/ to releases/#release.version/ for :systools.make_relup in the next release
    Path.join([release.path, "releases", "#{release.name}-#{release.version}.rel"])
    |> File.rename!(Path.join([release.version_path, "#{release.name}-#{release.version}.rel"]))

    release
  end

This will produce a tar.gz release without rel being duplicated 3 times in releases/version/ (app_name.rel and app_name-version.rel) and in releases/ (app_name-version.rel):

.
├── bin
│   └── my_project
├── erts-17.0.6
│   └── bin
│       ├── beam.smp
│       ├── ct_run
│       ├── dialyzer
│       ├── epmd
│       ├── erl
│       ├── erl_call
│       ├── erl_child_setup
│       ├── erl.src
│       ├── erlc
│       ├── erlexec
│       ├── escript
│       ├── heart
│       ├── inet_gethost
│       ├── run_erl
│       ├── start
│       ├── start_erl.src
│       ├── start.src
│       ├── to_erl
│       ├── typer
│       └── yielding_c_fun
├── lib
│   ├── compiler-10.0.4
│   │   └── ebin
│   ├── my_project-1.2.0
│   │   └── ebin
│   ├── elixir-1.20.4
│   │   └── ebin
│   ├── iex-1.20.4
│   │   └── ebin
│   ├── kernel-11.0.3
│   │   ├── ebin
│   │   └── include
│   ├── logger-1.20.4
│   │   └── ebin
│   ├── sasl-4.4
│   │   └── ebin
│   └── stdlib-8.0.4
│       ├── ebin
│       └── include
└── releases
    ├── 1.2.0
    │   ├── consolidated
    │   ├── elixir
    │   ├── env.sh
    │   ├── iex
    │   ├── relup
    │   ├── remote.vm.args
    │   ├── start_clean.boot
    │   ├── start_clean.script
    │   ├── start.boot
    │   ├── start.script
    │   ├── sys.config
    │   └── vm.args
    ├── COOKIE
    ├── my_project-1.2.0.rel
    ├── RELEASES
    └── start_erl.data
rogach

rogach OP

Nice! Thank you for your work on improving this.

defman

defman

You can also add releases/ to make_relup path and it will find rel for current release and unpack_release will also be happy :slight_smile: So rel would be used like that: releases/version/app_name.rel -> releases/app_name-version.rel -> create relup -> create tarball -> releases/version/app_name-version.rel (for next release)

rel_file = Path.join([releases_dir, "#{release.name}-#{release.version}.rel"])
File.rename!(Path.join([release.version_path, "#{release.name}.rel"]), rel_file)

...

:systools.make_relup(
  ~c"#{release.name}-#{release.version}",
  [~c"#{release.name}-#{version_up_from}"],
  [~c"#{release.name}-#{version_down_to}"],
  path: [
    to_charlist(Path.join(release.path, "releases/")),
    to_charlist(Path.join(release.path, "releases/*")),
    to_charlist(Path.join(release.path, "lib/*/ebin"))
  ],
  outdir: to_charlist(release.version_path)
)
— All posts loaded —

Where Next? Top

Trending in Guides/Tuts Top

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

Other Trending Topics Top

GenericJam
Edit: 2026 May 15 - This post is archived. Mob is alive!! Main docs: mob v0.7.11 — Documentation A bit of explanation for the slightly c...
New
JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
New
budgie
A little off-topic, but I feel like people here have a good head on their shoulders. I used to be quite good at making software. Was luc...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews