#205: Deploy Phoenix to a VPS in Minutes
Deploying Elixir apps to a PaaS is easy but expensive and while a VPS is cheaper, setting up the server, SSL, and Postgres can be a headache. That’s why I built Potions.io - VPS deployments with zero-downtime releases, out of the box. In fact, ElixirCasts is deployed with Potions. In this episode, I’ll show you how to deploy your app too.
Before we can deploy, we’ll need an app. Let’s create a simple guessing game - and because this is a guessing game written in Elixir we’ll call it pattern_match.
$ mix phx.new pattern_match
Once that finishes, we’ll move into the project and get it running.
$ cd pattern_match
$ mix ecto.create
The database for PatternMatch.Repo has been created
$ mix phx.server
Then if we open localhost:4000 in the browser, we’ve got the default Phoenix landing page. Let’s swap this out for the name of our app. We’ll open lib/pattern_match_web/controllers/page_html/home.html.heex and update it to have a heading with the name “Pattern Match”.
<Layouts.flash_group flash={@flash} />
<h1 class="flex items-center justify-center text-2xl font-semibold">
Pattern Match
</h1>
Back in the browser, if we refresh the page - we see our changes. Great, now before we build out our game, let’s deploy our app to Potions.
Over in the Potions dashboard, we’ll create a new server called pattern-match-prod. We’ll select a region and leave the default size and PostgreSQL version.
Provisioning can take a few minutes, so while that’s running, let’s get our app release-ready since Potions will build and deploy our app as an Elixir release. Let’s go to the command line, and run mix phx.gen.release.
$ mix phx.gen.release
* creating rel/overlays/bin/server
* creating rel/overlays/bin/server.bat
* creating rel/overlays/bin/migrate
* creating rel/overlays/bin/migrate.bat
* creating lib/pattern_match/release.ex
That gives us a few things. like server and migrate scripts and the release.ex module brings everything together. If we look at the release.ex module we see that it defines a migrate function that loads your app, starts the repo, and runs pending migrations. There’s also a rollback helper for rolling a specific repo back to a given version.
Alright, with that added our app is ready to be deployed. Now we just need to tell Potions where to deploy it from, so let’s put this code on GitHub. I’ll create a new public repo called pattern_match and copy
the push commands it gives us.
Then back in the terminal, let’s see what’s unstaged.
$ git status
Nothing’s been committed yet, so we’ll add everything and make our initial commit.
$ git add .
$ git commit -m "initial commit"
I’ll clear the screen paste in the push command and we’re live on GitHub!
Now we can deploy this to Potions. Back in Potions, our server is done provisioning - it’s marked “Active” and we can see of the details about our server here. Alright now let’s add an app to our server. We’ll head to the “Apps” page and click “Add app.”
Potions is going to ask us to pick a GitHub repo, but since we just created this repo, Potions doesn’t have access to it yet. Let’s click “Manage repos.” this takes us over to GitHub, where we can choose which repos Potions can see. We’ll add pattern_match to the list.
Now back in Potions, when we open the repo dropdown, we see pattern_match! Let’s select it.
And Potions selects a few defaults. We’ve got the app name: pattern_match. The branch we’ll deploy from. Elixir and Erlang versions. And if our app needs Node, we can pick a Node version here too.
Once we add the app, Potions provisions a database for us and almost immediately the app is ready to deploy.
Before we deploy, let’s scroll down to the “Potions Domain” section. Every app you create on Potions gets a preview domain automatically - no configuration needed. This is great because it means we can view and share our app a soon as it’s deployed!
Alright, let’s go to the deployments page and click “Deploy.” That kicks off the build. We’ll let it run. And we’re green. The deployment succeeded!
Now if you’ve deployed a Phoenix app before you may be thinking - how did this work when we didn’t set things like SECRET_KEY_BASE, DATABASE_URL, or PHX_HOST? That’s because Potions sets the environment variables that a default Phoenix app needs. It also runs migrations, so for our app there’s not extra setup required.
Let’s go back to our Potions domain - and if we click through - we’re taken to our app! It’s been deployed to our own VPS in hardly any time!
Now that we have our app deployed, let’s build our game and to help us do that we’ll use Tidewave. We cover Tidewave in episode 202, so if you’ve never used it and you’re curious about it’s features, pause here and watch that episode.
To add Tidewave to our app, we’ll copy the config from Hex and add it into our mix.exs deps list.
defp deps do
[
# ...
{:tidewave, "~> 0.5.6"},
# ...
]
end
Then we’ll fetch it.
$ mix deps.get
Resolving Hex dependencies...
Resolution completed in 0.146s
New:
circular_buffer 1.0.0
tidewave 0.5.6
Next we’ll open endpoint.ex and plug Tidewave in development only.
if Mix.env() == :dev do
plug Tidewave
end
With that added let’s restart the dev server, and open Tidewave in the browser. And we’ll pick our framework Phoenix. We’ve already configured Tidewave, so we can click the “already set up” link. Then we just need to paste in http://localhost:4000 and click “Connect.”
Perfect, Tidewave loaded with our application! I’ll paste in the prompt to create our game. You can read through this, but we’re asking Tidewave to build a memory-style guessing game on a 4x4 grid. We’ll flip into Plan mode and let Tidewave run.
While Tidewave works on the plan, let’s jump back to Potions. Our first deploy, we triggered from the UI. But we can also have Potions deploy automatically whenever we push to our branch. Let’s head to the app settings and turn on auto-deploy.
When we’re ready to move past the onpotions.io preview domain, we can add a custom domain right from the “Domains” page. Potions handles the DNS verification and certificate provisioning for us.
We can also see the default environment variables Potions generated for the app. If we look at PHX_HOST, it’s already set to our Potions domain. For LiveView apps, PHX_HOST needs to match the URL the browser is connecting from, otherwise the websocket gets rejected and your LiveView won’t mount. Potions handles that automatically.
On the “Database” page we’ve got info about the database Potions provisioned for this app: like the name, the user, and the connection details. While you can always SSH into your server and restore a database manually, Potions has a built-in import feature right here in the UI. Along with configurable scheduled backups.
One feature I use a lot is the in-browser remote IEx console. We click “Connect,” and Potions gives us an IEx session attached to our app, right in the browser.
Alright, let’s check on Tidewave. It finished the plan, and because this is a demo, let’s go ahead and use it as-is and auto-accept edits. If you’re building a production app you’d want to review the plan before approving.
I’ll fast-forward while Tidewave implements this.
And we’re done. Tidewave built our guessing game Pattern Match. Let’s have it make one more change and update the default header with a “Deployed with Potions.io” banner. This looks great - it’s ready to redeploy.
Over in the terminal, we’ll check the status again.
$ git status
And then we’ll add and commit our changes.
$ git add .
$ git commit -m "adds pattern match game"
Now to deploy, all we need to do is push these changes to GitHub.
$ git push origin main
And because we enabled auto-deploy, if we go back to our deployments page, our app is already building! Potions picked up the push and started the deployment. And subsequent deploys are much faster because Potions caches the build.
Once it’s green, we’ll go back to our Potions URL refresh the page and there’s Pattern Match, live with our new game!
That’s it. We created a Phoenix app, deployed it to Potions on our own server, pulled in Tidewave to build a real game, and shipped the changes. Potions made it easy to deploy our app to a VPS we own, with hardly any setup.