Ew
Applications Azure

Elixir with Phoenix on Ubuntu 24.04 on Azure User Guide

| Product: Elixir with Phoenix on Ubuntu 24.04 LTS on Azure

Overview

This guide covers the deployment and configuration of Elixir with Phoenix on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. Elixir is a dynamic, functional language that runs on the Erlang virtual machine (the BEAM), and Phoenix is its productive, real-time web framework — together the stack behind fault-tolerant, massively concurrent web applications and WebSocket channels at scale.

The image installs Elixir 1.20.2 and Phoenix 1.8.9 on Erlang/OTP 28.5, all pinned by exact version and verified against sha256 checksums (Erlang/OTP is compiled from the official source release; Elixir is the official precompiled build). Because Phoenix is a web framework, this image ships a genuinely browsable web UI: a demo Phoenix application is pre-deployed as a production Mix release — the self-contained BEAM artifact that bundles its own Erlang runtime — and served behind nginx on port 80. You can point a browser at the VM and see the standard Phoenix welcome page, a live JSON status route, and Phoenix LiveDashboard, the real-time BEAM observability console.

What is included:

  • Elixir 1.20.2 (Apache-2.0) and Phoenix 1.8.9 (MIT), pinned and sha256-verified

  • Erlang/OTP 28.5.0.3 compiled from the official source release, installed to /usr/local

  • elixir, iex and mix on the default system PATH for every user

  • A demo Phoenix app built as a production Mix release and deployed to /var/lib/phoenix-demo/rel/demo

  • phoenix-demo.service systemd unit auto-starting on boot, running as the unprivileged phoenix:phoenix system user

  • elixir-phoenix-firstboot.service systemd oneshot that generates a per-VM secret, starts the release + nginx and confirms the app answers before completing

  • Phoenix endpoint bound to loopback only (127.0.0.1:4000) — nginx fronts it on :80 with WebSocket upgrade for LiveView

  • Unauthenticated /healthz endpoint (nginx-native, HTTP 200) for load balancer / probe checks

  • The standard Phoenix welcome landing page at /

  • A live JSON status route at /api/status — server timestamp, Elixir/OTP/Phoenix versions and a concurrency-safe request counter (proof the BEAM executes code per request)

  • Phoenix LiveDashboard at /dashboard — live BEAM memory, processes, ETS and run-queues over a LiveView WebSocket

  • The Mix release and runtime data on a dedicated 20 GiB Azure data disk at /var/lib/phoenix-demo, independent of the OS disk and independently resizable

  • Ubuntu 24.04 LTS base with latest security patches applied at build time

  • Azure Linux Agent for seamless cloud integration and SSH key injection

  • 24/7 cloudimg support with guaranteed 24 hour response SLA

Prerequisites

  • Active Azure subscription, SSH public key, VNet + subnet in target region

  • Subscription to the Elixir with Phoenix listing on Azure Marketplace

Recommended virtual machine size: Standard_B2s (2 vCPU, 4 GB RAM) for development. Production Phoenix workloads should use Standard_D4s_v5 (4 vCPU, 16 GB RAM) or larger.

Step 1: Deploy from the Azure Portal

Search Elixir with Phoenix in Marketplace, select the cloudimg publisher, click Create. NSG rules: TCP 22 (admin), TCP 80 (HTTP), TCP 443 (reserved for TLS) from your client networks.

Step 2: Deploy from the Azure CLI

RG="phoenix-prod"; LOCATION="eastus"; VM_NAME="phoenix-01"
GALLERY_IMAGE_ID="/subscriptions/<sub-id>/resourceGroups/azure-cloudimg/providers/Microsoft.Compute/galleries/cloudimgGallery/images/elixir-phoenix-ubuntu-24-04/versions/<version>"
SSH_KEY="$(cat ~/.ssh/id_rsa.pub)"
az group create --name "$RG" --location "$LOCATION"
az network vnet create -g "$RG" --name phoenix-vnet --address-prefix 10.100.0.0/16 --subnet-name phoenix-subnet --subnet-prefix 10.100.1.0/24
az network nsg create -g "$RG" --name phoenix-nsg
az network nsg rule create -g "$RG" --nsg-name phoenix-nsg --name allow-ssh --priority 100 \
  --source-address-prefixes "<your-mgmt-cidr>" --destination-port-ranges 22 --access Allow --protocol Tcp
