sergio

sergio

Deploy Your Phoenix App on DigitalOcean in 30 Minutes

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

Most Liked

MRdotB

MRdotB

I created a video tutorial on deploying a Phoenix app using Kamal on DigitalOcean. The deployment setup includes two droplets running the phoenix app, a managed PostgreSQL, and a load balancer in front.

pdgonzalez872

pdgonzalez872

This is awesome. I wrote something similar a little while ago and it has a little bit more on the security stuff: here is the link.

Sometimes it’s great to take a step back and see what really is needed to get something up. Lots of abstractions today. Thanks for writing this!

pdgonzalez872

pdgonzalez872

Indeed, I rewrote my blog a few weeks ago and that changed the routing. Here is the link: PG's Blog

Hope it helps!

PS: I can’t edit the post above to fix the link, I tried.

MRdotB

MRdotB

Unfortunately I don’t have the rush anymore. This was my first video tutorial and probably my last.

Anyway it’s a bit outdated now since they release a major update on kamal it seems it’s more agnostic now and it should be easier to deploy a phoenix app.

kairos0ne

kairos0ne

Cowboys…

Where Next?

Popular in Guides/Tuts Top

OndrejValenta
Me and my boys started a new website specifically designed for other ASP.NET programmers that struggle, as we do, with their transformati...
New
hlx
Typed my very first blog post ever, hope it will help some people https://henricus.xyz/roll-your-own-email-password-authentication-with...
New
hauleth
Some time ago someone suggested me to write article about how I have configured my Vim to work with Elixir, now there it is: https://med...
New
sergio
https://sergiotapia.me/generate-images-with-name-initials-using-elixir-and-imagemagick-374eca4d14ff Hope this saves you guys some time!...
New
dgamidov
Hi folks, Just a short instruction. Maybe it will help somebody. https://v4-alpha.getbootstrap.com/getting-started Install boostrap...
New
danschultzer
I wrote this blog post based on our experiences setting up continuous delivery for our first production umbrella Phoenix app. Coming from...
New
f0rest8
Hi, TLDR: form attribute set on the input fields and button submit. I just wanted to share a solution I discovered when making live inl...
New
dennisreimann
I wrote a guide for implementing Passwordless Authentication a.k.a. "Magic Login Links": Feedback welcome!
New
AstonJ
With a few questions relating to this recently, I wonder if it might help having a thread where we share how we’ve set up Elixir/Erlang/P...
New
niku
I have published an elixir project with using Travis CI. I would like to share some tips & thoughts that I was getting through this ...
New

Other popular topics Top

William
I would like to know that is there any online source for learning Phoenix Framework for building E-Commerce Store? Any advantage on build...
New
AstonJ
You’re a programmer, so you don’t need spoon feeding with the conventional drivel about “this is an integer.” No. You need to know what’s...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
lucidguppy
I have a super simple question about elixir - how would I take a file like this foo bar baz and output a new file that enumerates th...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New

We're in Beta

About us Mission Statement