Deploy a Phoenix Application to Google App Engine
There aren’t many options available for quick and easy deployments of Elixir / Phoenix applications, but if you want to make use of Google’s excellent infra, Google App Engine might be a good choice for you. Without the hassle of setting up VPS instances, installing dependencies, maintaining a registry of Docker images or provisioning Kubernetes clusters, App Engine is an attractive option for simple deployments.
By the end of this guide, you should be able to build a release of your Phoenix app, set up a Postgres database on GCP using Cloud SQL Proxy and deploy your app to App Engine.
Costs can also quickly get out of hand on the Google Cloud platform, so I highly suggest you estimate the charges of each of the services before creating them using Google’s pricing calculator, as to not accrue unexpected charges to your billing account.
Create the DB with Cloud SQL
We need to create a production Postgres database on Google Cloud Platform, that our app can connect to.
-
Create a Project in Google Cloud Console (if you haven’t already). Note down the project ID.
-
Open Cloud SQL and create a new Postgres instance and database.
-
Give it a unique instance ID, and a password which we’ll use to connect to it later.
-
Set the Machine Type and Storage. Again, make sure you understand the pricing.
-
Note down the region as well when selecting it, we’ll need this too.
-
Once the instance is created, select “Databases” in the left sidebar and create the database for the app (The name is usually in the format
<APPNAME>_prod
, but you can choose anything you like).
-
-
Using the above values, find your connection id:
-
It should be
<PROJECT-ID>:<REGION>:<INSTANCE-ID>
-
You can also run Cloud SQL Proxy locally1 to figure that out and even connect to the database to test it:
# Make the connection available locally via socket $ cloud_sql_proxy -dir=/tmp/gcp-sql # Find the connection id $ ls /tmp/gcp-sql/ # Connect to it via psql (or alternatively any Postgres GUI) $ psql -h /tmp/gcp-sql/<CONNECTION-ID> -U postgres
-
To add an extra level of security, you might want to create a dedicated database user, just for the application.
Now back in your codebase, update config/prod.secret.exs
to reference the
database we just created:
config :my_app, MyApp.Repo,
socket_dir: "/cloudsql/<CONNECTION-ID>",
username: "postgres",
password: "<DB-PASSWORD>",
database: "<DB-NAME>",
pool_size: 15
The database connection will later be exposed at this path automatically via our App Engine configuration.
Build a Release
If you haven’t upgraded to Elixir 1.9+ yet, I suggest you do so now. Elixir 1.9 brought with it built-in releases, which removes the reliance on external packages like Distillery and also simplifies the whole process. While Elixir does allow you to configure releases with many available options, you can get away by simply running one command:
$ MIX_ENV=prod mix release
This will spit out a compiled binary of your application in _build/prod/rel
.
To test that it works correctly, you can start the app:
$ _build/prod/rel/my_app/bin/my_app start
Visiting localhost:4000
in your browser will work like before. You can find a
full list of options and more information on HexDocs.
Create a Docker Image
Since Elixir isn’t one of the supported languages in Google App Engine’s
Standard Environment, we have to go with the Flexible
Environment with a custom runtime2. This is made possible by
the use of a simple app.yaml
config file along with a custom Docker image of
your app.
To get started, create a Dockerfile
in the root folder of your app, and paste
this in:
# Set up Environment
# ------------------
FROM elixir:1.9.4-alpine
ENV APP_NAME my_app
ENV MIX_ENV prod
ENV PORT 8080
EXPOSE ${PORT}
# Install + Cache Dependencies
# -----------------------------
WORKDIR /source
RUN apk add --no-cache \
build-base \
nodejs \
nodejs-npm \
&& mix local.hex --force \
&& mix local.rebar --force
COPY mix.exs mix.lock config ./
COPY assets/package.json assets/package-lock.json ./assets/
RUN npm install --prefix assets
RUN mix do deps.get, deps.compile
# Compile and Release App
# -----------------------
COPY . .
RUN mix do compile, phx.digest
RUN mix release
# Serve the App
# -------------
WORKDIR /app
RUN cp -R /source/_build/${MIX_ENV}/rel/${APP_NAME}/* .
CMD ["bin/${APP_NAME}", "start"]
If you don’t need the source code to run mix tasks or other scripts, you can reduce the size of the final image by using multi-stage builds. See my post on Kubernetes deployments for an example of an advanced Docker image that uses them.
I also highly recommend you add a .dockerignore
file with the following
contents to speed up subsequent builds by ignoring temporary artifacts and other
irrelevant files:
# Elixir Artifacts
/_build/
/deps/
/doc/
/cover/
/.fetch
*.ez
my_app*.tar
erl_crash.dump
# Node Artifacts
npm-debug.log
/assets/node_modules/
# Irrelevant files
Dockerfile
/test/
/.iex.exs
We can now try building and running the docker image to check if everything works as expected. Depending on your application, you might have to perform some extra steps. Tag your image with a name and a version:
$ docker build -t my_app:v1 .
$ docker run -it --rm -p 8080:8080 my_app:v1
Visiting localhost:8080
in your browser should greet you with the application.
Notice that we’re using port 8080
here. This is because it’s the default port
Google App Engine expects our web server to be running on.
Deploy to App Engine
If you haven’t already installed the gcloud
SDK, install it and
log into your account:
$ gcloud auth login
$ gcloud config set project <PROJECT-ID>
Before we can finally deploy the application, we need to create an app.yaml
config file at the root of the project3:
env: flex
runtime: custom
beta_settings:
cloud_sql_instances: <CONNECTION-ID>
This will let App Engine know we’re using the flexible environment with a custom runtime, and to expose the database we created at the default path.
To deploy, just run:
$ gcloud app deploy
The first time you run this, it will ask you to select the region where you want to deploy (ideally this should be the same as the region you selected for the database). You’ll be able to see the full log while it uploads the source code, builds the image and exposes the web service.
If you run into any issues, the detailed logs in Cloud Build and Log Viewer can save you a lot of headache.
When done, you can open the app in the browser by running:
$ gcloud app browse
When you’ve updated the app and want to push out a newer version, just run the deploy command above again.
Run Migrations or Scripts
When deploying newer releases, you will occasionally want to run migrations or some arbitrary code and scripts (maybe also create the app database initially if you haven’t already done it).
Fortunately, App Engine is just a compute service underneath and lets you SSH into the VPS and the docker container running on it. Follow these commands to drop into a shell in the container:
# Find the instance id and version
$ gcloud app instances list
# SSH into the instance
$ gcloud app instances ssh <INSTANCE-ID> --service default --version <VERSION-ID>
# Connect to the docker container
$ docker exec -it "$(docker ps | grep -i 'bin.*start' | head -1 | awk '{print $1}')" /bin/sh
Note: The first time you run the ssh
command, it will generate a private
key for you. You can leave the passphrase empty but if you do set it, make sure
to save it for future reference.
To run mix tasks, open the source
directory and run them like you usually
would4:
$ cd /build
$ mix ecto.migrate
If you want to drop into the IEx shell, you can use the remote
command of the
Elixir release:
$ /app/bin/my_app remote
Concluding Notes
And that’s about it. YMMV obviously, depending on your app configuration or the way you want to do certain things. There are a lot more configuration options and guides available for each part of the DevOps cycle above:
- Elixir Releases
- Deploying Phoenix with Releases
- GAE Custom Runtime: Configuring your app
- Dockerfile Reference
- Cloud SQL Proxy is required to connect to any SQL database created on Google Cloud Platform. The only exception is for manually white-listed IPs.↩
- While there does exist a community sourced App Engine runtime for Elixir, I’ve found it very lacking in terms of customizability and it only fits the most basic Elixir and Phoenix applications.↩
- You can also have multiple versions of the config:
app.env-stage.conf
,app.job-queue.conf
, etc.↩ - If you don’t keep the source code in the final image, you can’t run mix
tasks through the command line. In that case, it’s recommended you
create a helper module which you can invoke via the
remote
orrpc
commands.↩