Como publicar uma aplicação Phoenix na VPS com release Elixir e Docker

Ilustração de contêiner Docker conectado a servidor VPS executando aplicação Phoenix

Fala aí, beleza? Para seguir o guia oficial de releases do Phoenix com Docker numa VPS, você roda mix phx.gen.release --docker num projeto Phoenix 1.6.3 ou superior, e ele gera o Dockerfile, o .dockerignore e os scripts bin/server e bin/migrate

Depois é construir a imagem, que leva só a release (um diretório autocontido com a VM Erlang, o Elixir, o seu código e as dependências), e rodar o container na VPS passando SECRET_KEY_BASE, DATABASE_URL, PHX_HOST e PORT 🙂

O foco aqui NÃO é deploy genérico de container, é o artefato de release: o que entra nele, o que fica de fora e por que isso muda o jeito de configurar segredos e rodar migrações

Pra hospedar, a recomendação da Hora de Codar é a VPS da Hostinger, porque ela dá acesso root e oferece um template de sistema operacional com Docker pra aplicar pelo painel, o que facilita preparar o ambiente que esse fluxo pede (este artigo contém links de afiliado da Hostinger)

O que é uma release Phoenix e por que ela muda o deploy com Docker

A release é a sua aplicação empacotada num diretório autocontido, com a VM Erlang, o Elixir, todo o código e as dependências

Ela é montada com este comando e vai parar em _build/prod/rel/<app>:

MIX_ENV=prod mix release

E por que isso importa pro Docker? Se liga em duas consequências práticas

  • Mesmo SO e mesma arquitetura: dá pra copiar o conteúdo de _build/prod/rel/<app> e rodar em qualquer máquina de produção, desde que ela tenha o mesmo sistema operacional e a mesma arquitetura da máquina de build. O Docker ajuda a preparar o ambiente de build e execução, mas a imagem também precisa ser compatível com a arquitetura da VPS. Usar Debian nos dois estágios não elimina essa condição
  • O Mix não vai junto: a ferramenta de build fica de fora da release. Então nada de mix ecto.migrate em produção, os comandos que você precisa (como as migrações) têm que estar DENTRO da release

Se você já fez um deploy de Astro com Docker, a ideia de container é a mesma, mas o que vai dentro da imagem é bem diferente: aqui não sobe runtime de linguagem com gerenciador de pacotes, sobe um artefato fechado

Pré-requisitos para publicar Phoenix com release e Docker na VPS

Antes de começar, confere essa lista:

  • Projeto em Phoenix 1.6.3 ou superior: é a partir dessa versão que o mix phx.gen.release existe, pra gerar os arquivos de release e o Dockerfile
  • VPS com Ubuntu 22.04 LTS ou 24.04 LTS e pelo menos 2 GB de RAM: é o que o tutorial de Docker da Hostinger recomenda (o mínimo técnico citado ali é 512 MB, mas o recomendado é 2 GB)
  • Acesso root ao servidor, pra instalar e administrar o Docker
  • Docker no servidor: dá pra instalar manualmente no Ubuntu seguindo o tutorial da Hostinger, ou usar o template de sistema operacional com Docker disponível no hPanel, em VPS → Manage → OS & Panel → Operating System, na seção Change OS, buscando e selecionando o template Docker (segundo o tutorial deles, a instalação leva cerca de 10 minutos). Este guia pressupõe um servidor com Docker já preparado. Como recomendação de segurança, não aplique uma opção de Change OS em um servidor com aplicações e dados para seguir o exemplo; confira o procedimento do provedor e prepare backup antes de qualquer mudança de sistema
  • PostgreSQL acessível pela aplicação, com a URL no formato ecto://USER:PASS@HOST/DATABASE

Antes de aplicar esse template, vale dar uma olhada no tutorial oficial da Hostinger pra entender exatamente o que a troca de sistema operacional faz no servidor

Sobre a base do servidor: os planos de VPS da Hostinger (KVM 1, KVM 2, KVM 4 e KVM 8) usam processadores AMD EPYC e armazenamento SSD NVMe, o que é uma base coerente pra compilar a imagem e rodar a release na mesma máquina

Passo 1: gerar os arquivos de release e o Dockerfile com mix phx.gen.release --docker

Na raiz do projeto, roda:

mix phx.gen.release --docker

O resultado esperado são estes arquivos:

  • lib/app_name/release.ex: módulo com as tarefas de migração (só é gerado quando o projeto usa Ecto)
  • rel/overlays/bin/migrate e rel/overlays/bin/migrate.bat: scripts que rodam as migrações (o gerador cria os dois arquivos independente do sistema operacional em que você roda o comando; o .bat é pensado pra quem for operar em Windows)
  • rel/overlays/bin/server e rel/overlays/bin/server.bat: scripts que iniciam a release com o servidor Phoenix ligado via variável de ambiente (de novo, os dois saem juntos, o .bat é só pra ambiente Windows)
  • Dockerfile: pronto pra qualquer deploy Docker padrão
  • .dockerignore: com os padrões de Elixir

Se o seu projeto não usa Ecto, os arquivos de migração simplesmente não aparecem, e tudo bem

Quer outra versão de Erlang/OTP ou de Elixir nas imagens base? O gerador tem as opções otp e elixir pra sobrescrever essas versões, aí vale confirmar a sintaxe exata da flag na documentação oficial do phx.gen.release antes de usar

Checagem: confere se os arquivos da lista apareceram no projeto antes de seguir, lembrando que release.ex, bin/migrate e migrate.bat só existem se o projeto usa Ecto

O erro comum deste passo: se o projeto usa npm ou yarn, o Dockerfile gerado não baixa esses pacotes sozinho

Você precisa adicionar manualmente um passo que instala as dependências JS, senão a compilação dos assets quebra no meio do build

Passo 2: entender o Dockerfile multi-stage e o que vai para a imagem de execução

O Dockerfile gerado tem dois estágios, os dois em Debian, e entender isso é o que separa esse deploy de um container genérico

Estágio builder: usa a imagem hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}

É ali que acontece a compilação, o build dos assets com MIX_ENV=prod mix assets.deploy e a montagem da release

Estágio final (chamado de final no template, também usa a imagem debian:${DEBIAN_VERSION}): pega SÓ a release pronta do builder

As linhas que fazem isso no template são estas:

COPY --from=builder --chown=nobody:root /app/_build/${MIX_ENV}/rel/<app> ./

USER nobody

CMD ["/app/bin/server"]

E por que isso é massa? A imagem final não carrega Mix nem toolchain de build, só o artefato

E o processo roda como nobody, ou seja, sem root dentro do container

O erro comum deste passo: tentar rodar mix dentro do container de produção

Não vai funcionar, o Mix ficou lá no estágio builder e não existe na release 😛

Passo 3: configurar as variáveis de ambiente lidas pelo config/runtime.exs

O config/runtime.exs roda depois da compilação e antes de o sistema iniciar, então os segredos entram como variáveis de ambiente na hora de subir o container, e não ficam gravados na imagem

As variáveis que importam aqui:

  • SECRET_KEY_BASE: assina e criptografa cookies e outros segredos. Gere com mix phx.gen.secret
  • DATABASE_URL: no formato ecto://USER:PASS@HOST/DATABASE. O runtime.exs gerado levanta erro se ela não existir em produção
  • PHX_HOST: o host usado no Endpoint (o padrão é example.com)
  • PORT: a porta do Endpoint (o padrão é 4000)
  • PHX_SERVER=true: liga o servidor web. O bin/server já define isso pra você

Pra gerar o segredo, na sua máquina de desenvolvimento:

mix phx.gen.secret

Um exemplo didático da lista de variáveis, com valores fictícios (troque tudo pelos seus):

SECRET_KEY_BASE=cole_aqui_o_valor_gerado_pelo_mix_phx_gen_secret
DATABASE_URL=ecto://app_user:[email protected]/minha_app_prod
PHX_HOST=meuapp.exemplo.com.br
PORT=4000

Guarde isso fora do repositório, beleza? Segredo commitado é dor de cabeça garantida

O erro comum deste passo: esquecer o PHX_HOST e deixar o padrão example.com

A variável existe justamente pra você apontar o Endpoint pro domínio real da aplicação, então vale confirmar que ela está definida com o host certo antes de ir pra produção

Passo 4: apontar a aplicação para um banco privado e rodar as migrações

Se o PostgreSQL roda numa rede privada, o host da DATABASE_URL aponta pro endereço interno do banco, e não pra um IP público

E se essa rede do banco for IPv6? Isso se configura no próprio config/runtime.exs: quando ECTO_IPV6 vem como true ou 1, o trecho acrescenta [:inet6] aos socket_options do Repo. O mix phx.gen.release não aplica isso automaticamente, ele só avisa no terminal pra você adicionar esse trecho quando ele ainda não existir no runtime.exs

Agora as migrações

Como não existe mix ecto.migrate na release, quem faz esse trabalho é o script bin/migrate, que chama as tarefas do módulo release.ex gerado no passo 1

É esse script que você executa no container da aplicação, com as mesmas variáveis de ambiente (principalmente a DATABASE_URL)

Pra não deixar a porta do banco aberta pra internet, use o gerenciamento de firewall da VPS e libere só o que a aplicação precisa expor

O erro comum deste passo: subir a aplicação sem DATABASE_URL

O runtime.exs levanta erro logo na inicialização, e o container não fica de pé

Passo 5: construir a imagem, subir na VPS e verificar se o Phoenix respondeu

