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

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
dogweather
I just finished a long process of configuring and debugging a Docker Compose-based dev environment. Several late-night hours! I think I’v...
New
crockwave
To integrate dropdown menus in a Phoenix Liveview app, you can use a combination of js, Hooks, CSS and your .leex and .ex code. You can...
New
berts-4865
Here is a quick guide to uploading a file from the browser to DO spaces. It is crude, but will hopefully save sometime time and frustrat...
New
tomciopp
TLS 1.3 has been out for a little over a year now, but it has been unavailable in Phoenix due to erlang’s handling of ssl. With the most ...
New
Zurga
In a quest to optimize the amount of data sent between the server and client I recently decided to try to use MessagePack instead of JSON...
New
drapermd
So here is the code I came up with to generically generate an array param that will be stored on a jsonb property in ecto. It only handl...
New
emilsoman
Hello, While answering a StackOverflow question on how to debug an elixir node running remotely, I thought it may be helpful to write a...
New
lukertty
Install web-mode and mmm-mode first and put this in your config file: (require 'mmm-mode) (require 'web-mode) (setq mmm-global-mode 'may...
New
bitli
In case this is handy for other people, here is how you can run Elixir on Android: Install https://termux.com/ apt update; apt upgrad...
New

Other popular topics Top

albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
grych
Hi folks, Few months ago I have announced the proof-of-concept of the library to manipulate the browsers DOM objects directly from Elixi...
639 49522 488
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
malloryerik
Hi, this is for people who, like me, have had some friction using .html.heex templates in VSCode. The solution seems to be, in a hyphena...
New
quazar
How to set Jason to encode all fields in ecto schema, I don’t care about security and implementing only is taking long list of attributes...
New
Jim
As a follow up to my earlier question: I have the code compiling and running but not getting a successful login from the rest server. ...
New
johnnyicon
Hi all, I've just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I'm trying to use Postg...
New
beno
I will often find my self writing things similar to: case some_value do nil -> something() "" -> something() _ -> someth...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers' Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New

We're in Beta

About us Mission Statement