Deploy de Phoenix no Heroku

Carlos Souza
January 12, 2024

Prestes a completar 15 anos 🎂, o Heroku continua sendo a minha escolha preferida para publicação rápida de aplicações na nuvem. Neste artigo, iremos aprender como publicar uma aplicação Elixir/Phoenix em sua infraestrutura utilizando buildpacks.

Antes de mais nada, é preciso entender alguns termos essenciais do Heroku:

  1. Dynos
  2. Slugs
  3. Buildpacks

Estes são os nomes dados pelo Heroku para alguns conceitos relacionados a tecnologia de containers.

Dynos

O Heroku utiliza containers para todas as aplicações publicadas e rodando em sua PAAS. Containers no Heroku são chamados de dynos. Podemos escolher entre diferentes tipos de dynos, cada um com uma determinada configuração. Existe um tipo gratuito e que costuma ser suficiente para experimentação e testes. O maior inconveniente deste tipo é o fato do dyno ser desligado após 30 minutos de inatividade, e o tempo de startup do primeiro request para “acordar” o dyno costuma demorar alguns segundos.

Slugs

Após dado um git push para o Heroku, as aplicações são transformadas em um artefato otimizado para rodar em um dyno. Este artefato é chamado de slug, e o responsável por esta transformação é o slug compiler.

Buildpacks

O slug compiler executa um conjunto de scripts. Estes scripts são conhecidos como buildpacks. Os buildpacks variam de acordo com a linguagem de programação, e uma aplicação pode utilizar mais do que um buildpack — como no caso de uma aplicação web Phoenix.

Realizando o Deployment

A partir de uma aplicação Phoenix rodando localmente e comunicando-se com um banco de dados, os seguintes passos são executados até que a aplicação esteja publicada no Heroku e disponível para acesso público:

1- Criação de um dyno

2- Configuração de buildpacks

3- Configuração de ENVs e secrets

4- Comando de deploy (spoiler: é o git push)

O código fonte completo da aplicação de exemplo que usaremos neste post está disponível neste link.

Criação de um dyno

A partir da raíz do projeto, rodamos o comando a seguir

heroku create --buildpack hashnuke/elixir

Este comando cria um novo dyno e já de imediato adiciona o buildpack de Elixir para as configurações de runtime. Caso a aplicação esteja versionada com o git, o novo remote é automaticamente adicionado às configurações locais. Após a conclusão do comando, o comando git remote -v deverá listar, além do origin, um remote de nome heroku.

Configurações de buildpacks

É recomendado que as versões de Elixir e Erlang utilizadas pela aplicação sejam declaradas de forma explícita. Isto é feito através do arquivo elixir_buildpack.config. Neste arquivo, localizado na raíz do projeto, definimos estas versões e mais uma configuração adicional com as linhas a seguir:

# elixir_buildpack.config
erlang_version=21.2.5
elixir_version=1.8.1
always_rebuild=true

A opção always_rebuild=true garante que as dependências sejam re-compiladas a cada deploy. Isso aumenta um pouco o tempo de deploy, mas por outro lado, ajuda a evitar erros oriundos de versões antigas de dependências.

Para que uma aplicação Phoenix rode com sucesso, alguns passos extras são necessários envolvendo a compilação dos assets e a inicialização do servidor de aplicação. Para nossa sorte, a simples adição do buildpack Phoenix Static resolve esta questão.

O comando a seguir instala o buildpack no nosso dyno:

heroku buildpacks:add https://github.com/gjaldon/heroku-buildpack-phoenix-static.git

Como fizemos para as versões de Erlang e Elixir utilizadas pelo buildpack anterior, é recomendado também que sejamos explícitos na versão do Node.js utilizada por este novo buildpack. Crie um arquivo phoenix_static_buildpack.config na raíz do projeto e adicione a linha a seguir a este arquivo:

# phoenix_static_buildpack.config
node_version=10.20.1

Concluímos as configurações de buildpacks necessárias.

Configuração de variáveis de ambiente e secrets

Alguns dos arquivos gerados pelo Phoenix precisam ser editados antes que a aplicação rode no Heroku. Os arquivos a serem editados são os seguintes:

  1. config/prod.exs
  2. config/prod.secret.exs
  3. lib/guitar_store_web/endpoint.ex