az network nsg rule create -g "$RG" --nsg-name phoenix-nsg --name allow-http --priority 110 \
  --destination-port-ranges 80 --access Allow --protocol Tcp
az network nsg rule create -g "$RG" --nsg-name phoenix-nsg --name allow-https --priority 120 \
  --destination-port-ranges 443 --access Allow --protocol Tcp
az vm create -g "$RG" --name "$VM_NAME" --image "$GALLERY_IMAGE_ID" \
  --size Standard_B2s --storage-sku StandardSSD_LRS \
  --admin-username azureuser --ssh-key-values "$SSH_KEY" \
  --vnet-name phoenix-vnet --subnet phoenix-subnet --nsg phoenix-nsg --public-ip-sku Standard

Step 3: Connect via SSH

ssh azureuser@<vm-ip>

Step 4: Verify the Phoenix and nginx Services

Confirm the demo release and nginx are both running, that Phoenix is bound to loopback only, that the health endpoint answers, and that the data disk is mounted.

for s in phoenix-demo.service nginx.service elixir-phoenix-firstboot.service; do
  printf '  %-34s %s\n' "$s" "$(systemctl is-active $s)"
done
ss -tlnH | awk '{print $4}' | grep -E ':4000$|:80$' | sort -u
curl -s -o /dev/null -w 'GET /healthz -> HTTP %{http_code}\n' http://127.0.0.1/healthz
findmnt -no SOURCE,TARGET,FSTYPE,SIZE /var/lib/phoenix-demo

phoenix-demo.service and nginx.service both active (running); the firstboot oneshot completed; ss shows the Phoenix endpoint bound to 127.0.0.1:4000 only with nginx public on :80; the health endpoint returns HTTP 200; and the Mix release lives on the dedicated 20 GiB data disk mounted at /var/lib/phoenix-demo

Step 5: Check the Health Endpoint

The /healthz endpoint is served by nginx itself (no round-trip to the BEAM), so it is ideal for Azure Load Balancer health probes.

curl -i http://127.0.0.1/healthz

Step 6: Open the Phoenix Landing Page in a Browser

Browse to http://<vm-public-ip>/. The demo Phoenix application serves the standard Phoenix framework welcome page, rendered by the running release on the BEAM VM.

The Phoenix framework welcome landing page in a web browser: the Phoenix logo, the v1.8.9 badge, the Peace of mind from prototype to production headline and the Guides, Source Code and Changelog cards, served by the pre-deployed demo Phoenix release

Step 7: Call the Live Status API

Browse to http://<vm-public-ip>/api/status, or curl it. The route returns live JSON: a server timestamp, the Elixir, Erlang/OTP and Phoenix version numbers, and a request counter held in a supervised process. Reload it and the counter increments and the timestamp advances — proof the BEAM VM is executing code per request, not serving a static file.

curl -s http://127.0.0.1/api/status
echo
curl -s http://127.0.0.1/api/status

The live JSON status route open in a web browser at /api/status showing the message, elixirVersion 1.20.2, otpRelease 28, phoenixVersion 1.8.9, a requestCount and a serverTime, rendered on the fly by a running Elixir process

curl http://127.0.0.1/api/status run twice: the requestCount increments from one call to the next and the serverTime advances, proving live per-request execution on the BEAM VM rather than a cached static response

Step 8: Explore Phoenix LiveDashboard

Browse to http://<vm-public-ip>/dashboard. Phoenix LiveDashboard is the real-time observability console for the BEAM VM, updating live over a LiveView WebSocket. The Home page shows the Erlang/OTP, Elixir and Phoenix versions, atom and process counts, memory breakdown and run-queue depth.

Phoenix LiveDashboard Home in a browser: the System information panel reporting Erlang/OTP 28, tiles for Elixir 1.20.2, Phoenix 1.8.9 and LiveDashboard 0.8.7, and live System limits for atoms, ports and processes plus a memory breakdown and run queues

The Processes page lists the live Erlang processes inside the BEAM VM with their memory, reduction count, message-queue length and current function, refreshing in real time.

