tutorial

How to Monitor Bakeware (Elixir) with Vigilmon

Learn how to add health checks, process liveness monitoring, and alerts to Elixir applications distributed as Bakeware self-contained binaries with Vigilmon.

How to Monitor Bakeware (Elixir) with Vigilmon

Bakeware is an Elixir library that compresses Mix releases into single self-contained executable binaries. The resulting file bundles the Erlang runtime, BEAM bytecode, and all dependencies — no Erlang or Elixir installation required on the target machine. Bakeware binaries boot in under a second and can be deployed as sidecar CLIs, microservices, or background daemons anywhere a Linux or macOS binary can run.

But the deployment simplicity that makes Bakeware binaries attractive also creates new operational blind spots. A binary that crashes silently on a server produces no immediate alert. A distributed CLI run across a fleet may fail on some nodes but not others. An HTTP microservice packed into a Bakeware binary still needs external verification that it is accepting requests after deployment. These failures demand active monitoring.

This tutorial adds production observability to an Elixir application distributed as a Bakeware binary:

  • An HTTP health check embedded in the binary's server component
  • HTTP uptime monitoring with Vigilmon
  • Process liveness heartbeat for long-running binary processes
  • Deployment verification heartbeat confirming the binary started successfully
  • Alerts on downtime and missed heartbeats

Step 1: Add Bakeware to your project

# mix.exs
defp deps do
  [
    {:bakeware, "~> 0.2", runtime: false},
    {:plug_cowboy, "~> 2.0"},
    {:plug, "~> 1.14"},
    {:jason, "~> 1.4"},
    {:req, "~> 0.4"}
  ]
end

Configure the release to use Bakeware:

# mix.exs
def project do
  [
    app: :my_app,
    version: "1.0.0",
    releases: [
      my_app: [
        steps: [:assemble, &Bakeware.assemble/1],
        bakeware: [compression_level: 19]
      ]
    ]
  ]
end

Step 2: Build and run the binary

# Build the binary
MIX_ENV=prod mix release

# The binary is produced at:
# _build/prod/rel/bakeware/my_app

# Run it
./_build/prod/rel/bakeware/my_app start

For HTTP service binaries, the application starts its Cowboy/Bandit server exactly as it would from a standard Mix release.


Step 3: Add a health check endpoint to your binary's HTTP server

If your Bakeware binary runs an HTTP service, add a health plug:

# lib/my_app_web/plugs/health_check.ex
defmodule MyAppWeb.Plugs.HealthCheck do
  import Plug.Conn

  def init(opts), do: opts

  def call(%Plug.Conn{request_path: "/health"} = conn, _opts) do
    checks = %{
      vm: check_vm(),
      uptime: uptime_seconds()
    }

    status = if checks.vm == :ok, do: 200, else: 503

    conn
    |> put_resp_content_type("application/json")
    |> send_resp(status, Jason.encode!(%{
      status: (if status == 200, do: "ok", else: "degraded"),
      checks: checks,
      version: Application.spec(:my_app, :vsn) |> to_string(),
      node: to_string(node())
    }))
    |> halt()
  end

  def call(conn, _opts), do: conn

  defp check_vm do
    # Verify the BEAM VM is responsive by running a trivial call
    _ = :erlang.system_info(:process_count)
    :ok
  rescue
    _ -> :error
  end

  defp uptime_seconds do
    {uptime_ms, _} = :erlang.statistics(:wall_clock)
    div(uptime_ms, 1000)
  end
end

Mount it in your endpoint:

# lib/my_app_web/endpoint.ex
defmodule MyAppWeb.Endpoint do
  use Phoenix.Endpoint, otp_app: :my_app

  plug MyAppWeb.Plugs.HealthCheck
  plug MyAppWeb.Router
end

Test it after running the binary:

curl http://localhost:4000/health
# {"status":"ok","checks":{"vm":"ok","uptime":42},"version":"1.0.0","node":"my_app@hostname"}

Step 4: Send a startup heartbeat on binary launch

For any Bakeware binary — HTTP service or daemon — ping a Vigilmon heartbeat monitor as soon as the application is fully initialized:

# lib/my_app/startup_reporter.ex
defmodule MyApp.StartupReporter do
  use GenServer
  require Logger

  def start_link(_), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)

  @impl true
  def init(state) do
    # Report after all children are ready (defer to let supervisor finish)
    send(self(), :report_startup)
    {:ok, state}
  end

  @impl true
  def handle_info(:report_startup, state) do
    version = Application.spec(:my_app, :vsn) |> to_string()
    Logger.info("Bakeware binary started: version=#{version} node=#{node()}")
    ping_startup_heartbeat(version)
    {:noreply, state}
  end

  defp ping_startup_heartbeat(version) do
    url = Application.get_env(:my_app, :vigilmon)[:startup_heartbeat_url]

    if url do
      case Req.get(url, receive_timeout: 10_000) do
        {:ok, _} ->
          Logger.info("Startup heartbeat sent to Vigilmon")
        {:error, reason} ->
          Logger.warning("Startup heartbeat failed: #{inspect(reason)}")
      end
    end
  end
