查看源代码 使用 Releases 部署

我们需要什么

本指南中我们只需要一个可运行的 Phoenix 应用程序。对于需要一个简单应用程序进行部署的人,请参考 快速上手指南

目标

本指南的主要目标是将您的 Phoenix 应用程序打包成一个自包含的目录,其中包括 Erlang VM、Elixir、所有代码和依赖项。此包可以随后被部署到生产机器上。

Releases,组装!

如果您还不熟悉 Elixir Releases,我们建议您在继续之前阅读 Elixir 的优秀文档

完成上述步骤后,您可以通过执行我们通用 部署指南 中的所有步骤并使用 mix release 来组装 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 任务为我们生成了一些文件,以帮助进行 Release 操作。首先,它创建了 servermigrate *overlay* 脚本,以便在 Release 中方便地运行 Phoenix 服务器或从 Release 中调用迁移。 rel/overlays 目录中的文件将被复制到每个 Release 环境中。接下来,它生成一个 release.ex 文件,该文件用于调用 Ecto 迁移,而无需依赖于 mix 本身。

注意:如果您是 Docker 用户,您可以将 --docker 标志传递给 mix phx.gen.release 以生成一个可用于部署的 Dockerfile。

接下来,我们可以调用 mix release 来构建 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 启动 Release,或者通过调用 _build/prod/rel/my_app/bin/server 启动 Web 服务器,其中您需要将 my_app 替换为您的当前应用程序名称。

现在,您可以获取 _build/prod/rel/my_app 目录下的所有文件,将其打包,并在与组装 Release 的机器具有相同操作系统和架构的任何生产机器上运行它。有关更多详细信息,请查看 针对 mix release 的文档

但在结束本指南之前,Release 还有一项功能,大多数 Phoenix 应用程序都会使用,所以让我们讨论一下。

Ecto 迁移和自定义命令

生产系统中的一个常见需求是执行设置生产环境所需的自定义命令。其中一个命令就是数据库迁移。由于我们没有 Mix(一个 *build* 工具)在 Release 中(Release 是一个生产工件),我们需要将这些命令直接引入 Release 中。

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.load(@app)
  end
end

将前两行替换为您的应用程序名称。

现在,您可以使用 MIX_ENV=prod mix release 组装一个新的 Release,并通过调用 eval 命令来调用任何代码,包括上述模块中的函数

$ _build/prod/rel/my_app/bin/my_app eval "MyApp.Release.migrate"

就是这样!如果您查看 migrate 脚本,您会发现它正是将此调用封装起来。

您可以使用此方法创建任何要在生产环境中运行的自定义命令。在本例中,我们使用了 load_app,它调用了 Application.load/1 来加载当前应用程序,而不会启动它。但是,您可能需要编写一个启动整个应用程序的自定义命令。在这种情况下,必须使用 Application.ensure_all_started/1。请记住,启动应用程序将启动当前应用程序的所有进程,包括 Phoenix 端点。可以通过更改您的监督树以在特定条件下不启动某些子进程来避免这种情况。例如,在 Release 命令文件中,您可以执行以下操作

defp start_app do
  load_app()
  Application.put_env(@app, :minimal, true)
  Application.ensure_all_started(@app)
end

然后在您的应用程序中,检查 Application.get_env(@app, :minimal),并在设置了该值时仅启动部分子进程。

容器

Elixir Releases 与容器技术(如 Docker)配合使用效果很好。思路是在 Docker 容器中组装 Release,然后基于 Release 工件构建镜像。

如果您调用 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.14.5-erlang-25.3.2.4-debian-bullseye-20230612-slim
#
ARG ELIXIR_VERSION=1.14.5
ARG OTP_VERSION=25.3.2.4
ARG DEBIAN_VERSION=bullseye-20230612-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 -y && apt-get install -y build-essential git \
    && apt-get clean && rm -f /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

COPY priv priv

COPY lib lib

COPY assets assets

# compile assets
RUN mix assets.deploy

# Compile the release
RUN mix compile

# 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}

RUN apt-get update -y && \
  apt-get install -y libstdc++6 openssl libncurses5 locales ca-certificates \
  && apt-get clean && rm -f /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 运行。

关于配置容器化应用程序的几点说明

  • 如果您在容器中运行应用程序,则需要将 Endpoint 配置为监听 "公共" :ip 地址(如 0.0.0.0),以便可以从容器外部访问该应用程序。主机应该将容器的端口发布到其自己的公共 IP 还是 localhost,取决于您的需求。
  • 您可以在运行时提供的配置(使用 config/runtime.exs)越多,您的镜像在不同环境中的可重用性就越高。特别是,诸如数据库凭据和 API 密钥之类的机密信息不应编译到镜像中,而应在基于该镜像创建容器时提供。这就是为什么 Endpoint:secret_key_base 默认在 config/runtime.exs 中配置的原因。
  • 如果可能,所有在运行时需要的环境变量都应该在 config/runtime.exs 中读取,而不是散布在整个代码中。将它们全部在一个地方显示出来,将有助于确保容器获得它们所需的内容,尤其是当进行基础设施工作的人员不参与 Elixir 代码开发时。库尤其不应直接读取环境变量;它们的所有配置都应该由顶层应用程序提供,最好是 不使用应用程序环境