The Phoenix LiveDashboard Processes page in a browser: a live, sortable table of Erlang processes inside the BEAM VM including the code server, the LiveDashboard LiveView, the Bandit/thousand_island HTTP listener on port 4000 and the telemetry poller, each with memory, reductions and current function

LiveDashboard is exposed unauthenticated as a demo/showcase surface. Before production use, restrict it (see Step 13) — put it behind authentication or remove the route.

Step 9: Confirm the Toolchain and Deployed Release

The Elixir toolchain is on the system PATH, and the demo app is a self-contained production Mix release that bundles its own Erlang runtime.

elixir --version
ls -1 /var/lib/phoenix-demo/rel/demo/
test -x /var/lib/phoenix-demo/rel/demo/bin/demo && echo "release binary present"

elixir --version reporting Elixir 1.20.2 on Erlang/OTP 28, the Phoenix version from the demo release, and the deployed production Mix release directory under /var/lib/phoenix-demo/rel/demo with its bundled ERTS and bin/demo launcher

Step 10: Read the Endpoint Notes

First boot writes a non-secret notes file documenting the demo URLs and how to deploy your own release.

cat /var/lib/cloudimg/elixir-phoenix-endpoints.notes

cat /var/lib/cloudimg/elixir-phoenix-endpoints.notes showing the Phoenix landing, status API, LiveDashboard and healthz URLs, the Elixir binary and release directory paths, and the one-line instruction for building and deploying your own release

Step 11: Build and Deploy Your Own Phoenix Release

The image is a full Elixir/Phoenix build environment. To ship your own application, generate it, build a production release, and point the systemd unit at it. Because a Mix release bundles its own runtime, nothing else is needed at runtime.

# On a build host (or this VM), generate and build a release, then browse http://<vm-ip>/ :
mix phx.new myapp
cd myapp
MIX_ENV=prod mix assets.deploy
MIX_ENV=prod mix release

# Deploy it onto the data disk and repoint the service:
sudo systemctl stop phoenix-demo.service
sudo cp -a _build/prod/rel/myapp /var/lib/phoenix-demo/rel/myapp
sudo chown -R phoenix:phoenix /var/lib/phoenix-demo/rel/myapp
sudo sed -i 's#/rel/demo#/rel/myapp#g; s#/bin/demo#/bin/myapp#g' /etc/systemd/system/phoenix-demo.service
sudo systemctl daemon-reload
sudo systemctl start phoenix-demo.service

The per-VM SECRET_KEY_BASE, PHX_HOST and PORT live in /var/lib/phoenix-demo/demo.env (read by the systemd unit as an EnvironmentFile).

Step 12: Managing the Phoenix Service

sudo systemctl status phoenix-demo.service --no-pager
sudo systemctl restart phoenix-demo.service
sudo journalctl -u phoenix-demo.service -n 50 --no-pager

Use sudo journalctl -u phoenix-demo.service -f to follow the logs live, and sudo -u phoenix /var/lib/phoenix-demo/rel/demo/bin/demo remote to attach a remote IEx shell to the running node.

Step 13: Security Recommendations

  • Restrict or remove LiveDashboard. The /dashboard route is unauthenticated in this demo image. For production, put it behind authentication (a Plug that checks a basic-auth header or an admin session) or remove the live_dashboard route from your router.

  • Add TLS. Terminate HTTPS at nginx (port 443) with your certificate, or front the VM with Azure Application Gateway. Port 443 is reserved in the NSG for this.

  • Lock down the NSG. Restrict TCP 22 to your management CIDR, and expose TCP 80/443 only to the clients or load balancer that need them.

  • Keep the OS patched. Unattended security upgrades are enabled by default.

  • Rotate the Phoenix secret if you clone the VM: delete /var/lib/phoenix-demo/demo.env and restart elixir-phoenix-firstboot.service to regenerate SECRET_KEY_BASE.

Support

cloudimg provides 24/7 commercial support with a guaranteed 24 hour response SLA, separate from the upstream open-source projects. Contact support@cloudimg.co.uk.

Elixir is licensed under Apache-2.0 and Phoenix under the MIT license — free and open source with no per-CPU or per-deployment fee. The cloudimg charge covers packaging, security patching, image maintenance and support.