檢視原始碼 在 Heroku 上部署
我們需要的
我們需要的只有正常運作的 Phoenix 應用程式。對於需要簡單部署應用的使用者,請參閱 立刻執行指南。
目標
本指南的主要目標是讓 Phoenix 應用程式在 Heroku 上執行。
限制
Heroku 是絕佳的平台,Elixir 在上面表現良好。但是,如果計畫運用 Elixir 和 Phoenix 的進階功能,您可能會遇到限制,例如
連線數量有限。
- Heroku 限制同時連線數 以及 每個連線持續時間。通常會使用 Elixir 製作需要大量同時活躍連線的即時應用程式,而 Phoenix 能夠 在單一伺服器上處理超過 200 萬個連線。
分散式群集不可行。
- Heroku 將 dymos 相互隔離。這表示 分散式 Phoenix 頻道 和 分散式任務 這類功能需要倚賴類似 Redis 之類的東西,而非 Elixir 的內建分散功能。
代理、通用伺服器 和 ETS 之類的記憶體內狀態,每 24 小時就會消失。
- Heroku 每 24 小時會重新啟動 dymos,不論節點是否正常運作。
內建 偵測器 無法搭配 Heroku 使用。
- Heroku 允許連線進入您的 dyno,但是您無法使用偵測器觀察 dyno 的狀態。
如果您是剛入門,或者您不期望使用以上功能,那麼 Heroku 應該可以滿足您的需求。舉例來說,如果您正在將一個執行在 Heroku 上的既有應用程式遷移至 Phoenix,並保留類似的功能組,那麼 Elixir 的表現將會像您目前的堆疊一樣好,甚至更好。
如果您想要一個沒有這些限制的平台即服務,請嘗試 Gigalixir。如果您想部署至如 EC2、Google Cloud 等的雲端平台,請考慮使用 mix release
。
步驟
讓我們將這個過程分成幾個步驟,這樣我們就可以掌握進度。
- 初始化 Git 儲存庫
- 註冊 Heroku
- 安裝 Heroku Toolbelt
- 建立並設定 Heroku 應用程式
- 使我們的專案準備好供 Heroku 使用
- 部署時間!
- 有用的 Heroku 指令
初始化 Git 儲存庫
Git 是一個廣受歡迎的去中心化版本控制系統,也用於將應用程式部署至 Heroku。
在我們能夠推送到 Heroku 之前,我們需要在我們的專案目錄中初始化一個本機 Git 儲存庫並提交我們的檔案至此儲存庫。我們可以透過以下指令做到這一點
$ git init
$ git add .
$ git commit -m "Initial commit"
Heroku 在此處提供了一些關於如何使用 Git 的絕佳資訊 here。
註冊 Heroku
註冊 Heroku 非常簡單,只要前往 https://signup.heroku.com/ 並填寫表單即可。
免費方案會提供我們一個網頁 dyno 和一個工作者 dyno,以及一個免費的 PostgreSQL 和 Redis 執行個體。
這些 dyon 旨在供測試和開發使用,並附有一些限制。若要執行一個製作應用程式,請考慮升級至付費方案。
安裝 Heroku Toolbelt
一旦我們註冊完畢,我們便可在此處 下載符合我們系統的 Heroku Toolbelt 正確版本。
作為 Toolbelt 一部分的 Heroku CLI 對於建立 Heroku 應用程式、列出目前對既有應用程式執行的 dynos、追蹤日誌或執行一次性指令(例如 mix 任務)非常有用。
建立並設定 Heroku 應用程式
在 Heroku 上部署 Phoenix app 有兩種不同的方法,一種是使用 Heroku buildpack,另一種是使用其 container stack。這兩種方法的差別在於我們使用的方式,來讓 Heroku 處理我們的 build。在 buildpack 的案例中,我們需要更新 Heroku 上 app 的設定檔,來使用 Phoenix/Elixir 特定 buildpack。在 container 方法中,我們對於想要如何設定 app 有更多的主導權,我們可以使用 Dockerfile
和來定義 container image heroku.yml
。本節將探討 buildpack 的方法,如果要使用 Dockerfile,通常建議將 app 轉換成使用版本,我們稍後會說明。
建立應用程式
一個 buildpack 是用於封裝架構和/或執行時期支援的便捷方式。Phoenix 需要 2 個 buildpack 才能在 Heroku 上執行,第一個添加了基本 Elixir 支援,第二個添加了 Phoenix 特定命令。
在安裝 Toolbelt 之後,我們來建立 Heroku 應用程式,我們將使用 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 命令時,它可能會提示我們登入,如果發生這種情況,只要輸入你在註冊時指定的電子郵件和密碼即可。
注意:Heroku 應用程式的名稱是上面輸出的「建立」後的隨機字串(mysterious-meadow-6277),這將是唯一的,因此預期會看到「mysterious-meadow-6277」以外的名稱。
注意:輸出中的網址是我們應用程式的網址,如果我們現在在瀏覽器中開啟它,我們將會看到 Heroku 的預設歡迎頁面。
注意:如果在執行
heroku create
之前沒有初始化 Git 儲存庫,我們在這個時間點上就不會正確設定 Heroku 遠端儲存庫,我們可以透過執行以下程式碼手動設定:heroku git:remote -a [我們的應用程式名稱]。
buildpack 使用預先定義的 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*"
最後,讓我們告訴 build pack 如何啟動我們的 Web 伺服器,在專案的根目錄建立一個名為 Procfile
的檔
web: mix phx.server
選擇性:Node、npm 和 Phoenix 靜態 buildpack
預設情況下,Phoenix 使用 esbuild
並為你管理所有 asset,然而,如果使用 node
和 npm
,你需要安裝 Phoenix 靜態 buildpack 來處理它們
$ 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")
最後,如果你計畫要使用 websocket,那麼我們需要在 lib/hello_web/endpoint.ex
中縮短 websocket 傳輸的逾時時間。如果你不計畫使用 websocket,那麼將其設定為 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 Add-on 後,DATABASE_URL
設定變數會由 Heroku 自動建立。我們可以使用 Heroku toolbelt 建立資料庫
$ heroku addons:create heroku-postgresql:mini
現在,我們設定 POOL_SIZE
設定變數
$ heroku config:set POOL_SIZE=18
此值應該略低於可用的連線數,留幾個連線用於遷移和混合作業。小型資料庫允許 20 個連線,因此我們將此數字設為 18。如果其他動態程式會共用資料庫,請減小 POOL_SIZE
,以讓每個動態程式獲得均等的資源。
稍後執行混合作業時(在我們將專案 Push 到 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 殼層連線至你的 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)