No arquivo config/prod.exs iremos declarar a URL da nossa aplicação e o port utilizado para conexões seguras HTTPS. A URL da nossa aplicação foi retornada pelo primeiro comando que rodamos ao criar o dyno, mas também pode ser exibida através do comando heroku info.

Copie o valor da URL sem o protocolo e substitua a linha deste trecho de código

url: [host: "example.com", port: 80],

com o código a seguir:

http: [port: {:system, "PORT"}],
url: [scheme: "https", host: "<valor-da-URL-sem-o-protocolo>", port: 443],
force_ssl: [rewrite_on: [:x_forwarded_proto]],

O trecho final de código deverá ficar da seguinte forma:

config :guitar_store, GuitarStoreWeb.Endpoint,
http: [port: {:system, "PORT"}],
url: [scheme: "https", host: "valor-da-URL-sem-o-protocolo>", port: 443],
force_ssl: [rewrite_on: [:x_forwarded_proto]],
cache_static_manifest: "priv/static/cache_manifest.json"

Para completar a configuração de SSL, abra o arquivo config/prod.secret.exs e descomente a linha # ssl: true. Feito isto, o trecho de código deverá ficar assim:

config :guitar_store, GuitarStore.Repo,
ssl: true,
url: database_url,
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")

Finalmente, uma última modificação para adicionar suporte a Websockets. No arquivo lib/guitar_store/endpoint.ex, mude o seguinte trecho

socket "/socket", GuitarStoreWeb.UserSocket,
websocket: true,
longpoll: false

para o código a seguir:

socket "/socket", GuitarStoreWeb.UserSocket,
websocket: [timeout: 45_000],
longpoll: false

Isto ativa suporte a Websockets com um timeout de 45 segundos - 10 segundos a menos do que o timeout atual de 55 segundos do Heroku para inatividade em conexões.

Pronto?… quase ;) Concluímos as modificações na aplicação. Agora precisamos adicionar um banco de dados ao nosso dyno e configurar as variáveis de ambiente necessárias para que nossa aplicação se conecte a ele. Precisamos também configurar uma chave secreta 🔐, responsável pela criação e proteção das sessões dos usuários conectados.

Execute os seguintes comandos para criar e configurar um banco de dados relacional no Heroku:

heroku addons:create heroku-postgresql:hobby-dev
heroku config:set POOL_SIZE=18

Para gerar uma chave secreta, podemos utilizar uma task do próprio Phoenix. Rode o seguinte comando:

mix phx.gen.secret

O resultado deste comando é a nossa chave secreta 🔐. Copie este valor e o utilize no comando a seguir, que configura o valor da chave como variável de ambiente no nosso dyno:

heroku config:set SECRET_KEY_BASE="<valor-da-chave-secreta>"

Pronto! :) O próximo passo é publicar a nossa aplicação.

Comando de deploy

Para publicar nossa aplicação, basta um git push para o repositório criado pelo Heroku para a nossa aplicação, e que foi adicionado à nossa configuração local. Crie um commit com as últimas mudanças:

git add .
git commit -m "Deploy to Heroku"

Verifique que todos os arquivos editados foram adicionados ao commit e que não há arquivos staged ou untracked pelo git, e rode o comando:

git push heroku main

Se tudo der certo (🤞), após alguns instantes a aplicação estará publicada!

O último passo antes que possamos acessar nossa aplicação é a execução das migrations, criando as tabelas necessárias no banco de dados. Rode o comando a seguir:

heroku run "POOL_SIZE=2 mix ecto.migrate"

Agora está pronto de verdade! Para acessar a aplicação, visite a URL ou então rode o comando heroku open. Este comando abre o browser padrão e visita a nossa aplicação no endereço de produção.

Conclusão

Neste post, aprendemos como publicar aplicações Phoenix no Heroku utilizando buildpacks. Além desta estratégia, também é possível publicar aplicações através de um Dockerfile. A estratégia com Dockerfile pode parecer um pouco mais trabalhosa, mas oferece maior controle sobre o processo de compilação e geração de artefatos, o que pode valer a pena dependendo do seu caso.

Espero que este post tenha sido útil e que possa ajudar você a publicar seus projetos Phoenix para o mundo! \o/