檢視來源 透過版本部署

需要用到的工具

在本指南中,我們唯一需要的是一個正常運作的 Phoenix 應用程式。對於需要一個簡單應用程式來部署的人,請依照啟動和執行指南進行操作。

目標

在本指南中的主要目標是將您的 Phoenix 應用程式封裝成一個獨立目錄,其中包括 Erlang VM、Elixir,以及您的所有程式碼和依賴項。之後可以將此封裝放置到生產機器中。

組裝版本

如果您還不熟悉 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 任務生成了幾個檔案來協助我們進行版本發布。首先,它產生了 servermigrate overlay 腳本,用於在版本內方便地執行 phoenix 伺服器或執行版本中的遷移。 rel/overlays 目錄中的檔案會複製到每個版本環境中。接著,它生成了 release.ex 檔案,用於在不依賴 mix 本身的情況下呼叫 Ecto 遷移。

注意:如果您是 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 的文件

但在我們完成本指南之前,還有版本中的另一個功能供大多數 Phoenix 應用程式使用,因此讓我們來討論一下。

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

其中您可以用應用程式名稱取代前兩行。

現在,您可以使用 MIX_ENV=prod mix 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 端點。這能透過改變您的監督樹來規避,以便在特定情況下不啟動特定子行程。例如,您可以在發行版本指令碼檔案中進行以下操作

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.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 或本地端埠,則取決於您的需求。
  • 您能在執行階段提供的設定越多(使用config/runtime.exs),您的映像將在各種環境中越能重複使用。特別是,例如資料庫認證和 API 金鑰等機密資訊,不應編譯至映像中,而應在以此映像為基礎建立容器時提供。這是為何Endpoint:secret_key_base預設設定於config/runtime.exs 之中。
  • 如果可行,在執行階段所需的任何環境變數都應在config/runtime.exs中讀取,而不是分佈至您的程式碼各處。將所有變數明確顯示在同個地方,將能更輕鬆地確認容器取得所需項目,尤其當執行基礎架構作業的人員,不會處理 Elixir 程式碼時。特別是,函式庫不應直接讀取環境變數;所有設定都應該由頂層應用程式交付,最好不使用應用程式環境