查看源代码 在 Heroku 上部署

我们需要什么

本指南中唯一需要的是一个正常运行的 Phoenix 应用程序。对于需要部署简单应用程序的用户,请按照 入门指南 操作。

目标

本指南的主要目标是在 Heroku 上运行 Phoenix 应用程序。

限制

Heroku 是一个很棒的平台,Elixir 在其上表现良好。但是,如果您计划利用 Elixir 和 Phoenix 提供的先进功能,例如以下内容,则可能会遇到限制:

如果您刚开始使用,或者您不希望使用上述功能,那么 Heroku 应该足以满足您的需求。例如,如果您正在将运行在 Heroku 上的现有应用程序迁移到 Phoenix,并保持类似的功能集,那么 Elixir 的性能会与当前堆栈一样好,甚至更好。

如果您想要一个没有这些限制的平台即服务,请尝试使用 Gigalixir。如果您希望部署到云平台(例如 EC2、Google Cloud 等),请考虑使用 mix release

步骤

让我们将此过程分成几个步骤,以便我们能够跟踪当前位置。

  • 初始化 Git 仓库
  • 注册 Heroku 帐户
  • 安装 Heroku 工具带
  • 创建和设置 Heroku 应用程序
  • 使项目准备好用于 Heroku
  • 部署时间!
  • 有用的 Heroku 命令

初始化 Git 仓库

Git 是一种流行的分布式版本控制系统,也用于将应用程序部署到 Heroku。

在我们能够推送到 Heroku 之前,我们需要初始化本地 Git 仓库并将我们的文件提交到该仓库。我们可以在项目目录中运行以下命令来实现:

$ git init
$ git add .
$ git commit -m "Initial commit"

Heroku 提供了一些关于如何使用 Git 的信息,请 点击此处查看

注册 Heroku 帐户

注册 Heroku 帐户非常简单,只需前往 https://signup.heroku.com/ 并填写表格即可。

免费计划将为我们提供一个 Web Dyno 和一个工作程序 Dyno,以及一个免费的 PostgreSQL 和 Redis 实例。

这些旨在用于测试和开发,并且有一些限制。为了运行生产应用程序,请考虑升级到付费计划。

安装 Heroku 工具带

注册后,我们可以从 此处 下载适合我们系统的 Heroku 工具带的正确版本。

工具带的一部分 Heroku CLI 对创建 Heroku 应用程序、列出当前运行的应用程序 Dyno、尾随日志或运行一次性命令(例如 mix 任务)很有用。

创建和设置 Heroku 应用程序

在 Heroku 上部署 Phoenix 应用程序有两种不同的方法。我们可以使用 Heroku 构建包或其容器堆栈。这两种方法之间的区别在于我们告诉 Heroku 如何处理我们的构建。在构建包情况下,我们需要更新我们在 Heroku 上的应用程序配置以使用 Phoenix/Elixir 特定的构建包。在容器方法中,我们可以更全面地控制如何设置应用程序,并且可以使用 Dockerfileheroku.yml 定义容器映像。本节将探讨构建包方法。为了使用 Dockerfile,通常建议将应用程序转换为使用版本,我们将在后面介绍。

创建应用程序

构建包 是一种方便的打包框架和/或运行时支持的方式。Phoenix 需要 2 个构建包才能在 Heroku 上运行,第一个添加了基本的 Elixir 支持,第二个添加了 Phoenix 特定的命令。

安装了工具带后,让我们创建 Heroku 应用程序。我们将使用最新版本的 Elixir 构建包 来实现。

$ heroku create --buildpack hashnuke/elixir
Creating app... done, ⬢ mysterious-meadow-6277
Setting buildpack to hashnuke/elixir... done
https://mysterious-meadow-6277.herokuapp.com/ | https://git.heroku.com/mysterious-meadow-6277.git

注意:我们第一次使用 Heroku 命令时,它可能会提示我们登录。如果发生这种情况,只需输入我们在注册时指定的电子邮件和密码即可。

注意:Heroku 应用程序的名称是上面输出中“Creating”之后的随机字符串(mysterious-meadow-6277)。这将是唯一的,因此预计会看到与“mysterious-meadow-6277”不同的名称。

注意:输出中的 URL 是我们应用程序的 URL。如果我们在浏览器中打开它,现在将看到默认的 Heroku 欢迎页面。

注意:如果我们在运行 heroku create 命令之前没有初始化 Git 仓库,那么此时我们的 Heroku 远程仓库将无法正确设置。我们可以通过运行以下命令手动设置它:heroku git:remote -a [our-app-name]。

