檢視原始碼 在 Heroku 上部署

我們需要的

我們需要的只有正常運作的 Phoenix 應用程式。對於需要簡單部署應用的使用者,請參閱 立刻執行指南

目標

本指南的主要目標是讓 Phoenix 應用程式在 Heroku 上執行。

限制

Heroku 是絕佳的平台,Elixir 在上面表現良好。但是,如果計畫運用 Elixir 和 Phoenix 的進階功能,您可能會遇到限制,例如

如果您是剛入門,或者您不期望使用以上功能,那麼 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,然而,如果使用 nodenpm,你需要安裝 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)