Skip to main contentSkip to navigation
[email protected]
Client AreaSupport
Hosting Mammoth
HostingMammothYour Data, Our Responsibility
Home
Solutions
Hosting Services
Store
Pricing
About
Blog
API
Contact

Stay Ahead of the Curve

Get the latest insights on cybersecurity, AI innovations, and enterprise data solutions delivered to your inbox.

Hosting Mammoth
HostingMammothEnterprise Solutions

Enterprise-grade data solutions. Hosting, recovery, cybersecurity, and AI-powered services for businesses worldwide.

[email protected]
Sun - Fri, 9:00am - 5:00pm

Services

  • Cloud Hosting
  • Data Recovery
  • Cybersecurity
  • Legal Support
  • MSP Services
  • Web Development
  • AI Services
  • Free Server Migration

Hosting

  • VPS Hosting (NVMe SSD)
  • VDS Hosting (NVMe)
  • Storage VPS (High SSD)
  • GPU Servers
  • Managed Services
  • Cloud Firewall
  • Load Balancer
  • One-Click Apps
  • n8n Hosting
  • Object Storage
  • FAQ

Company

  • Store
  • Pricing
  • About Us
  • Locations
  • Blog
  • Testimonials
  • Contact
  • Affiliate Program
  • White-Label
  • Terms of Service
  • Privacy Policy
  • Browser Cookies
  • SLA

Support

  • Client Area
  • Submit Ticket
  • Knowledge Base
  • Server Status
  • API Documentation

© 2026 Hosting Mammoth. All rights reserved.

Knowledge Base
Getting StartedAccount ManagementVPS HostingGPU ServersStorage VPSCloud FirewallLoad BalancerServer ManagementBilling & PaymentsSupport & TicketsAffiliate ProgramReseller ProgramMarketplace & Appsn8n HostingManaged ServicesServer MigrationAPI & DevelopersSecurityTroubleshootingGlossaryInstall Guides
  1. Home
  2. /
  3. Support
  4. /
  5. Install Guides
  6. /
  7. How To Install Elixir Phoenix Ubuntu
GUIDEInstall Guides

How to Install Elixir + Phoenix on Ubuntu 24.04 VPS: Production Release Deploy

18 min read

How to Install Elixir + Phoenix on Ubuntu 24.04 VPS: Production Release Deploy

Elixir and Phoenix are a formidable pair for building real-time web applications, APIs, and distributed systems. Running on the BEAM virtual machine (the same runtime that powers WhatsApp and Discord), they give you lightweight processes, preemptive scheduling, and fault tolerance out of the box. This guide walks you through installing Elixir and Phoenix on an Ubuntu 24.04 VPS using asdf, building a production mix release, wiring up PostgreSQL, and fronting the app with Nginx for LiveView WebSockets.

Skip the setup? Launch a pre-tuned Ubuntu 24.04 VPS in 60 seconds on our CloudCore Starter plan and start deploying Phoenix apps today.