构建包使用预定义的 Elixir 和 Erlang 版本,但为了避免在部署时出现意外,最好明确列出我们在生产环境中想要使用的 Elixir 和 Erlang 版本,使其与我们在开发环境中或持续集成服务器中使用的版本相同。这可以通过在项目的根目录中创建一个名为 elixir_buildpack.config 的配置文件来实现,该文件包含目标 Elixir 和 Erlang 版本。

# Elixir version
elixir_version=1.14.0

# Erlang version
# https://github.com/HashNuke/heroku-buildpack-elixir-otp-builds/blob/master/otp-versions
erlang_version=24.3

# Invoke assets.deploy defined in your mix.exs to deploy assets with esbuild
# Note we nuke the esbuild executable from the image
hook_post_compile="eval mix assets.deploy && rm -f _build/esbuild*"

最后,让我们告诉构建包如何启动 Web 服务器。在项目的根目录中创建一个名为 Procfile 的文件。

web: mix phx.server

可选:Node、npm 和 Phoenix Static 构建包

默认情况下,Phoenix 使用 esbuild 并为您管理所有资产。但是,如果您使用的是 nodenpm,则需要安装 Phoenix Static 构建包 来处理它们。

$ heroku buildpacks:add https://github.com/gjaldon/heroku-buildpack-phoenix-static.git
Buildpack added. Next release on mysterious-meadow-6277 will use:
  1. https://github.com/HashNuke/heroku-buildpack-elixir.git
  2. https://github.com/gjaldon/heroku-buildpack-phoenix-static.git

使用此构建包时,您需要将所有资产捆绑委托给 npm。因此,您必须从 elixir_buildpack.config 中删除 hook_post_compile 配置,并将其移动到 assets/package.json 的部署脚本中。类似于以下内容:

{
  ...
  "scripts": {
    "deploy": "cd .. && mix assets.deploy && rm -f _build/esbuild*"
  }
  ...
}

Phoenix Static 构建包使用预定义的 Node.js 版本,但为了避免在部署时出现意外,最好明确列出我们在生产环境中想要使用的 Node.js 版本,使其与我们在开发环境中或持续集成服务器中使用的版本相同。这可以通过在项目的根目录中创建一个名为 phoenix_static_buildpack.config 的配置文件来实现,该文件包含目标 Node.js 版本。

# Node.js version
node_version=10.20.1

请参阅 配置部分 以了解完整详细信息。您可以创建自己的自定义构建脚本,但目前我们将使用 提供的默认脚本

最后,请注意,由于我们使用的是多个构建包,因此您可能会遇到顺序错误的问题(Elixir 构建包需要在 Phoenix Static 构建包之前运行)。Heroku 的文档 对此进行了更好的解释,但您需要确保 Phoenix Static 构建包最后运行。

使项目准备好用于 Heroku

每个新的 Phoenix 项目都附带一个配置文件 config/runtime.exs(以前为 config/prod.secret.exs),它从 环境变量 加载配置和机密。这与 Heroku 最佳实践 (12 要素应用程序) 非常契合,因此我们唯一要做的工作就是配置 URL 和 SSL。

首先,让我们告诉 Phoenix 只使用网站的 SSL 版本。在 config/prod.exs 中找到端点配置。

config :scaffold, ScaffoldWeb.Endpoint,
  url: [port: 443, scheme: "https"],

... 并添加 force_ssl

config :scaffold, ScaffoldWeb.Endpoint,
  url: [port: 443, scheme: "https"],
  force_ssl: [rewrite_on: [:x_forwarded_proto]],

force_ssl 需要在这里设置,因为它是一个编译时配置。在 runtime.exs 中设置它将无效。

然后在您的 config/runtime.exs(以前为 config/prod.secret.exs)中。

... 添加 host

config :scaffold, ScaffoldWeb.Endpoint,
  url: [host: host, port: 443, scheme: "https"]

并在存储库配置中取消注释 # ssl: true, 行。它将如下所示:

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

最后,如果您计划使用 WebSockets,那么我们需要在 lib/hello_web/endpoint.ex 中降低 WebSocket 传输的超时时间。如果您不打算使用 WebSockets,那么将其设置为 false 就可以了。您可以在 文档 中找到有关可用选项的更多说明。

defmodule HelloWeb.Endpoint do
  use Phoenix.Endpoint, otp_app: :hello

  socket "/socket", HelloWeb.UserSocket,
    websocket: [timeout: 45_000]

  ...
end

还在 Heroku 中设置主机。

$ heroku config:set PHX_HOST="mysterious-meadow-6277.herokuapp.com"

这确保了在达到 Heroku 的 55 秒超时窗口之前,Phoenix 会关闭所有闲置连接。