end

Add MyApp.StartupReporter as the last child in your supervision tree so it fires only after all services are ready:

children = [
  MyApp.Repo,
  MyAppWeb.Endpoint,
  MyApp.StartupReporter   # ← last in the list
]

Step 5: Set up HTTP monitoring in Vigilmon

Point Vigilmon at the health endpoint your Bakeware binary serves:

  1. Sign up at vigilmon.online
  2. Click New Monitor → HTTP
  3. Enter https://yourdomain.com/health
  4. Set check interval: 1 minute (paid) or 5 minutes (free)
  5. Under Advanced, add a keyword check for "ok" to verify the health status
  6. Save

Why external monitoring matters for Bakeware binaries:

Bakeware binaries eliminate runtime dependencies, but they introduce their own failure modes. The binary may crash on boot due to a missing environment variable. A new version may fail to bind to the expected port. A daemon binary may silently exit after a transient error. None of these produce user-visible errors without external monitoring.

Vigilmon's response time history is also useful for detecting post-deployment regressions: a new binary version that takes longer to respond may indicate a startup issue or a performance regression in the new release.


Step 6: Heartbeat monitoring for long-running daemon binaries

For Bakeware binaries that run as background daemons (not HTTP services), use a heartbeat monitor instead:

# lib/my_app/workers/liveness_worker.ex
defmodule MyApp.Workers.LivenessWorker do
  use GenServer
  require Logger

  @interval :timer.minutes(5)

  def start_link(_), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)

  @impl true
  def init(state) do
    schedule_tick()
    {:ok, state}
  end

  @impl true
  def handle_info(:ping, state) do
    ping_heartbeat()
    schedule_tick()
    {:noreply, state}
  end

  defp schedule_tick, do: Process.send_after(self(), :ping, @interval)

  defp ping_heartbeat do
    url = Application.get_env(:my_app, :vigilmon)[:liveness_heartbeat_url]

    if url do
      case Req.get(url, receive_timeout: 10_000) do
        {:ok, _} -> :ok
        {:error, reason} -> Logger.warning("Liveness heartbeat failed: #{inspect(reason)}")
      end
    end
  end
end

In Vigilmon, create a Heartbeat monitor:

  1. New Monitor → Heartbeat
  2. Set interval to 7 minutes (5-minute ping interval with 2-minute grace)
  3. Copy the ping URL and set it as VIGILMON_LIVENESS_HEARTBEAT_URL on the server

If the binary crashes or the daemon exits, Vigilmon will alert you within one check interval.


Step 7: Alerts via Slack

In Vigilmon, go to Notifications → New Channel → Slack and paste your incoming webhook URL.

Enable the channel on all monitors. Binary-related incidents commonly appear as:

  • HTTP health check down (binary crashed or failed to bind to port)
  • Response time spike (new binary version with a performance regression)
  • Startup heartbeat missed (deployment failed — binary didn't reach full initialization)
  • Liveness heartbeat missed (daemon exited silently between restarts)

Configure Vigilmon to send all alert types to your deployment channel so every binary lifecycle event is captured.


Step 8: Status page

  1. Status Pages → New Status Page in Vigilmon
  2. Add your HTTP health monitor and all heartbeat monitors
  3. Link the status page from your deployment runbook

README badge:

![Uptime](https://vigilmon.online/badge/your-monitor-slug)

What you've built

| What | How | |------|-----| | Health check endpoint | HealthCheck plug with VM liveness + uptime | | Version in health response | Application.spec/2 included in JSON payload | | HTTP uptime monitoring | Vigilmon HTTP monitor → /health | | Startup heartbeat | StartupReporter GenServer pings Vigilmon on init | | Daemon liveness heartbeat | LivenessWorker pings Vigilmon every 5 minutes | | Post-deploy verification | Vigilmon startup heartbeat miss = failed deployment | | Slack downtime alerts | Vigilmon Slack notification channel | | Status page | Vigilmon public status page |

Bakeware makes Elixir applications trivial to distribute. Vigilmon ensures those distributed binaries stay alive and healthy wherever they run.


Next steps

  • Store the binary version in the heartbeat URL as a query parameter (?version=1.0.0) so Vigilmon logs confirm which version is running
  • Run a second Vigilmon HTTP monitor on a canary server to catch regressions before rolling out a new binary to the full fleet
  • Use Vigilmon response time history to compare startup latency across binary versions
  • Create a dedicated Vigilmon project per binary to isolate alert noise between services

Get started free at vigilmon.online.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →