Phoenix 应用部署到 Heroku 完整实战指南:Buildpack 与 Container 双方案详解
【免费下载链接】phoenixPeace of mind from prototype to production项目地址: https://gitcode.com/gh_mirrors/ph/phoenix
本指南基于 Phoenix 官方部署文档 guides/deployment/heroku.md 编写,目标是把一个可运行的 Phoenix 应用完整部署到 Heroku 平台。你将掌握:通过 Elixir Buildpack 与 Container 容器栈两种方式部署的完整流程、
elixir_buildpack.config/Procfile/heroku.yml等关键配置文件、生产环境 SSL 与 WebSocket 超时的正确配置,以及环境变量、数据库迁移与常见故障排查的实战技巧。
前置条件:一个可工作的 Phoenix 应用
本指南唯一需要的硬性条件是一个本地可运行的 Phoenix 应用。如果你还没有应用可以部署,请先跟随 Up and Running 入门指南 创建一个基础应用再继续。
在动手部署前,也建议先通读 部署概述(Deployment 简介),它梳理了生产部署的三步主线:应用密钥(secrets)处理 → 静态资源编译 → 生产环境启动服务器。Heroku 指南正是这条主线在 PaaS 平台上的具体落地。
认识平台限制:Heroku 与 Elixir/Phoenix 的边界
Heroku 是一个优秀的平台,Elixir 在其上运行良好。但如果你打算使用 Elixir 和 Phoenix 的进阶特性,需要注意以下限制:
- 连接数受限:Heroku 限制了同时连接数与每个连接的持续时间。Elixir 常被用于实时应用,这类应用需要大量并发长连接——Phoenix 官方博客曾展示过单服务器支撑超 200 万 WebSocket 连接的场景,而 Heroku 的路由层会限制这种能力。
- 无法分布式集群:Heroku 的防火墙将 dyno 相互隔离,这意味着分布式 Phoenix Channels、分布式任务等能力无法直接依赖 Erlang 内建分发,需要借助 Redis 之类的中间件。
- 内存态数据每 24 小时丢失:Agent、GenServer、ETS 中保存的内存状态,会因 Heroku 每 24 小时强制重启 dyno 而全部丢失(无论节点是否健康)。
- 内建 observer 不可用:Heroku 虽然允许连接进 dyno,但你无法用 observer 观察 dyno 内部状态。
什么时候 Heroku 够用?如果你刚起步,或并不依赖上述特性,Heroku 完全足够。例如:把运行在 Heroku 上的现有应用迁移到 Phoenix,且功能集相近时,Elixir 的表现会与现有技术栈相当甚至更好。如果需要一个没有这些限制的 PaaS,官方文档侧边栏列有其他替代方案;如果倾向自管云主机(EC2、Google Cloud 等),建议改用mix release部署,详见 releases 指南(其中包含可直接使用的示例 Dockerfile,仓库模板见 priv/templates/phx.gen.release/Dockerfile.eex)。
部署步骤总览
整个流程可以拆解为以下几步,便于跟踪进度:
- 初始化 Git 仓库
- 注册 Heroku 账号
- 安装 Heroku Toolbelt(CLI)
- 创建并配置 Heroku 应用
- 让项目适配 Heroku
- 正式部署
- 常用 Heroku 命令
初始化 Git 仓库
Heroku 通过 Git 推送代码完成部署,因此在推送之前,需要先在项目目录初始化本地 Git 仓库并提交文件:
$ git init $ git add . $ git commit -m "Initial commit"注册 Heroku 账号
前往 Heroku 官网注册页填写表单即可完成注册。Free 计划会提供 1 个 web dyno 和 1 个 worker dyno,并附带免费的 PostgreSQL 与 Redis 实例——这些资源定位是测试与开发,存在各种限制;要运行生产应用,请升级到付费套餐。
安装 Heroku Toolbelt
注册完成后,下载对应系统的 Heroku Toolbelt。其中包含的Heroku CLI非常有用,可以完成:创建 Heroku 应用、列出某应用正在运行的 dyno、实时查看日志(tail logs)、以及运行一次性命令(例如执行 mix 任务)。
方式一:通过 Buildpack 创建并配置 Heroku 应用
在 Heroku 上部署 Phoenix 应用有两条路线:使用 Heroku buildpacks,或使用其 container 栈。两者的核心区别在于如何告诉 Heroku 处理构建过程:
- Buildpack 路线:需要在 Heroku 上配置 Phoenix/Elixir 专属 buildpack,由平台按既定流程编译;
- Container 路线:通过
Dockerfile和heroku.yml完全自定义容器镜像,对应用组装拥有更多控制权(通常建议配合 release 使用,后面详述)。
本节先深入 buildpack 路线。
创建应用:两个必备 Buildpack
Buildpack 是一种打包框架/运行时支持的便捷机制。Phoenix 需要两个 buildpack 才能在 Heroku 上运行:第一个提供基础 Elixir 支持,第二个补充 Phoenix 专属命令。
安装好 Toolbelt 后,用最新版 Elixir buildpack 创建应用:
$ 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 命令时可能会提示登录,输入注册时的邮箱和密码即可;
- 输出中 "Creating" 后面的随机字符串(
mysterious-meadow-6277)就是应用名,每次创建都会不同; - 输出里的 URL 就是应用地址,现在在浏览器打开会看到 Heroku 默认欢迎页;
- 如果执行
heroku create之前没有初始化 Git 仓库,此时 Heroku 远程仓库不会自动配置好,需要手动执行heroku git:remote -a [your-app-name]补上。
固定 Elixir / Erlang 版本:elixir_buildpack.config
Buildpack 内置了一套预定义的 Elixir 和 Erlang 版本。为避免部署时出现意外,最佳实践是在elixir_buildpack.config中显式声明与开发环境、CI 环境一致的版本。在项目根目录创建该文件:
# Elixir version elixir_version=1.18.4 # Erlang version # https://github.com/HashNuke/heroku-buildpack-elixir-otp-builds/blob/master/otp-versions erlang_version=27.0 # 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*"这里hook_post_compile调用的mix assets.deploy是 Phoenix 项目mix.exs中定义的 alias——它负责编译资源并生成带摘要的静态清单文件,供生产环境快速服务资源(参见 部署概述 中“编译应用资源”一节;本仓库的 alias 定义见 mix.exs)。
声明启动方式:Procfile
接着在项目根目录创建Procfile,告诉 buildpack 如何启动 Web 服务器:
web: mix phx.server可选:Node / npm 与 Phoenix Static buildpack
默认情况下,Phoenix 使用esbuild替你管理全部前端资源。但如果你用的是node和npm,就需要额外安装 Phoenix Static buildpack 来处理它们:
$ heroku buildpacks:add https://github.com/gigalixir/gigalixir-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/gigalixir/gigalixir-heroku-buildpack-phoenix-static.git使用该 buildpack 时,应把资源打包全部委托给npm,因此需要从elixir_buildpack.config中移除hook_post_compile,把它挪到assets/package.json的 deploy 脚本里:
{ ... "scripts": { "deploy": "cd .. && mix assets.deploy && rm -f _build/esbuild*" } ... }同样地,Phoenix Static buildpack 内置了预定义 Node.js 版本,为消除部署差异,请在项目根目录创建phoenix_static_buildpack.config显式声明版本:
# Node.js version node_version=10.20.1(完整配置项请参考该 buildpack 的官方文档;你也可以编写自定义构建脚本,这里使用其默认脚本即可。)
最后注意:由于使用了多个 buildpack,可能出现顺序错乱的问题(Elixir buildpack 必须先于 Phoenix Static buildpack 运行)。请务必确保Phoenix Static buildpack 排在最后。
让项目适配 Heroku:SSL、Host 与 WebSocket 超时
每个新 Phoenix 项目都会自带config/runtime.exs,它在**启动时(boot time)**从环境变量加载配置和密钥——这与 Heroku 的最佳实践(12-factor app)天然契合。参考仓库模板 installer/templates/phx_single/config/runtime.exs.eex 可以看到:SECRET_KEY_BASE缺失时应用会直接raise并提示用mix phx.gen.secret生成;PHX_HOST默认回落到example.com;HTTP 端口从PORT环境变量读取。因此剩余工作主要是配置 URL 与 SSL。
第一步:强制 HTTPS(编译期配置)
告诉 Phoenix 只使用 HTTPS 版本的网站。找到config/prod.exs中的 endpoint 配置:
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属于编译期(compile time)配置,在runtime.exs中设置不会生效,所以必须写在这里。新项目模板 installer/templates/phx_single/config/prod.exs.eex 默认就带上了force_ssl: [rewrite_on: [:x_forwarded_proto], exclude: [...]]的示例,其中rewrite_on: [:x_forwarded_proto]正是为了让 Heroku 这类反向代理后面的应用能识别 HTTPS 请求头。
第二步:配置 Host(运行时配置)
然后在config/runtime.exs中加上host:
config :scaffold, ScaffoldWeb.Endpoint, url: [host: host, port: 443, scheme: "https"]第三步:数据库连接启用 SSL
取消仓库(repository)配置中# ssl: true,一行的注释,最终如下:
config :hello, Hello.Repo, ssl: true, url: database_url, pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")第四步:降低 WebSocket 超时
如果你计划使用 WebSocket,需要降低lib/hello_web/endpoint.ex中 WebSocket 传输的超时时间;如果不用 WebSocket,保持默认即可。参考仓库端点模板 installer/templates/phx_web/endpoint.ex.eex:
defmodule HelloWeb.Endpoint do use Phoenix.Endpoint, otp_app: :hello socket "/socket", HelloWeb.UserSocket, websocket: [timeout: 45_000] ... end同时在 Heroku 上设置 host:
$ heroku config:set PHX_HOST="mysterious-meadow-6277.herokuapp.com"为什么是 45 秒?从 Phoenix 源码(lib/phoenix/endpoint.ex)的socket/3文档可以看到,WebSocket 的:timeout表示“连接最后一次收到数据后保持打开的超时时间”,默认是60_000ms。而 Heroku 的 HTTP 超时窗口是55 秒。把 Phoenix 侧的超时设为 45 秒,可以确保任何空闲连接都会在到达 Heroku 55 秒超时窗口之前被 Phoenix 主动关闭,避免连接被平台意外切断。
在 Heroku 中创建环境变量
DATABASE_URL 与 POOL_SIZE
DATABASE_URL配置变量会在添加 Heroku Postgres add-on 时自动创建。先用 Toolbelt 创建数据库:
$ heroku addons:create heroku-postgresql:mini然后设置POOL_SIZE:
$ heroku config:set POOL_SIZE=18这个值应略低于可用连接数,留出几条给迁移和 mix 任务使用。mini 数据库允许 20 条连接,所以这里设为 18。如果多个 dyno 共享同一数据库,需要按 dyno 数量均分,相应调低POOL_SIZE。
之后在 Heroku 上运行 mix 任务时(项目已推送之后),也要限制其连接池:
$ heroku run "POOL_SIZE=2 mix hello.task"这样 Ecto 就不会试图打开超过可用上限的连接。
SECRET_KEY_BASE
还需要基于随机字符串创建SECRET_KEY_BASE。先用mix phx.gen.secret生成新密钥:
$ mix phx.gen.secret xvafzY4y01jYuzLm3ecJqo008dVnU3CN4f+MamNd1Zue4pXvfvUjbiXT8akaIF53你的随机字符串一定不同,切勿使用示例值。
从实现上看(lib/mix/tasks/phx.gen.secret.ex),mix phx.gen.secret默认生成64 字符的密钥,也接受自定义长度参数(mix phx.gen.secret [length],最小 32),内部通过:crypto.strong_rand_bytes/1取强随机字节再 Base64 编码得到。
然后把它设置到 Heroku:
$ heroku config:set SECRET_KEY_BASE="xvafzY4y01jYuzLm3ecJqo008dVnU3CN4f+MamNd1Zue4pXvfvUjbiXT8akaIF53" Setting config vars and restarting mysterious-meadow-6277... done, v3 SECRET_KEY_BASE: xvafzY4y01jYuzLm3ecJqo008dVnU3CN4f+MamNd1Zue4pXvfvUjbiXT8akaIF53该值由config/runtime.exs在启动时读取,用于签名/加密 Cookie 与其他机密数据;开发/测试环境使用的是模板内置的默认值,生产环境必须通过环境变量提供(缺失即报错),且不应提交到版本控制系统。
部署时刻!
提交所有改动并推送到 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上面的输出是早期版本的示例日志(注意其中WARNING: elixir_buildpack.config wasn't found——当时还没提交该文件,所以回落到默认版本),今天执行时流程一致但版本号、输出细节会不同。上面的日志也印证了部署流水线:检测到 elixir 应用 → 检查/安装 Erlang 与 Elixir → 安装 Hex/rebar → 拉取依赖并编译 → 导出MIX_ENV=prod等配置 → 压缩并启动。
在终端执行heroku open即可用浏览器打开 Phoenix 欢迎页。如果应用使用 Ecto 访问数据库,首次部署后还需要运行迁移:
$ heroku run "POOL_SIZE=2 mix ecto.migrate"大功告成!
方式二:通过 Container 栈部署
创建 Heroku 应用
把应用的栈设置为container,即可用Dockerfile定义应用组装方式:
$ heroku create Creating app... done, ⬢ mysterious-meadow-6277 $ heroku stack:set container在项目根目录新增heroku.yml文件,可在其中定义应用使用的 addons、镜像构建方式以及传递给镜像的配置。示例:
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使用 release 并编写 Dockerfile
接下来需要在项目根目录定义包含应用的Dockerfile。强烈建议配合 release 使用——release 只打包实际用到的 Erlang/Elixir 部分,能显著缩小镜像体积。请先阅读 releases 指南,其末尾附有可直接使用的示例 Dockerfile;该模板在仓库中的位置是 priv/templates/phx.gen.release/Dockerfile.eex,采用多阶段构建:builder 阶段安装 Hex/rebar、拉取 prod 依赖、编译代码与资源并mix release,final 阶段仅拷贝编译产物,以nobody用户运行/app/bin/server。
镜像定义就绪后,推送应用到 Heroku,平台会自动开始构建镜像并部署。
常用 Heroku 命令
查看应用日志(加--tail可实时跟踪):
$ 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(远程 IEx 调试)
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 会话:
iex --sname console --remsh server。
之后你就拥有一个通向 dyno 内部 Erlang VM 的 IEx 会话了。注意此方式无法使用 observer 查看状态(见前文“平台限制”)。
故障排查
编译错误(Compilation Error)
偶尔会出现“本地编译成功、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: ! Push rejected, failed to compile elixir app remote: remote: Verifying deploy... remote: remote: ! Push rejected to mysterious-meadow-6277. To https://git.heroku.com/mysterious-meadow-6277.git原因通常是过期的依赖没有正确重新编译。可以在应用根目录的elixir_buildpack.config中加入一行,强制 Heroku 每次部署都重编所有依赖:
always_rebuild=true提交该文件后重新推送即可解决。
连接超时错误(Connection Timeout Error)
如果执行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解决办法是给 run 命令加上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)补充:多实例下的 Long-Polling 与状态问题
heroku.md通篇假设单一 web dyno 部署。若生产环境运行多台机器(这也是官方推荐的做法),部署时还需要留意 Long-Polling 传输的问题:Phoenix 的 Socket 同时支持 WebSocket 与 Long-Polling 两种传输(installer/templates/phx_web/endpoint.ex.eex 中同时声明了websocket:与longpoll:配置),客户端会在 WebSocket 连接失败时按配置的longPollFallbackMs自动回退到 Long-Polling(前端实现见 assets/js/phoenix/socket.js)。由于 Long-Polling 的每个请求都可能落到不同机器,要让长连接状态得以保留,部署方案必须满足其一:启用 Erlang VM 集群能力(默认Phoenix.PubSub可跨节点广播)、更换Phoenix.PubSub适配器(如 Redis)、或由部署平台实现粘性会话(sticky sessions)。这一点在 部署概述 的“Clustering and Long-Polling Transports”一节有更完整的论证——而正如前文“平台限制”所说,Heroku 的 dyno 默认互相隔离,既不支持原生集群,也未必提供粘性会话,这正是需要考虑替代 PaaS 或自管主机的原因。
【免费下载链接】phoenixPeace of mind from prototype to production项目地址: https://gitcode.com/gh_mirrors/ph/phoenix
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考