Pra acessar o servidor, você pode usar SSH ou o terminal web da Hostinger, que já vem no painel com assistência de IA

O fluxo é este:

  • construir a imagem a partir do Dockerfile gerado no passo 1
  • rodar o container passando as variáveis do passo 3
  • executar o bin/migrate como no passo 4

O builder e a imagem final usam uma base Debian compatível. A arquitetura também deve corresponder ao destino: a documentação de builds multiplataforma do Docker explica que a conteinerização não elimina a exigência de compatibilidade com a arquitetura do host

Por isso compensa construir a imagem pensando na arquitetura da VPS, em vez de copiar uma pasta _build compilada no seu notebook

Checagem final: o container fica de pé executando /app/bin/server e a aplicação responde na porta definida em PORT

O erro comum deste passo: iniciar a release direto pelo binário padrão, sem PHX_SERVER=true

O processo sobe, mas o endpoint não escuta nada… por isso o CMD do Dockerfile usa bin/server, que já liga o servidor

Se você prefere um painel que cuida do build e dos containers por você, vale olhar como instalar o Coolify na VPS e apontar ele pro mesmo Dockerfile

Erros comuns ao publicar uma release Phoenix com Docker

O container sobe, mas o Phoenix não responde

  • Causa: o servidor web não foi ligado, falta PHX_SERVER=true
  • Solução: inicie pela bin/server (o CMD padrão do Dockerfile gerado) ou defina a variável

Erro de DATABASE_URL ausente na inicialização

  • Causa: o runtime.exs gerado exige a variável em produção
  • Solução: passe DATABASE_URL no formato ecto://USER:PASS@HOST/DATABASE

O build quebra na etapa dos assets

  • Causa: o projeto usa npm ou yarn e o Dockerfile não baixa esses pacotes
  • Solução: adicione o passo de instalação das dependências JS antes do mix assets.deploy

Comando mix não encontrado em produção

  • Causa: o Mix não faz parte da release
  • Solução: use bin/migrate e as tarefas do release.ex pras migrações

A release copiada para outro servidor não roda

  • Causa: sistema operacional ou arquitetura diferentes da máquina de build
  • Solução: construa a release dentro da imagem Docker para o sistema e a arquitetura da VPS, usando bases compatíveis no builder e no estágio final

A aplicação não conecta no banco IPv6

  • Causa: o Repo não está configurado pra IPv6
  • Solução: no config/runtime.exs, acrescente [:inet6] aos socket_options do Repo quando ECTO_IPV6 for true ou 1 (o mix phx.gen.release avisa isso no terminal quando esse trecho ainda não existe)

Como prevenir

Antes de cada deploy, passa um checklist rápido: SECRET_KEY_BASE, DATABASE_URL, PHX_HOST e PORT definidos, e ECTO_IPV6 se o banco for IPv6

E confirme que o backup semanal automático da VPS está ativo, assim você tem uma camada de segurança caso o deploy dê problema 😀

Vídeo complementar do canal Hora de Codar, sobre um assunto relacionado.

Próximo passo: manter a release atualizada com segurança na VPS

A cada nova versão da aplicação, o ciclo é sempre o mesmo: gerar a nova imagem, rodar o bin/migrate e só então trocar o container antigo pelo novo

Planeje a ordem conforme a compatibilidade da migration com as versões antiga e nova da aplicação. Como recomendação operacional deste guia, ensaie a alteração em staging e tenha backup antes de modificar o banco; rodar a migration antes da troca, sozinho, não garante que o deploy seja seguro

E se a migração der ruim? A VPS da Hostinger já inclui backups semanais automáticos, com restauração pelo painel, o que ajuda como rede de segurança, mas vale também o hábito de testar a migração num ambiente de staging antes de rodar em produção, sempre que possível

A VPS também tem um prazo de 30 dias pra pedir reembolso (sujeito às condições do programa), o que ajuda se você quer validar o ambiente com calma

Se esse fluxo de release faz sentido pro seu projeto, bora conferir a VPS para este projeto e subir a primeira versão

Até o próximo post! =)

Quer aprender mais sobre inteligência artificial e programação na prática? Conheça o canal Hora de Codar no YouTube e acompanhe os próximos vídeos.

Escrito por | Matheus Battisti

Matheus Battisti
Fundador da Hora de Codar

Programador apaixonado pelo mundo das tecnologias, sempre buscando em aprender e se aprofundar em linguagens, frameworks e o que mais for necessário para executar um bom trabalho. Agora tem uma nova missão que é de passar seu conhecimento adiante para formar novos programadores e especializar mais os que já são.

Formações

Formação Claude Code

Formação Claude Code

Domine Claude Code do absoluto zero até o avançado

  • 120 aulas
  • 4 projetos
  • 9h 48min

Blog | Mais populares