Развертывание с помощью релизов
Исходный кодОсновная цель этого руководства — упаковать ваше приложение Phoenix в самодостаточный каталог, содержащий виртуальную машину Erlang, Elixir, весь ваш код и зависимости. Затем этот пакет можно перенести на рабочую машину.
Что нам понадобится
Для этого руководства нам понадобится только работающее приложение Phoenix. Если вам нужно простое приложение для развертывания, следуйте руководству «Начало работы».
Собираем релизы!
Если вы еще не знакомы с релизами Elixir, перед продолжением рекомендуем прочитать отличную документацию Elixir.
Когда будете готовы, соберите релиз, выполнив все шаги из нашего общего руководства по развертыванию и добавив в конце mix release. Давайте повторим эти шаги.
Сначала задайте переменные окружения:
$ mix phx.gen.secret REALLY_LONG_SECRET $ export SECRET_KEY_BASE=REALLY_LONG_SECRET $ export DATABASE_URL=ecto://USER:PASS@HOST/database
Затем загрузите зависимости, чтобы скомпилировать код и ресурсы:
# Initial setup $ mix deps.get --only prod $ MIX_ENV=prod mix compile # Compile assets $ MIX_ENV=prod mix assets.deploy
Теперь выполните mix phx.gen.release:
$ mix phx.gen.release
==> my_app
* creating rel/overlays/bin/server
* creating rel/overlays/bin/server.bat
* creating rel/overlays/bin/migrate
* creating rel/overlays/bin/migrate.bat
* creating lib/my_app/release.ex
Your application is ready to be deployed in a release!
# To start your system
_build/dev/rel/my_app/bin/my_app start
# To start your system with the Phoenix server running
_build/dev/rel/my_app/bin/server
# To run migrations
_build/dev/rel/my_app/bin/migrate
Once the release is running:
# To connect to it remotely
_build/dev/rel/my_app/bin/my_app remote
# To stop it gracefully (you may also send SIGINT/SIGTERM)
_build/dev/rel/my_app/bin/my_app stop
To list all commands:
_build/dev/rel/my_app/bin/my_app
Задача phx.gen.release создала несколько файлов, которые помогут нам при работе с релизами. Во-первых, она создала скрипты overlay server и migrate, упрощающие запуск сервера Phoenix внутри релиза или выполнение миграций из релиза. Файлы из каталога rel/overlays копируются в каждое окружение релиза. Затем она создала файл release.ex, который позволяет запускать миграции Ecto без зависимости от самого mix.
Примечание: Если вы используете Docker, передайте флаг --docker команде mix phx.gen.release, чтобы создать Dockerfile, готовый к развертыванию.
Затем можно выполнить mix release, чтобы собрать релиз:
$ MIX_ENV=prod mix release
Generated my_app app
* assembling my_app-0.1.0 on MIX_ENV=prod
* using config/runtime.exs to configure the release at runtime
Release created at _build/prod/rel/my_app!
# To start your system
_build/prod/rel/my_app/bin/my_app start
...
Запустить релиз можно командой _build/prod/rel/my_app/bin/my_app start, а запустить веб-сервер — командой _build/prod/rel/my_app/bin/server. Вместо my_app укажите текущее имя приложения.
Теперь можно взять все файлы из каталога _build/prod/rel/my_app, упаковать их и запустить на любой рабочей машине с той же ОС и архитектурой, что и машина, на которой был собран релиз. Подробнее см. в документации по mix release.
Миграции Ecto
В рабочих системах часто требуется выполнять специальные команды для настройки рабочего окружения. Одна из таких команд — миграция базы данных. Поскольку в релизах, являющихся артефактами для рабочего окружения, нет Mix — инструмента сборки, — эти команды необходимо включить непосредственно в релиз.
Команда phx.gen.release создала в каталоге lib/my_app/release.ex вашего проекта следующий файл release.ex со следующим содержимым:
defmodule MyApp.Release do
@app :my_app
def migrate do
load_app()
for repo <- repos() do
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
end
end
def rollback(repo, version) do
load_app()
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
end
defp repos do
Application.fetch_env!(@app, :ecto_repos)
end
defp load_app do
Application.ensure_all_started(:ssl)
Application.ensure_loaded(@app)
end
end
Замените первые две строки именами своих приложений.
Теперь можно собрать новый релиз с помощью MIX_ENV=prod mix release и запустить любой код, включая функции из приведенного выше модуля, командой eval:
$ _build/prod/rel/my_app/bin/my_app eval "MyApp.Release.migrate"
Вот и всё! Если заглянуть в скрипт migrate, вы увидите, что в нем выполняется именно эта команда. В зависимости от того, где вы развертываете приложение, можно отдельно вызвать команду migrate или изменить скрипт server, чтобы миграция базы данных выполнялась перед запуском приложения.
Пользовательские команды
Для создания любых пользовательских команд, выполняемых в рабочем окружении, можно использовать тот же подход, что и для миграций. Каждая команда должна вызывать load_app, которая вызывает Application.ensure_loaded/1, чтобы загрузить текущее приложение, не запуская его.
Однако для некоторых команд может потребоваться запустить приложение целиком. В таких случаях вместо Application.load/1 следует использовать Application.ensure_all_started/1. Имейте в виду, что запуск приложения запустит все процессы в его дереве супервизии, включая конечную точку Phoenix. Этого можно избежать, изменив дерево супервизии так, чтобы при определенных условиях некоторые дочерние процессы не запускались. Например, в файле команд релиза можно написать:
defp start_app do load_app() Application.put_env(@app, :minimal, true) Application.ensure_all_started(@app) end
Затем в приложении можно проверить Application.get_env(@app, :minimal) и запускать только часть дочерних процессов, если эта переменная задана.
Контейнеры
Релизы Elixir хорошо работают с контейнерными технологиями, такими как Docker. Идея состоит в том, чтобы собрать релиз внутри контейнера Docker, а затем создать образ на основе артефактов релиза.
Если выполнить mix phx.gen.release --docker, вы увидите новый файл с содержимым примерно такого вида:
# Find eligible builder and runner images on Docker Hub. We use Ubuntu/Debian
# instead of Alpine to avoid DNS resolution issues in production.
#
# https://hub.docker.com/r/hexpm/elixir/tags?page=1&name=ubuntu
# https://hub.docker.com/_/ubuntu?tab=tags
#
# This file is based on these images:
#
# - https://hub.docker.com/r/hexpm/elixir/tags - for the build image
# - https://hub.docker.com/_/debian?tab=tags&page=1&name=bullseye-20230612-slim - for the release image
# - https://pkgs.org/ - resource for finding needed packages
# - Ex: hexpm/elixir:1.18.4-erlang-27.3.4.3-debian-trixie-20250908-slim
#
ARG ELIXIR_VERSION=1.18.4
ARG OTP_VERSION=27.3.4.3
ARG DEBIAN_VERSION=trixie-20250908-slim
ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}"
ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}"
FROM ${BUILDER_IMAGE} AS builder
# install build dependencies
RUN apt-get update \
&& apt-get install -y --no-install-recommends build-essential git \
&& rm -rf /var/lib/apt/lists/*
# prepare build dir
WORKDIR /app
# install hex + rebar
RUN mix local.hex --force \
&& mix local.rebar --force
# set build ENV
ENV MIX_ENV="prod"
# install mix dependencies
COPY mix.exs mix.lock ./
RUN mix deps.get --only $MIX_ENV
RUN mkdir config
# copy compile-time config files before we compile dependencies
# to ensure any relevant config change will trigger the dependencies
# to be re-compiled.
COPY config/config.exs config/${MIX_ENV}.exs config/
RUN mix deps.compile
RUN mix assets.setup
COPY priv priv
COPY lib lib
# Compile the release
RUN mix compile
COPY assets assets
# compile assets
RUN mix assets.deploy
# Changes to config/runtime.exs don't require recompiling the code
COPY config/runtime.exs config/
COPY rel rel
RUN mix release
# start a new build stage so that the final image will only contain
# the compiled release and other runtime necessities
FROM ${RUNNER_IMAGE} AS final
RUN apt-get update \
&& apt-get install -y --no-install-recommends libstdc++6 openssl libncurses6 locales ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Set the locale
RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen \
&& locale-gen
ENV LANG=en_US.UTF-8
ENV LANGUAGE=en_US:en
ENV LC_ALL=en_US.UTF-8
WORKDIR "/app"
RUN chown nobody /app
# set runner ENV
ENV MIX_ENV="prod"
# Only copy the final release from the build stage
COPY --from=builder --chown=nobody:root /app/_build/${MIX_ENV}/rel/my_app ./
USER nobody
# If using an environment that doesn't automatically reap zombie processes, it is
# advised to add an init process such as tini via `apt-get install`
# above and adding an entrypoint. See https://github.com/krallin/tini for details
# ENTRYPOINT ["/tini", "--"]
CMD ["/app/bin/server"]
Здесь my_app — имя вашего приложения. В результате в /app будет готовое приложение, которое можно запускать как /app/bin/server.
Несколько замечаний о настройке контейнеризированного приложения:
Чем больше параметров конфигурации можно задать во время выполнения (с помощью
config/runtime.exs), тем проще повторно использовать образы в разных окружениях. В частности, секреты, такие как учетные данные базы данных и API-ключи, не следует включать в образ при компиляции. Их нужно передавать при создании контейнеров на основе этого образа. Именно поэтомуEndpointпо умолчанию настраивает:secret_key_baseвconfig/runtime.exs.По возможности все переменные окружения, необходимые во время выполнения, следует считывать в
config/runtime.exs, а не распределять по коду. Если все они собраны в одном месте, будет проще убедиться, что контейнеры получают необходимые параметры, особенно если специалист, отвечающий за инфраструктуру, не работает с кодом Elixir. Библиотеки, в частности, никогда не должны напрямую считывать переменные окружения; все параметры конфигурации им должно передавать приложение верхнего уровня, желательно не используя окружение приложения.
Кластеризация
Elixir и виртуальная машина Erlang обладают невероятной возможностью объединяться в кластер и беспрепятственно обмениваться сообщениями между узлами. Для включения кластеризации нужны две отдельные возможности:
Соединение узлов: разные экземпляры одной службы должны обмениваться данными друг с другом. Это возможность виртуальной машины Erlang.
Обнаружение служб: для заданной службы должна быть возможность найти IP-адреса всех ее экземпляров. В Phoenix входит
dns_cluster, обеспечивающий обнаружение служб на основе DNS «из коробки», но можно использовать и другие методы.
Обнаружение через DNS
Конфигурацию кластеризации обычно добавляют в rel/env.sh.eex. Этот файл выполняется до запуска релиза, поэтому он отлично подходит для настройки приложения во время выполнения с учетом окружения развертывания. Вот общий шаблон:
# Uncomment if IPv6 is required
# export ECTO_IPV6="true"
# export ERL_AFLAGS="-proto_dist inet6_tcp"
# Erlang uses a port mapper daemon on each node,
# it by default runs on port 4369
export ERL_EPMD_PORT=4369
# Use the ports 4370-4372 for nodes to communicate.
export ERL_AFLAGS="-kernel inet_dist_listen_min 4370 inet_dist_listen_max 4372"
export RELEASE_DISTRIBUTION="name"
export RELEASE_NODE="app-${PLATFORM_DEPLOYMENT_SHA}@${PLATFORM_DEPLOYMENT_IP}"
export DNS_CLUSTER_QUERY="your-app.internal"
Приведенный выше скрипт выполняет несколько действий:
Настраивает приложение на использование портов 4369, 4370, 4371 и 4372 для обмена данными. Возможно, потребуется явно открыть эти порты для внутреннего TCP-трафика на платформе развертывания (в дополнение к выбранному HTTP-порту).
Затем настраивает приложение на использование полных имен. В имя каждого приложения входит текущий SHA развертывания в виде
PLATFORM_DEPLOYMENT_SHA(точное имя переменной окружения зависит от платформы), поэтому при каждом развертывании создается отдельный кластер, а текущий IP указывается в видеPLATFORM_DEPLOYMENT_IP(также зависит от платформы). Если IP недоступен, его можно вычислить какNODE_IP=hostname | tr -d ' '.Наконец, задает DNS-запрос, с помощью которого будут находиться IP-адреса других экземпляров.
Некоторые платформы, например Fly.io, Railway и Render, изначально предоставляют частные сети с поддержкой DNS-запросов. Вам нужно только соответствующим образом настроить переменную DNS_CLUSTER_QUERY.
Другие платформы, например Digital Ocean App Platform и Northflank, позволяют узлам напрямую подключаться друг к другу, но не предоставляют обнаружение служб через DNS. В следующем разделе мы рассмотрим различные механизмы обнаружения служб.
Альтернативные механизмы обнаружения
Не все платформы поддерживают DNS-запросы для обнаружения служб, но существует множество альтернативных способов соединить узлы. Ознакомьтесь со следующими библиотеками:
libcluster — предоставляет стратегии для соединения узлов с помощью протоколов gossip, Kubernetes, EC2 и других средств.
libcluster_postgres — плагин для
libcluster, который использует PostgreSQL для обнаружения узлов. Поскольку большинство приложений уже используют базу данных, скорее всего PostgreSQL, это подходящий вариант, не требующий дополнительной настройки.
При использовании перечисленных выше библиотек, скорее всего, можно удалить dns_query из зависимостей приложения.
Развертывание без epmd
В приведенном выше фрагменте мы использовали порты 4369, 4370, 4371 и 4372. Однако виртуальная машина Erlang позволяет запускать распределенную систему через фиксированный порт — такой подход также называют развертыванием без epmd. Чтобы включить его, выполните следующие действия.
Удалите строки:
export ERL_EPMD_PORT=4369 export ERL_AFLAGS="-kernel inet_dist_listen_min 4370 inet_dist_listen_max 4372"
Создайте файл rel/vm.args.eex со следующим содержимым:
-start_epmd false -erl_epmd_port 6789
Создайте файл rel/remote.vm.args.eex со следующим содержимым:
-start_epmd false -erl_epmd_port 6789 -dist_listen false
Теперь между экземплярами нужно открыть только порт 6789 (помимо HTTP-порта) для внутреннего трафика.
© 2014 Chris McCord
Licensed under the MIT License.
https://phoenix.hexdocs.pm/releases.html