Prerequisites
Phoenix is a high-performance web framework built on Elixir and the Erlang VM (BEAM), designed for building scalable, fault-tolerant real-time applications. Its lightweight process model and built-in WebSocket support via Phoenix Channels make it ideal for chat apps, live dashboards, IoT backends, and low-latency APIs.
- A RamNode KVM VPS with Ubuntu 24.04 LTS installed
- Root SSH access to your server
- A registered domain name with DNS pointed to your VPS IP address
- A Phoenix application ready for deployment (Phoenix 1.7+)
- Basic familiarity with Linux command line and Elixir/Phoenix development
💡 Recommended VPS Plan
A RamNode KVM 2GB plan ($10/month) provides ample resources for most Phoenix applications. The BEAM VM is highly memory-efficient — a typical Phoenix app serves thousands of concurrent connections on 1–2 GB of RAM. For high-traffic or LiveView-heavy applications, consider the 4GB plan.
| Use Case | Plan | RAM | Best For |
|---|---|---|---|
| Small API / personal project | $5/mo | 1 GB | REST/GraphQL APIs, small LiveView apps |
| Production web app | $20/mo | 4 GB | Full-stack Phoenix with LiveView |
| High-concurrency realtime | $40/mo+ | 8 GB+ | Chat, IoT, high-traffic Channels |
Initial Server Setup
sudo apt update && sudo apt upgrade -y
sudo apt install -y build-essential git curl wget \
autoconf m4 libncurses-dev libssl-dev \
libwxgtk3.2-dev libgl1-mesa-dev libglu1-mesa-dev \
libpng-dev libssh-dev unixodbc-dev xsltproc fopCreate a Deploy User
Avoid running your application as root. Create a dedicated deploy user with sudo privileges.
sudo adduser deploy
sudo usermod -aG sudo deploy
sudo su - deployConfigure the Firewall
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw statusInstall Erlang and Elixir
We recommend installing Erlang and Elixir via asdf version manager for flexible version management in production.
Install asdf
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 ~/.bashrcInstall Erlang
asdf plugin add erlang
asdf install erlang 27.2
asdf global erlang 27.2⚠️ Note: Erlang compilation takes 10–15 minutes on a 2-core VPS. This is normal.
Install Elixir
asdf plugin add elixir
asdf install elixir 1.17.3-otp-27
asdf global elixir 1.17.3-otp-27Verify Installation
elixir --version
# Expected output:
# Erlang/OTP 27 [erts-15.x]
# Elixir 1.17.3 (compiled with Erlang/OTP 27)Install Node.js
Phoenix uses esbuild by default for asset bundling, but Node.js is still needed for certain dependencies and tooling.
asdf plugin add nodejs
asdf install nodejs 22.12.0
asdf global nodejs 22.12.0
node --versionInstall and Configure PostgreSQL
PostgreSQL is the recommended database for Phoenix applications.
sudo apt install -y postgresql postgresql-contrib
sudo systemctl enable postgresql
sudo systemctl start postgresqlCreate Database and User
sudo -u postgres psql
CREATE USER myapp WITH PASSWORD 'your_secure_password';
CREATE DATABASE myapp_prod OWNER myapp;
GRANT ALL PRIVILEGES ON DATABASE myapp_prod TO myapp;
\q🔒 Security Note: Use a strong, randomly generated password. Generate one with: openssl rand -base64 32
Build Your Phoenix Release
Phoenix releases are self-contained packages that include the Erlang runtime, your compiled application, and all dependencies. This is the recommended approach for production.
Clone Your Application
cd /home/deploy
git clone https://github.com/youruser/myapp.git
cd myappSet Environment Variables
export DATABASE_URL="ecto://myapp:your_secure_password@localhost/myapp_prod"
export SECRET_KEY_BASE="$(mix phx.gen.secret)"
export PHX_HOST="yourdomain.com"
export PHX_SERVER=true
export PORT=4000
export MIX_ENV=prodchmod 600 /home/deploy/.env.prod
source /home/deploy/.env.prodCompile and Build the Release
export MIX_ENV=prod
# Install dependencies
mix local.hex --force
mix local.rebar --force
mix deps.get --only prod
# Compile
mix compile
# Build assets
mix assets.deploy
# Build the release
mix releaseRun Database Migrations
_build/prod/rel/myapp/bin/myapp eval \
"MyApp.Release.migrate()"Configure systemd Service
Use systemd to manage your Phoenix application as a service, ensuring it starts on boot and restarts on failure.
[Unit]
Description=MyApp Phoenix Application
After=network.target postgresql.service
Requires=postgresql.service
[Service]
Type=exec
User=deploy
Group=deploy
WorkingDirectory=/home/deploy/myapp
EnvironmentFile=/home/deploy/.env.prod
ExecStart=/home/deploy/myapp/_build/prod/rel/myapp/bin/server
ExecStop=/home/deploy/myapp/_build/prod/rel/myapp/bin/myapp stop
Restart=on-failure
RestartSec=5
SyslogIdentifier=myapp
RemainAfterExit=no
# Hardening
NoNewPrivileges=true
ProtectSystem=full
ProtectHome=read-only
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable myapp
sudo systemctl start myapp
sudo systemctl status myappNginx Reverse Proxy
Nginx serves as a reverse proxy in front of your Phoenix application, handling SSL termination, static file serving, and WebSocket upgrades for Phoenix Channels and LiveView.
sudo apt install -y nginx
sudo systemctl enable nginxupstream phoenix {
server 127.0.0.1:4000;
}
server {
listen 80;
server_name yourdomain.com;
location / {
proxy_pass http://phoenix;
proxy_http_version 1.1;
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;
# WebSocket support (Channels & LiveView)
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400;
}
# Static files (served by Nginx)
location /assets/ {
alias /home/deploy/myapp/priv/static/assets/;
gzip_static on;
expires max;
add_header Cache-Control public;
}
}sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginxSSL with Let's Encrypt
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com
# Verify auto-renewal is configured
sudo certbot renew --dry-runCertbot automatically modifies your Nginx configuration to enable HTTPS and set up a redirect from HTTP. Certificates auto-renew via a systemd timer.
Automated Deployment Script
#!/bin/bash
set -e
APP_DIR=/home/deploy/myapp
source /home/deploy/.env.prod
echo "==> Pulling latest code..."
cd $APP_DIR
git pull origin main
echo "==> Installing dependencies..."
mix deps.get --only prod
echo "==> Compiling..."
mix compile
echo "==> Building assets..."
mix assets.deploy
echo "==> Building release..."
mix release --overwrite
echo "==> Running migrations..."
_build/prod/rel/myapp/bin/myapp eval 'MyApp.Release.migrate()'
echo "==> Restarting service..."
sudo systemctl restart myapp
echo "==> Deployment complete!"chmod +x /home/deploy/deploy.sh
# Run whenever you push updates
/home/deploy/deploy.shMonitoring & Maintenance
Application Logs
# View live logs
sudo journalctl -u myapp -f
# View last 100 lines
sudo journalctl -u myapp -n 100 --no-pagerBEAM Remote Console
Elixir releases include a remote console for live debugging and inspection:
_build/prod/rel/myapp/bin/myapp remote
# Inside the console:
iex> :observer_cli.start() # if observer_cli is a dependency
iex> Process.list() |> length()Health Check Endpoint
Add a health check route for uptime monitoring:
defmodule MyAppWeb.HealthController do
use MyAppWeb, :controller
def index(conn, _params), do: json(conn, %{status: "ok"})
end
# In your router:
# get "/health", HealthController, :indexBEAM VM Configuration
Add to rel/vm.args.eex to optimize the BEAM VM for your VPS:
# Scheduler configuration
+S 2:2 # Match your VPS CPU cores
+sbt db # Bind schedulers to CPUs
+swt very_low # Low scheduler wakeup threshold
# Memory
+MBas aobf # Allocation strategy
+MBlmbcs 512 # Largest multi-block carrier size
# Network
+A 64 # Async thread pool sizePostgreSQL Tuning (2GB VPS)
| Parameter | Default | Recommended | Purpose |
|---|---|---|---|
| shared_buffers | 128MB | 512MB | Shared memory for caching |
| effective_cache_size | 4GB | 1GB | Query planner memory estimate |
| work_mem | 4MB | 8MB | Per-operation sort/hash memory |
| maintenance_work_mem | 64MB | 128MB | Maintenance operations memory |
| max_connections | 100 | 50 | Match Ecto pool size + overhead |
Troubleshooting
App won't start
Run sudo journalctl -u myapp -n 50 to check logs. Verify env vars in .env.prod — ensure DATABASE_URL and SECRET_KEY_BASE are set correctly.
502 Bad Gateway
Run curl http://localhost:4000 to confirm the Phoenix app is running and bound to port 4000. Check systemd status with sudo systemctl status myapp.
WebSocket errors
Check browser dev console. Verify Nginx proxy_set_header Upgrade and Connection headers are set in the site config.
Database connection refused
Run sudo systemctl status postgresql. Check pg_hba.conf allows local connections and verify credentials match .env.prod.
Asset 404 errors
Run ls priv/static/assets/ to verify assets exist. Run mix assets.deploy before building the release. Check the Nginx alias path matches.
High memory usage
Use :erlang.memory() in the remote console. Tune pool_size in your Repo config. Check for process leaks with Process.list().
Phoenix Application Deployed Successfully!
Your Phoenix application is now running in production on a RamNode VPS with PostgreSQL, Nginx reverse proxy, SSL encryption, and automated deployments.
Next Steps:
- Set up CI/CD with GitHub Actions for automated deployments
- Configure log rotation with
logrotate - Add APM with AppSignal, New Relic, or Prometheus + Grafana
- Implement database backups with
pg_dumpon a cron schedule - Explore Phoenix LiveView for real-time UI without JavaScript
- Scale horizontally with multiple VPS nodes and
libclusterfor distributed Erlang
