Sheharyar Naseer

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.

  1. Create a Project in Google Cloud Console (if you haven’t already). Note down the project ID.
  2. 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).
  3. 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:


  1. 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.

  2. 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.

  3. You can also have multiple versions of the config: app.env-stage.conf, app.job-queue.conf, etc.

  4. 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 or rpc commands.