Table of Contents

  • Why Elixir and Phoenix?
  • Prerequisites
  • Step 1: Update System Packages
  • Step 2: Install Build Dependencies
  • Step 3: Install asdf Version Manager
  • Step 4: Install Erlang/OTP 26 and Elixir 1.16
  • Step 5: Install Hex, Rebar3, and phx_new
  • Step 6: Install and Configure PostgreSQL
  • Step 7: Create a Phoenix Application
  • Step 8: Configure runtime.exs for Production
  • Step 9: Compile Assets with esbuild and Tailwind
  • Step 10: Build a Mix Release
  • Step 11: Create a systemd Service
  • Step 12: Nginx Reverse Proxy with WebSocket Upgrade
  • Post-Install: Hot-Code Deploy and Clustering
  • Troubleshooting
  • Next Steps
  • Why Elixir and Phoenix?

    Elixir is a functional, dynamically typed language that compiles to BEAM bytecode and inherits three decades of battle-tested Erlang/OTP engineering. The reasons teams choose it for production web workloads are concrete:

    • Massive concurrency -- The BEAM schedules millions of lightweight processes across your CPU cores. A single modest VPS can hold hundreds of thousands of stateful connections simultaneously. There is no thread-pool sizing, no event-loop starvation, no async/await colouring.
    • Fault tolerance -- OTP supervisors restart crashed processes automatically in milliseconds. "Let it crash" is a real, production-viable strategy because failure is isolated to a single process, not the whole node.
    • Preemptive scheduling -- Unlike Node.js or Python, one slow request cannot block every other request. The scheduler reduces every process at regular intervals, so latency stays flat under load.
    • Phoenix LiveView -- Build rich, real-time user interfaces entirely in server-rendered Elixir, pushing diffs over a WebSocket. You get SPA feel with none of the client-side JavaScript complexity, and the same process model handles a hundred thousand concurrent LiveView sessions without breaking a sweat.
    • First-class distribution -- Nodes connect over the distributed Erlang protocol. Global process registries, pub/sub, and clustering are library calls, not infrastructure projects.
    • Batteries-included releases -- mix release produces a self-contained tarball that runs anywhere with the same glibc -- no Dockerfile required, no runtime install on the target machine.
    Typical production uses include real-time dashboards, chat and presence systems, IoT gateways, API backends, job processors (Oban), and any workload where long-lived stateful connections or sub-second tail latency matter.

    Prerequisites

    Before you begin, make sure you have:

    • A VPS running Ubuntu 24.04 LTS with root or sudo access
    • SSH access to your server
    • At least 2 GB of RAM (4 GB+ recommended if you will also run PostgreSQL on the same host)
    • At least 20 GB of free disk space for the toolchain, release artifacts, and database
    • A registered domain pointing to your server's IP (optional but required for SSL)
    Recommended Plan: CloudCore Starter
    >
    Phoenix apps have a small runtime footprint thanks to the BEAM. For a production app with PostgreSQL on the same host, we recommend the CloudCore Starter plan:
    >
    - 4 vCPU cores
    - 6 GB RAM
    - 100 GB NVMe SSD
    - Unmetered bandwidth
    >
    That is enough headroom for tens of thousands of concurrent LiveView connections plus a local Postgres instance. For larger workloads or clustered deployments, scale up to CloudCore Professional.

    Connect to your server:

    bash
    ssh root@your-server-ip

    Step 1: Update System Packages

    Start with a clean, fully patched base.

    bash
    sudo apt update && sudo apt upgrade -y

    If the kernel was upgraded, reboot:

    bash
    sudo reboot

    Reconnect via SSH after a minute.

    Step 2: Install Build Dependencies

    Erlang compiles from source when installed through asdf, so you need a C toolchain plus the libraries Erlang links against (crypto, SSL, wxWidgets for observer, ODBC, etc.). Installing the full set now avoids opaque build failures later.

    bash
    sudo apt install -y build-essential autoconf m4 libncurses5-dev \
      libwxgtk3.2-dev libwxgtk-webview3.2-dev libgl1-mesa-dev libglu1-mesa-dev \
      libpng-dev libssh-dev unixodbc-dev xsltproc fop libxml2-utils libncurses-dev \
      openjdk-21-jdk curl git unzip

    Explanation:

    • build-essential, autoconf, m4 -- C compiler and build tools for Erlang
    • libssl-dev (pulled by build-essential deps) -- TLS support in :crypto and :ssl
    • libncurses-dev -- Terminal support for the Erlang shell
    • libwxgtk* -- Observer GUI (you can skip on headless servers, but kerl warns loudly)
    • unixodbc-dev -- ODBC database driver bindings
    • openjdk-21-jdk -- Required by some Erlang documentation targets

    Step 3: Install asdf Version Manager

    asdf is the recommended way to install Erlang and Elixir on Linux. It lets you pin exact versions per project, upgrade without touching system packages, and run multiple Erlang/Elixir versions side by side.

    Clone asdf to your home directory and source it:

    bash
    git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.14.1
    echo '. "$HOME/.asdf/asdf.sh"' >> ~/.bashrc
    echo '. "$HOME/.asdf/completions/asdf.bash"' >> ~/.bashrc
    source ~/.bashrc

    Verify the install:

    bash
    asdf --version

    Expected output:

    text
    v0.14.1-f00f759

    Add the Erlang and Elixir plugins:

    bash
    asdf plugin add erlang https://github.com/asdf-vm/asdf-erlang.git
    asdf plugin add elixir https://github.com/asdf-vm/asdf-elixir.git

    List plugins to confirm:

    bash
    asdf plugin list

    Expected output:

    text
    elixir
    erlang

    Step 4: Install Erlang/OTP 26 and Elixir 1.16

    Phoenix 1.7 requires Erlang/OTP 24+ and Elixir 1.14+. The combination below (OTP 26.2.5 + Elixir 1.16.3 built against OTP 26) is a well-tested production pairing.

    Install Erlang (this compiles from source and takes 5-10 minutes on a 4 vCPU VPS):

    bash
    export KERL_CONFIGURE_OPTIONS="--disable-debug --without-javac --enable-shared-zlib --enable-dynamic-ssl-lib"
    asdf install erlang 26.2.5
    asdf global erlang 26.2.5

    Verify:

    bash
    erl -eval 'io:format("~s", [erlang:system_info(otp_release)]), halt().' -noshell

    Expected output:

    text
    26

    Install Elixir (precompiled, takes seconds):

    bash
    asdf install elixir 1.16.3-otp-26
    asdf global elixir 1.16.3-otp-26

    Verify:

    bash
    elixir --version

    Expected output:

    text
    Erlang/OTP 26 [erts-14.2.5] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [jit:ns]

    Elixir 1.16.3 (compiled with Erlang/OTP 26)

    The pairing 1.16.3-otp-26 is important -- asdf ships Elixir precompiled against specific OTP majors, so always match to the Erlang major you installed.

    Step 5: Install Hex, Rebar3, and phx_new

    Hex is Elixir's package manager, Rebar3 compiles Erlang dependencies, and phx_new is the Phoenix project generator.

    bash
    mix local.hex --force
    mix local.rebar --force
    mix archive.install hex phx_new --force

    Verify:

    bash
    mix phx.new --version

    Expected output:

    text
    Phoenix installer v1.7.14

    Step 6: Install and Configure PostgreSQL

    Phoenix defaults to PostgreSQL via Ecto. If you do not already have a database running, see our dedicated guide on how to install PostgreSQL on Ubuntu 24.04. The short version:

    bash
    sudo apt install -y postgresql postgresql-contrib
    sudo systemctl enable --now postgresql

    Create a dedicated role and database for your app (replace the password with something from openssl rand -hex 24):

    bash
    sudo -u postgres psql <<EOF
    CREATE USER myapp WITH PASSWORD 'CHANGE_ME_STRONG_PASSWORD';
    CREATE DATABASE myapp_prod OWNER myapp;
    ALTER USER myapp CREATEDB;
    EOF

    The CREATEDB grant is needed only if you want mix ecto.create to work for this user; production releases typically do not need it once the database exists.

    Step 7: Create a Phoenix Application

    Generate a new Phoenix app. We will call it myapp.

    bash
    cd ~
    mix phx.new myapp --database postgres
    cd myapp

    Answer Y when asked to fetch and install dependencies.

    Update config/dev.exs with your Postgres credentials, then create and migrate the dev database to confirm the stack works end to end:

    bash
    mix ecto.create
    mix ecto.migrate
    mix phx.server

    Visit http://your-server-ip:4000 (you may need to sudo ufw allow 4000 temporarily). Stop the server with Ctrl+C twice when you see the welcome page.

    Step 8: Configure runtime.exs for Production

    Phoenix generates config/runtime.exs specifically for production-time configuration. Unlike prod.exs, which is read at compile time, runtime.exs is evaluated at release boot -- this is where you read environment variables on the production server.

    Open config/runtime.exs and confirm the :prod block contains these essentials:

    elixir
    if config_env() == :prod do
      database_url =
        System.get_env("DATABASE_URL") ||
          raise "environment variable DATABASE_URL is missing"

    secret_key_base = System.get_env("SECRET_KEY_BASE") || raise "environment variable SECRET_KEY_BASE is missing"

    host = System.get_env("PHX_HOST") || "example.com" port = String.to_integer(System.get_env("PORT") || "4000")

    config :myapp, Myapp.Repo, url: database_url, pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"), socket_options: [:inet6]

    config :myapp, MyappWeb.Endpoint, url: [host: host, port: 443, scheme: "https"], http: [ip: {127, 0, 0, 1}, port: port], secret_key_base: secret_key_base, server: true end

    Key points:

    • DATABASE_URL -- Standard postgres://user:pass@host:port/db connection string
    • SECRET_KEY_BASE -- 64+ bytes of random data used to sign sessions and cookies. Generate one with mix phx.gen.secret
    • PHX_HOST -- The public hostname. Used by URL helpers and for CSRF/origin checks
    • http: [ip: {127, 0, 0, 1}, port: port] -- Bind to loopback only; Nginx will proxy public traffic to it
    • server: true -- Ensures the Cowboy/Bandit web server actually starts under the release (unlike mix phx.server, a release does not auto-start it)

    Step 9: Compile Assets with esbuild and Tailwind

    Modern Phoenix ships with esbuild and tailwind as Elixir-wrapped binaries -- no Node.js required on the build host.

    bash
    mix assets.deploy

    This runs esbuild to bundle JavaScript, tailwind to generate CSS, and phx.digest to fingerprint the output into priv/static/. If you see missing-binary errors, fetch the platform binaries first:

    bash
    mix esbuild.install
    mix tailwind.install

    Step 10: Build a Mix Release

    A mix release bundles your compiled BEAM files, your config, all dependencies, and optionally the Erlang runtime itself (erts) into a self-contained tarball. Booting the release does not require mix, Elixir, or Erlang to be installed on the target machine.

    Set the production environment variables and build:

    bash
    export MIX_ENV=prod
    export SECRET_KEY_BASE=$(mix phx.gen.secret)
    mix deps.get --only prod
    mix compile
    mix assets.deploy
    mix release

    Output ends with:

    text
    * assembling myapp-0.1.0 on MIX_ENV=prod
    
    • using config/runtime.exs to configure the release at runtime
    Release created at _build/prod/rel/myapp

    # To start your system _build/prod/rel/myapp/bin/myapp start

    include_erts: true or false?

    In mix.exs under releases:, the include_erts option controls whether the Erlang runtime is bundled:

    • include_erts: true (default) -- Produces a fully self-contained release. Perfect when build host and target share a glibc version. Tarball is ~30 MB larger.
    • include_erts: false -- Assumes Erlang is installed on the target. Use when you already manage OTP centrally (same asdf setup on prod) and want smaller artifacts.
    For a single VPS deploy where you build and run on the same box, stick with the default true and treat the release as your unit of deployment.

    Running Migrations at Boot

    A release does not include mix, so mix ecto.migrate does not work on the target. Use a release module that drives Ecto.Migrator directly. Create lib/myapp/release.ex:

    elixir
    defmodule Myapp.Release do
      @app :myapp

    def migrate do load_app() for repo <- repos() do {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true)) end end

    defp repos do Application.fetch_env!(@app, :ecto_repos) end

    defp load_app do Application.load(@app) end end

    Now you can run migrations on the server with:

    bash
    _build/prod/rel/myapp/bin/myapp eval "Myapp.Release.migrate()"

    Or invoke it automatically from the systemd unit on start, as shown in the next step.

    Copying the Release

    If you build on a different host (CI or local dev), copy the release tarball or the rel directory to the server:

    bash
    tar -czf myapp-release.tar.gz -C _build/prod/rel myapp
    scp myapp-release.tar.gz root@your-server-ip:/opt/
    ssh root@your-server-ip "cd /opt && tar -xzf myapp-release.tar.gz"

    The extracted release lives at /opt/myapp.

    Step 11: Create a systemd Service

    systemd is the cleanest way to supervise a Phoenix release on Ubuntu. It restarts the node on crash, starts at boot, captures logs to journald, and lets you pass environment variables via an EnvironmentFile.

    Create a dedicated user:

    bash
    sudo useradd --system --home-dir /opt/myapp --shell /bin/false myapp
    sudo chown -R myapp:myapp /opt/myapp

    Create the environment file (mode 600 because it contains secrets):

    bash
    sudo tee /etc/myapp.env > /dev/null <<EOF
    DATABASE_URL=postgres://myapp:CHANGE_ME_STRONG_PASSWORD@localhost:5432/myapp_prod
    SECRET_KEY_BASE=$(openssl rand -base64 48 | tr -d '\n')
    PHX_HOST=example.com
    PORT=4000
    POOL_SIZE=10
    LANG=en_US.UTF-8
    EOF
    sudo chmod 600 /etc/myapp.env

    Create the unit file at /etc/systemd/system/myapp.service:

    bash
    sudo tee /etc/systemd/system/myapp.service > /dev/null <<'EOF'
    [Unit]
    Description=Myapp Phoenix Release
    After=network.target postgresql.service
    Requires=postgresql.service

    [Service] Type=exec User=myapp Group=myapp WorkingDirectory=/opt/myapp EnvironmentFile=/etc/myapp.env ExecStartPre=/opt/myapp/bin/myapp eval "Myapp.Release.migrate()" ExecStart=/opt/myapp/bin/myapp start ExecStop=/opt/myapp/bin/myapp stop Restart=on-failure RestartSec=5 LimitNOFILE=65535 KillMode=process

    [Install] WantedBy=multi-user.target EOF

    Key choices:

    • Type=exec -- systemd considers the service started once the main process is exec'd. Works well with bin/myapp start (which does not fork).
    • ExecStartPre -- Runs migrations idempotently on every start. If there is nothing to migrate, it is a fast no-op.
    • LimitNOFILE=65535 -- BEAM opens one file descriptor per socket. Raise the limit or you will hit it at a few thousand concurrent connections.
    • Restart=on-failure -- systemd restarts the release if it crashes. OTP supervisors handle in-process failures; systemd is the last line of defence.
    Enable and start the service:

    bash
    sudo systemctl daemon-reload
    sudo systemctl enable --now myapp
    sudo systemctl status myapp

    Expected output:

    text
    ● myapp.service - Myapp Phoenix Release
         Loaded: loaded (/etc/systemd/system/myapp.service; enabled; preset: enabled)
         Active: active (running) since Wed 2026-04-16 10:00:00 UTC; 10s ago
       Main PID: 12345 (beam.smp)

    Tail logs with sudo journalctl -u myapp -f.

    Step 12: Nginx Reverse Proxy with WebSocket Upgrade

    Phoenix LiveView uses a persistent WebSocket. Nginx must be told explicitly to forward the Upgrade and Connection headers, otherwise the handshake will 400 and LiveView will fall back to full-page reloads.

    For the full Nginx install walkthrough, see our how to install Nginx on Ubuntu 24.04 guide. The Phoenix-specific vhost looks like this:

    bash
    sudo tee /etc/nginx/sites-available/myapp > /dev/null <<'EOF'
    map $http_upgrade $connection_upgrade {
        default upgrade;
        ''      close;
    }

    upstream phoenix_upstream { server 127.0.0.1:4000; keepalive 32; }

    server { listen 80; server_name example.com; return 301 https://$host$request_uri; }

    server { listen 443 ssl http2; server_name example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    client_max_body_size 20m;

    # LiveView and channels WebSocket location /live/websocket { proxy_pass http://phoenix_upstream; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 86400s; proxy_send_timeout 86400s; }

    # Everything else location / { proxy_pass http://phoenix_upstream; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 60s; } } EOF

    sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/ sudo nginx -t && sudo systemctl reload nginx

    The critical pieces:

    • map $http_upgrade $connection_upgrade -- Synthesises the right Connection: upgrade header only when the client actually requests an upgrade, avoiding interference with keep-alive on normal HTTP requests.
    • proxy_http_version 1.1 -- Required for WebSockets; HTTP/1.0 does not support the upgrade mechanism.
    • proxy_read_timeout 86400s on /live/websocket -- A day's worth of idle tolerance so LiveView connections are not killed by default 60-second proxy timeouts.
    • upstream ... keepalive 32 -- Reuses connections to the BEAM, cutting TCP handshake overhead on busy sites.
    Obtain an SSL certificate with Certbot if you have not already:

    bash
    sudo apt install -y certbot python3-certbot-nginx
    sudo certbot --nginx -d example.com

    Visit https://example.com -- your Phoenix app should load with a secure padlock and LiveView should stay connected (check the browser devtools Network tab for a WebSocket in 101 Switching Protocols).

    Post-Install: Hot-Code Deploy and Clustering

    Hot-Code Upgrades with Release Overlays

    One of OTP's headline features is appup-based hot upgrades -- swapping modules in a running node without dropping connections. Phoenix mix releases support this via :appup and release overlays, but in practice most teams deploy by rolling the systemd service:

    bash
    sudo systemctl restart myapp

    OTP's graceful shutdown gives in-flight requests a short grace period. Combined with Nginx retries, a rolling restart is usually invisible to users.

    If you genuinely need zero-downtime hot upgrades (rare, and worth the complexity only for always-on stateful systems), use Distillery or hand-author the appup with mix release --upgrade. Start from the Elixir release documentation before committing to this path.

    Clustering with libcluster

    To run Phoenix on multiple nodes (for redundancy, horizontal scale, or LiveView presence across regions), add libcluster:

    elixir
    # mix.exs
    {:libcluster, "~> 3.3"}
    elixir
    # config/runtime.exs
    config :libcluster,
      topologies: [
        gossip: [
          strategy: Cluster.Strategy.Gossip,
          config: [
            port: 45892,
            if_addr: "0.0.0.0",
            multicast_addr: "230.1.1.251",
            broadcast_only: true
          ]
        ]
      ]

    And supervise it in application.ex:

    elixir
    {Cluster.Supervisor, [Application.get_env(:libcluster, :topologies), [name: Myapp.ClusterSupervisor]]}

    Set RELEASE_COOKIE to the same value on every node and open UDP 45892 between them. Nodes will discover each other automatically and Node.list/0 will show the cluster membership. For multi-region deploys, swap the Gossip strategy for Kubernetes, EPMD, or ErlangHosts as appropriate.

    Once clustered, Phoenix PubSub and Presence work across nodes transparently -- a LiveView on node A will receive updates broadcast from node B with no code changes.

    CRDTs for Shared State

    When nodes need eventually consistent shared state (distributed counters, session stores, rate limiters), reach for delta_crdt or Horde. Horde in particular gives you a distributed process registry and dynamic supervisor that survives network partitions -- the BEAM-native equivalent of a service mesh sidecar.

    Troubleshooting

    ProblemCauseSolution
    Error loading module ... : undefined function on release startinclude_erts: false on a target without matching OTPRebuild with include_erts: true, or install the exact OTP major version on the target
    cannot execute: required file not found running bin/myappMissing C runtime libs on target (libstdc++, libtinfo)sudo apt install -y libtinfo6 libstdc++6 on the target matches the build host
    ** (Postgrex.Error) FATAL 28P01 password authentication failedWrong DATABASE_URL password or pg_hba.conf policyVerify with psql "$DATABASE_URL". Check /etc/postgresql/16/main/pg_hba.conf for scram-sha-256 vs md5
    eaddrinuse or :eaddrinuse on port 4000Another Phoenix instance (or leftover iex) is boundsudo lsof -i :4000 then kill. Check for a running mix phx.server from dev
    LiveView reconnects constantly, "socket closed" in JS consoleNginx missing WebSocket upgrade headersConfirm map $http_upgrade block and proxy_set_header Upgrade on /live/websocket
    Nodes cannot see each other (Node.list() == [])Cookie mismatch or firewall blocking EPMD/gossipSame RELEASE_COOKIE on every node; open TCP 4369 (EPMD) and UDP 45892 (gossip) between them
    :logger handler crashed or :too_many_open_filesLimitNOFILE too lowRaise to 65535 in the systemd unit and sudo systemctl daemon-reload
    Migrations "stuck" on deployConcurrent migration from two nodesMigrate on one node only, or use Ecto.Migrator lock. Consider a dedicated one-shot systemd unit for migrations

    Reading Logs

    Everything the release prints goes to journald via systemd:

    bash
    sudo journalctl -u myapp -f          # live tail
    sudo journalctl -u myapp -n 200      # last 200 lines
    sudo journalctl -u myapp --since "1 hour ago"

    To attach a remote iex shell to the running node for live inspection:

    bash
    sudo -u myapp /opt/myapp/bin/myapp remote

    You now have a full iex prompt inside the production BEAM. Use :observer.start() (over X11-forwarded SSH) or :recon for process-level diagnostics.

    Next Steps

    Your Phoenix app is running on a production Ubuntu VPS with PostgreSQL, a mix release under systemd, and Nginx fronting LiveView WebSockets. Recommended follow-ups:

    • Add background jobs with Oban -- Oban is the de facto job queue for Elixir. Uses your existing Postgres, gives you cron, retries, rate limiting, and a web dashboard.
    • Wire up observability -- Add PromEx for Prometheus metrics and ship them to Grafana. BEAM telemetry is rich: per-process memory, scheduler utilisation, Ecto query timings, Phoenix endpoint stats.
    • Automate deploys -- Script the release build + scp + systemctl restart flow, or adopt Kamal / flyctl for declarative deploys.
    • Scale horizontally -- Once you have libcluster running, add a second CloudCore node behind the same Nginx (or a load balancer) and let Phoenix PubSub spread events across the cluster.
    • Explore the Phoenix docs -- The official Phoenix Framework guides and the Elixir language docs are both excellent and worth reading end to end.

    Skip the Manual Install -- Launch Elixir-Ready VPS
    >
    Every CloudCore Starter plan ships with Ubuntu 24.04, NVMe storage, and the networking headroom your Phoenix LiveView workload needs. Deploy a node in 60 seconds and have your first mix release running in under an hour.
    >
    - 4 vCPU, 6 GB RAM, 100 GB NVMe
    - Unmetered bandwidth for LiveView WebSockets
    - Full root access and cloud-init support
    - EU and North American datacentre choice
    >
    Launch Your CloudCore Starter VPS -- production-grade Phoenix hosting from day one.

    Was this article helpful?

    ← Back to Install GuidesBrowse all categories →

    Still have questions?

    Contact Support →Submit a Ticket