在 Heroku 中创建环境变量

当我们添加 Heroku Postgres 附加组件 时,Heroku 会自动创建 DATABASE_URL 配置变量。我们可以通过 Heroku 工具带创建数据库。

$ heroku addons:create heroku-postgresql:mini

现在我们设置 POOL_SIZE 配置变量。

$ heroku config:set POOL_SIZE=18

此值应略小于可用连接数,留出几个连接用于迁移和混合任务。小型数据库允许 20 个连接,因此我们将此数字设置为 18。如果其他 dyno 将共享数据库,请减少 POOL_SIZE 以便每个 dyno 拥有相同的份额。

稍后运行混合任务(在将项目推送到 Heroku 之后)时,您还需要像这样限制其池大小。

$ heroku run "POOL_SIZE=2 mix hello.task"

这样 Ecto 不会尝试打开超过可用连接数的连接。

我们仍然需要根据随机字符串创建 SECRET_KEY_BASE 配置。首先,使用 mix phx.gen.secret 获取新密钥。

$ mix phx.gen.secret
xvafzY4y01jYuzLm3ecJqo008dVnU3CN4f+MamNd1Zue4pXvfvUjbiXT8akaIF53

您的随机字符串将不同;请勿使用此示例值。

现在在 Heroku 中设置它。

$ heroku config:set SECRET_KEY_BASE="xvafzY4y01jYuzLm3ecJqo008dVnU3CN4f+MamNd1Zue4pXvfvUjbiXT8akaIF53"
Setting config vars and restarting mysterious-meadow-6277... done, v3
SECRET_KEY_BASE: xvafzY4y01jYuzLm3ecJqo008dVnU3CN4f+MamNd1Zue4pXvfvUjbiXT8akaIF53

部署时间!

我们的项目现在已准备好部署到 Heroku 上。

让我们提交所有更改。

$ git add elixir_buildpack.config
$ git commit -a -m "Use production config from Heroku ENV variables and decrease socket timeout"

然后部署。

$ git push heroku main
Counting objects: 55, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (49/49), done.
Writing objects: 100% (55/55), 48.48 KiB | 0 bytes/s, done.
Total 55 (delta 1), reused 0 (delta 0)
remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----> Multipack app detected
remote: -----> Fetching custom git buildpack... done
remote: -----> elixir app detected
remote: -----> Checking Erlang and Elixir versions
remote:        WARNING: elixir_buildpack.config wasn't found in the app
remote:        Using default config from Elixir buildpack
remote:        Will use the following versions:
remote:        * Stack cedar-14
remote:        * Erlang 17.5
remote:        * Elixir 1.0.4
remote:        Will export the following config vars:
remote:        * Config vars DATABASE_URL
remote:        * MIX_ENV=prod
remote: -----> Stack changed, will rebuild
remote: -----> Fetching Erlang 17.5
remote: -----> Installing Erlang 17.5 (changed)
remote:
remote: -----> Fetching Elixir v1.0.4
remote: -----> Installing Elixir v1.0.4 (changed)
remote: -----> Installing Hex
remote: 2015-07-07 00:04:00 URL:https://s3.amazonaws.com/s3.hex.pm/installs/1.0.0/hex.ez [262010/262010] ->
"/app/.mix/archives/hex.ez" [1]
remote: * creating /app/.mix/archives/hex.ez
remote: -----> Installing rebar
remote: * creating /app/.mix/rebar
remote: -----> Fetching app dependencies with mix
remote: Running dependency resolution
remote: Dependency resolution completed successfully
remote: [...]
remote: -----> Compiling
remote: [...]
remote: Generated phoenix_heroku app
remote: [...]
remote: Consolidated protocols written to _build/prod/consolidated
remote: -----> Creating .profile.d with env vars
remote: -----> Fetching custom git buildpack... done
remote: -----> Phoenix app detected
remote:
remote: -----> Loading configuration and environment
remote:        Loading config...
remote:        [...]
remote:        Will export the following config vars:
remote:        * Config vars DATABASE_URL
remote:        * MIX_ENV=prod
remote:
remote: -----> Compressing... done, 82.1MB
remote: -----> Launching... done, v5
remote:        https://mysterious-meadow-6277.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.
To https://git.heroku.com/mysterious-meadow-6277.git
 * [new branch]      master -> master

在终端中输入 heroku open 应会在浏览器中打开 Phoenix 欢迎页面。如果您正在使用 Ecto 访问数据库,您还需要在首次部署后运行迁移。

$ heroku run "POOL_SIZE=2 mix ecto.migrate"

就是这样!

