Hey there, we’re going to walk through deploying a Phoenix app to a DigitalOcean droplet, manually - no tools no nothing. Just straight up BASH! But don’t worry, I’ve done the hard work so you don’t have to.
It occurred to me this week after seeing some devs on twitter arguing about vercel/self-hosted/serveless that I have ever deployed a Phoenix app myself, by hand, the good old fashioned way. I’ve always used a PaaS, be that Gigalixir, Heroku, Render, Railway, Fly. The last time I manually deployed something was with PHP in 2012.
Maybe the newer generation of devs know no other way, so I want you to take 30 minutes of your day today, give this a shot, and take a peek at how the sausage is made. It’s not as scary as you think!
This is not a production ready server. Missing: firewals, security, domains, ssl, rolling deploys, backups, etc.
The Setup: Creating Our Phoenix Project
First things first, let’s whip up a new Phoenix project. We’re going with LiveView because, well, it’s awesome, and we’re using binary IDs because we’re cool like that. Fire up your terminal and let’s get cracking:
mix phx.new pokemon --binary-id --live
cd pokemon
Setting Up Our Digital Ocean Droplet
Alright, now we’re going to set up our cozy little home in the cloud. Head over to DigitalOcean and create a new Droplet. Here’s what you want:
- Region: New York
- Datacenter: 1
- OS: Ubuntu 24.04 LTS x264
- Plan: Regular SSD, $4/month
Set up root password authentication and make sure to copy that IPv4 address. You’re gonna need it!
Connecting to Our Droplet
Time to make friends with our new Droplet. In your terminal, type:
ssh root@your-ip
Accept that SSH key fingerprint (it’s cool, trust me) and enter your root password. Boom! You’re in.
Updating Our System
Let’s make sure our new cloud home is up to date:
sudo apt update && sudo apt upgrade -y
Installing PostgreSQL 16
Now, we need a place to store all our Pokémon data. Enter PostgreSQL 16:
sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
sudo apt update
sudo apt install postgresql-16 -y
Start it up and make sure it runs on boot:
sudo systemctl start postgresql
sudo systemctl enable postgresql
Configuring PostgreSQL
Time to set up our database. We’re going with ‘pokemon’ as our password because, well, why not?
sudo -u postgres psql -c "ALTER USER postgres PASSWORD 'pokemon';"
Now, let’s make PostgreSQL accessible from anywhere (don’t worry, we’ll secure it later):
sudo nano /etc/postgresql/16/main/postgresql.conf
# Change: listen_addresses = '*'
sudo nano /etc/postgresql/16/main/pg_hba.conf
# Add: host all all 0.0.0.0/0 md5
Restart PostgreSQL and open up the firewall:
sudo systemctl restart postgresql
sudo ufw allow 5432/tcp
Create our app’s database:
sudo -u postgres psql
CREATE DATABASE pokemon;
CREATE USER pokemon WITH PASSWORD 'pokemon';
GRANT ALL PRIVILEGES ON DATABASE pokemon TO pokemon;
\q
Installing Dependencies
Our Droplet needs some tools to run our Phoenix app:
sudo apt install -y build-essential inotify-tools
sudo apt install elixir
Setting Up Caddy
We’re using Caddy as our web server because it’s simple and awesome. Let’s install it:
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy
Start it up:
sudo systemctl start caddy
sudo systemctl enable caddy
Preparing Our App for Deployment
Back on your local machine, let’s get our Phoenix app ready for the big leagues:
mix assets.deploy
MIX_ENV=prod mix compile
MIX_ENV=prod mix phx.digest
MIX_ENV=prod mix release
tar -czf pokemon.tar.gz -C _build/prod/rel/pokemon .
Now, let’s send it to our Droplet:
scp pokemon.tar.gz root@your_droplet_ip:/opt/pokemon/
Deploying Our App
On the Droplet, extract our app:
tar -xzf /opt/pokemon/pokemon.tar.gz -C /opt/pokemon
Configure Caddy to serve our app:
sudo nano /etc/caddy/Caddyfile
Add this to the Caddyfile:
:80 {
root * /opt/pokemon
file_server
try_files {path} {path}/ /index.html
reverse_proxy localhost:4000
}
Restart Caddy:
sudo systemctl restart caddy
Finally, start our Phoenix app:
/opt/pokemon/bin/pokemon start
And there you have it, folks! Your Phoenix app should now be live on your Droplet’s IP address. Go ahead, give it a visit in your browser. You should see your app running in all its glory!
So now you guys know how the sausage is made. You’ve peeked behind the curtain and perhaps gained new appreciation to what PaaS providers abstract away for you. Or maybe you decided this is totally easy and something you can handle yourself. Either way the choice is your now and you’re informed.
Where to next?
The next step beyond this simple first implementation would be to use something like Kamal or Coolify. Beyond that use a PaaS like Fly.io