Elixir and the Phoenix framework are the go-to stack for applications requiring massive concurrency with real-time features — live dashboards, multiplayer games, collaborative tools, or chat systems where thousands of simultaneous WebSocket connections are the norm rather than the exception. Deployed on a Hong Kong VPS, Phoenix’s BEAM virtual machine handles 100,000+ concurrent WebSocket connections on modest hardware, all with CN2 GIA routing delivering sub-60ms latency to mainland Chinese users.
Why Elixir/Phoenix?
- Massive concurrency — the BEAM VM runs millions of lightweight processes; 100,000 concurrent WebSocket connections on a 4 GB VPS is realistic
- Phoenix LiveView — real-time server-rendered UI updates without writing JavaScript; ideal for dashboards and collaborative features
- Fault tolerance — OTP supervision trees restart failed processes automatically; self-healing without systemd restarts
- Hot code deployment — update running code without dropping connections in some configurations
Step 1: Install Erlang and Elixir
<code">apt update && apt upgrade -y apt install -y curl wget git unzip build-essential # Install via asdf (recommended — manages versions) git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.14.0 echo '. "$HOME/.asdf/asdf.sh"' >> ~/.bashrc source ~/.bashrc asdf plugin add erlang asdf plugin add elixir # Install Erlang 27 + Elixir 1.17 asdf install erlang 27.0 asdf install elixir 1.17.2-otp-27 asdf global erlang 27.0 asdf global elixir 1.17.2-otp-27 elixir --version mix --version
Step 2: Configure Phoenix for Production
<code"># config/runtime.exs — production configuration
import Config
if config_env() == :prod do
database_url =
System.get_env("DATABASE_URL") ||
raise "DATABASE_URL environment variable is missing"
config :myapp, Myapp.Repo,
url: database_url,
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
ssl: false
secret_key_base =
System.get_env("SECRET_KEY_BASE") ||
raise "SECRET_KEY_BASE environment variable is missing"
host = System.get_env("PHX_HOST") || "yourdomain.com"
port = String.to_integer(System.get_env("PORT") || "4000")
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
# Configure Redis for Phoenix.PubSub (optional — for multi-node)
# config :myapp, Myapp.PubSub,
# adapter: Phoenix.PubSub.Redis,
# url: System.get_env("REDIS_URL")
endStep 3: Build a Release
<code"># mix.exs — configure releases
def project do
[
releases: [
myapp: [
include_executables_for: [:unix],
applications: [runtime_tools: :permanent]
]
]
]
end
# On your VPS:
export MIX_ENV=prod
export SECRET_KEY_BASE=$(mix phx.gen.secret)
export DATABASE_URL=postgresql://myapp:pass@localhost/myapp_prod
export PHX_HOST=yourdomain.com
mix deps.get --only prod
mix compile
mix assets.deploy # Compile and minify CSS/JS
mix phx.gen.release
mix release
# Release is in _build/prod/rel/myapp/
ls _build/prod/rel/myapp/bin/
# myapp myapp.bat migrate serverStep 4: Deploy to VPS
<code">useradd -m -s /bin/bash phoenix mkdir -p /opt/myapp chown phoenix:phoenix /opt/myapp # Copy release scp -r _build/prod/rel/myapp root@VPS_IP:/opt/myapp/release # Create environment file cat > /opt/myapp/.env << 'EOF' DATABASE_URL=postgresql://myapp_user:Password!@localhost/myapp_production SECRET_KEY_BASE=GENERATED_64_CHAR_KEY PHX_HOST=yourdomain.com PORT=4000 POOL_SIZE=10 EOF chmod 600 /opt/myapp/.env
Systemd Service
<code">cat > /etc/systemd/system/phoenix.service << 'EOF' [Unit] Description=Phoenix Application After=network.target postgresql.service [Service] Type=simple User=phoenix WorkingDirectory=/opt/myapp EnvironmentFile=/opt/myapp/.env ExecStart=/opt/myapp/release/bin/myapp start ExecStop=/opt/myapp/release/bin/myapp stop Restart=always RestartSec=5 [Install] WantedBy=multi-user.target EOF # Run migrations before starting su - phoenix -c "/opt/myapp/release/bin/migrate" systemctl enable --now phoenix systemctl status phoenix
Step 5: Nginx for Phoenix + WebSockets
<code">cat > /etc/nginx/sites-available/phoenix << 'EOF'
upstream phoenix {
server 127.0.0.1:4000;
keepalive 32;
}
server {
listen 443 ssl http2;
server_name yourdomain.com;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
# WebSocket upgrade support (required for Phoenix Channels + LiveView)
location /socket/ {
proxy_pass http://phoenix;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 86400s; # Long timeout for persistent WS connections
}
# LiveView WebSocket
location /live/ {
proxy_pass http://phoenix;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 86400s;
}
# Regular HTTP requests
location / {
proxy_pass http://phoenix;
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;
}
}
EOF
ln -s /etc/nginx/sites-available/phoenix /etc/nginx/sites-enabled/
certbot --nginx -d yourdomain.com
nginx -t && systemctl reload nginxStep 6: Phoenix LiveView Example
<code"># lib/myapp_web/live/dashboard_live.ex
defmodule MyappWeb.DashboardLive do
use MyappWeb, :live_view
@impl true
def mount(_params, _session, socket) do
if connected?(socket) do
# Subscribe to real-time updates
Phoenix.PubSub.subscribe(Myapp.PubSub, "metrics")
:timer.send_interval(5000, self(), :update_metrics)
end
{:ok, assign(socket, metrics: fetch_metrics())}
end
@impl true
def handle_info(:update_metrics, socket) do
{:noreply, assign(socket, metrics: fetch_metrics())}
end
@impl true
def handle_info({:new_order, order}, socket) do
# Real-time push when new order arrives — no page refresh
{:noreply, update(socket, :orders, &[order | &1])}
end
defp fetch_metrics do
%{
active_users: Myapp.Analytics.active_users(),
orders_today: Myapp.Orders.count_today(),
revenue_today: Myapp.Orders.revenue_today()
}
end
@impl true
def render(assigns) do
~H"""
Live Dashboard — Updates automatically
“”” end end
Concurrency Benchmarks on HK VPS
| Metric | Phoenix (BEAM) | Node.js | Go |
|---|---|---|---|
| WebSocket connections (4 GB VPS) | 100,000+ | 10,000–30,000 | 50,000–100,000 |
| Memory per connection | ~2 KB | ~50 KB | ~8 KB |
| Process restart on crash | Automatic (OTP) | PM2/systemd | systemd |
| HTTP requests/sec (4 vCPU) | 30,000–80,000 | 15,000–30,000 | 40,000–80,000 |
Conclusion
Elixir Phoenix on a Hong Kong VPS is uniquely suited for real-time applications targeting Asia-Pacific users — the BEAM VM handles concurrency that would require horizontal scaling in other stacks, Phoenix LiveView enables reactive UIs without JavaScript frameworks, and CN2 GIA routing delivers WebSocket connections to mainland Chinese users at under 60ms latency. For collaborative tools, live dashboards, or any application where real-time updates matter, the Phoenix/BEAM stack on a Hong Kong VPS is a compelling choice.
Run real-time apps: Browse Server.HK Hong Kong VPS plans — a 4 GB plan handles 50,000+ concurrent Phoenix WebSocket connections for most real-time applications.