使用容器堆栈部署到 Heroku

创建 Heroku 应用程序

将您的应用程序的堆栈设置为 container,这使我们能够使用 Dockerfile 来定义我们的应用程序设置。

$ heroku create
Creating app... done, ⬢ mysterious-meadow-6277
$ heroku stack:set container

在根文件夹中添加一个新的 heroku.yml 文件。在此文件中,您可以定义应用程序使用的附加组件、如何构建镜像以及哪些配置传递给镜像。您可以在 此处 了解有关 Heroku 的 heroku.yml 选项的更多信息。这是一个示例。

setup:
  addons:
    - plan: heroku-postgresql
      as: DATABASE
build:
  docker:
    web: Dockerfile
  config:
    MIX_ENV: prod
    SECRET_KEY_BASE: $SECRET_KEY_BASE
    DATABASE_URL: $DATABASE_URL

设置发布和 Dockerfile

现在我们需要在项目根文件夹中定义一个 Dockerfile,其中包含您的应用程序。我们建议在这样做时使用发布,因为发布将使我们能够仅使用我们实际使用的 Erlang 和 Elixir 部分来构建容器。请遵循 发布文档。在本指南的末尾,有一个您可以使用的示例 Dockerfile 文件。

设置好镜像定义后,您可以将应用程序推送到 heroku,您会看到它开始构建镜像并部署它。

有用的 Heroku 命令

我们可以通过在项目目录中运行以下命令来查看应用程序的日志。

$ heroku logs # use --tail if you want to tail them

我们还可以启动一个连接到我们的终端的 IEx 会话,以便在应用程序的环境中进行实验。

$ heroku run "POOL_SIZE=2 iex -S mix"

事实上,我们可以使用 heroku run 命令运行任何内容,例如上面的 Ecto 迁移任务。

$ heroku run "POOL_SIZE=2 mix ecto.migrate"

连接到您的 dyno

Heroku 使您能够使用 IEx shell 连接到您的 dyno,它允许运行 Elixir 代码,例如数据库查询。

  • 修改 Procfile 中的 web 进程以运行命名节点。

    web: elixir --sname server -S mix phx.server
  • 重新部署到 Heroku。

  • 使用 heroku ps:exec 连接到 dyno(如果您在同一个存储库中有多个应用程序,您需要使用 --app APP_NAME--remote REMOTE_NAME 指定应用程序名称或远程名称)。

  • 使用 iex --sname console --remsh server 启动 iex 会话。

您拥有一个进入 dyno 的 iex 会话!

故障排除

编译错误

有时,应用程序会在本地编译,但在 Heroku 上不会。Heroku 上的编译错误将类似于以下内容。

remote: == Compilation error on file lib/postgrex/connection.ex ==
remote: could not compile dependency :postgrex, "mix compile" failed. You can recompile this dependency with "mix deps.compile postgrex", update it with "mix deps.update postgrex" or clean it with "mix deps.clean postgrex"
remote: ** (CompileError) lib/postgrex/connection.ex:207: Postgrex.Connection.__struct__/0 is undefined, cannot expand struct Postgrex.Connection
remote:     (elixir) src/elixir_map.erl:58: :elixir_map.translate_struct/4
remote:     (stdlib) lists.erl:1353: :lists.mapfoldl/3
remote:     (stdlib) lists.erl:1354: :lists.mapfoldl/3
remote:
remote:
remote:  !     Push rejected, failed to compile elixir app
remote:
remote: Verifying deploy...
remote:
remote: !   Push rejected to mysterious-meadow-6277.
remote:
To https://git.heroku.com/mysterious-meadow-6277.git

这与过时的依赖项有关,这些依赖项没有正确地重新编译。可以强制 Heroku 在每次部署时重新编译所有依赖项,这应该可以解决此问题。方法是在应用程序的根目录中添加一个名为 elixir_buildpack.config 的新文件。该文件应包含此行。

always_rebuild=true

将此文件提交到存储库,然后尝试再次推送到 Heroku。

连接超时错误

如果您在运行 heroku run 时不断收到连接超时,这可能意味着您的互联网提供商已阻止了端口号 5000。

heroku run "POOL_SIZE=2 mix myapp.task"
Running POOL_SIZE=2 mix myapp.task on mysterious-meadow-6277... !
ETIMEDOUT: connect ETIMEDOUT 50.19.103.36:5000

您可以通过在运行命令中添加 detached 选项来克服此问题。

heroku run:detached "POOL_SIZE=2 mix ecto.migrate"
Running POOL_SIZE=2 mix ecto.migrate on mysterious-meadow-6277... done, run.8089 (Free)