從 Heroku 遷移至 Fly.io

Lastor
Code 隨筆放置場
16 min readAug 3, 2023

最近花了點時間,把以前跟別人一起做的 side project 練習作,從 Heroku 搬到 Fly.io 上。

Heroku 取消免費方案後,搜尋了許多替代方案,大多人推薦的是 Fly.io 或是 Render,姑且先選擇了 Fly.io 來試試。

Fly.io 可以直接使用 GitHub 註冊,而他操作的方式主要是透過終端機 CLI 上傳部署,似乎無法像 Heroku 那樣,用 Web GUI 來操作,也沒辦法直接連接 GitHub repo 進行同步。

Fly.io 會先透過 CLI 工具進行初始化,偵測你的專案環境生成兩份設定檔,其中一份是 docker 的 image 設定,上傳後就直接用 docker 建構。

Fly.io 有提供 Heroku 的遷移方案,但我這邊選擇從零新架一個 App,想嘗試直接遷移的可以自行嘗試。

註冊之後就會有一連串的引導訊息,會教你起一個 Hello World 的 App,可以自行選擇要用 Node.js 亦或是其他語言。基本上照作就好,這篇會記錄一些自己認為的重點,或著該說是坑點的地方。

本文紀錄的是 2023 / 08 版本的 Fly.io。

Pricing 收費

先講一下收費,因為我被這個坑到了。目前 Google 搜尋到的許多文章,會建議使用 Fly.io,很大原因也是因為他有基本免費方案,且不用綁信用卡也能使用基本功能。

但他改了,可能因為 Heroku 難民大量湧入,所以改了收費相關的細節。可以參考官方關於 Pricing 的頁面。

重點大概是說,他們現在不提供一般意義上的免費方案,而是依照用量來收費,只要低於這個用量,無論你是用甚麼方案,都會是免費。

  • Up to 3 shared-cpu-1x 256mb VMs
  • 3GB persistent volume storage (total)
  • 160GB outbound data transfer

雖然我對 Server 這塊不是說很熟,有點沒看明白。但我的理解上應該是一個組織 (Personal 個人也算一個組織) 最多可以有 3 個 VM,每個都不得超過 256mb。

volume 是管儲存空間相關的東西,不得超過 3GB。最後一條應該網站流量吧,我不太確定。

操作 Fly.io 的 CLI 時,會有一個 scale 指令,這開頭的指令大多都是擴充容器配置用的。看網上文章,似乎之前的版本只有操作到 scale 時才會跟你說要綁信用卡。

但是當前版本的 Fly.io 已經改成要先綁信用卡才允許基本使用了。

另外要注意的是,官方的 Get Started 教學,會誘導你去設定超出免費額度的容器配置,跟著照作的會就會直接被收費。雖然收的不多就是,我大概被收了 0.2 美元。

註冊 & 安裝 flyctl CLI

自己挑一個喜歡的方式註冊,完成之後會跳出歡迎訊息,跟你說推薦安裝他們的 CLI 工具 flyctl。如果不想裝東西,也可以使用 Web 平台。

但這是一個 Modal 彈窗,我不小心點到旁邊就直接關掉了,然後就找不到他所謂的 Web 平台相關訊息。不過整體看來他是推薦使用 CLI 的,Web 操作平台就先無視吧。

直接照這個教學,依照不同 OS 安裝就好。

我的是 Windows,經過測試無法使用 cmder 以 Linux Shell 的語法安裝。還是得乖乖的用 PowerShell 去安裝。

$ pwsh -Command "iwr https://fly.io/install.ps1 -useb | iex"

安裝之後可以用以下指令確認是否可以呼叫,如果不行的話重開終端機試試。

$ flyctl -h

// alias
$ fly -h

然而官網教學沒有說明怎麼移除他,要去爬官方的討論串才有。大致上是沒有快速 uninstall 的指令或程式,要手動去移除他的資料夾。

# 列出指令所在的 path
$ where fly

// C:\Users\[USER_NAME]\.fly\bin\fly.exe

接下來要從 CLI 登入取得權限,他會打開瀏覽器做驗證。

$ fly auth login

建立 App

繼續跟著官方的新手引導走,或是在 Dashboard 首頁,都會提示說可以先選擇一個擅長的語言,建立一個 Hello World App。我是選擇 Node.js 然後跟著跑一遍。

他會給你一個 GitHub example repo,裡面是一個很簡單的 Express.js App。可以直接 clone 下來使用,或是要自己弄一個也可以。

反正一個簡單的專案弄好之後,就在專案根目錄執行 Fly.io 的 App 初始化。

$ fly launch

然後會跟 vite create app 類似,問你一些問題,有選項可以選。比較重要的是 region 區域,顧名思義是問機器要架在哪。預設會選擇所在地最近的地方。台灣的話預設會是香港。

但香港比較適合面對大陸客群,如果目標是東南亞可以選新加坡,如果是東北亞可以選日本東京。他這邊可以直接 key in 搜尋,輸入 sin 就會出現新加坡,輸入 to 就會出現東京。

之後可能會問說要不要設 PostgreSQL 以及 Redis,因為我這專案是用 MySQL,沒有用 Redis,所以都選 No。

基礎訊息選完之後,他會問要不要直接 deploy,但有時候不會問,我不確定判斷依據是啥。先選擇 No,因為還有其他必要設定得先弄。

初始化完成之後,會多出兩支檔案 fly.tomlDockerfile ,前者是機器的設定,後者是 docker image 的設定。他會自動偵測你的環境去產生預設配置,可以自己再去改。

回到 Fly.io 網站上的 Dashboard 重新整理,應該可以看到 App 已經被 new 出來,正在待命。

可以直接點進去看配置,或是用下列指令確認基本訊息,例如現在使用的 memory 有多少之類。要特別注意 memory 超過 256mb 就會被收費,預設啥都不改的話,就會是 256mb。

// 查看 App 基本訊息
$ fly status

// 查看 memory, CPU 個數這些配置
$ fly scale show

陽春的 Hello World 專案的話,可以直接 deploy,看看跑起來是啥感覺。deploy 期間,會列出進度 log,需要跑一陣子,因為他要跑 docker 然後裝一些東西。

$ fly deploy

跑完之後我記得會列出最終網址,也可以輸入指令直接開啟網頁,或是在 Web 進入該 App 設定,也能看到部署後的網址。

$ fly open

設定環境變數

fly.toml 這檔案看副檔名可以得知是 TOML 格式。

裡面有個 [env] 區塊,可以用來設定 非敏感 的環境變數,像是 port 這類東西可以直接寫在這。要注意一下 port 是否跟後面的 internal_port 一樣,有看到老外被這個寫不一樣給坑到。

# fly.toml
[env]
PORT = "8080"

[http_service]
internal_port = 8080
# ...

敏感訊息的環境變數,可以用 CLI 設置。

# set 環境變數
$ fly secrets set KEY=value

# 移除
$ fly secrets unset KEY

# list all
$ fly secrets list

這個設定是那種 set 上去就再也看不到值的設計,不像 Heroku 那樣還可以在後台打開來看。所以最好找個地方 memo 下來,免得以後忘了。

也可以在 Web 上操作,點進 App 之後 menu 有一個 Secrets 可以設定。

指定 Node.js 版本

Dockerfile 裡面可以直接指定 Node.js 版本。因為我這是老專案,當初是用 node10,所以我這邊手動指定用 v10 LTS 的版本。

# Adjust NODE_VERSION as desired
ARG NODE_VERSION=10.24.1

其實在前面執行 fly launch 的時候,就會跳提示說偵測到我是 node10,但該版本已不支援,自動設定為 node18。而我這邊把它改回來。

然後要把 Dockerfile 下面關於 python 的部分砍掉。這一段在 node10 會跑不起來。

# Install packages needed to build node modules
RUN apt-get update -qq && \
apt-get install -y python-is-python3 pkg-config build-essential

這一段 python-is-python3 的安裝,推測應該是跟 node-gyp 有關,這玩意跟 node-sass 有關,相信這陣子 node14 到 16 之間,應該很多人被 node-sass 坑過。

因為我這專案沒有用 Sass,也沒有其他會調用到 node-gyp 編譯的套件,所以直接把這段 python 砍掉不裝了,不然這問題太難搞。

那如果你是 node10,又有使用 Sass 的話,就要再研究看看該怎麼辦了…… 可能得改裝 python2,或是想辦法把專案升到 node14+,並把 node-sass 換成 sass。

配置 MySQL Database

Fly.io 似乎是推薦使用 PostgreSQL 的,要用 MySQL 的話可以在官網文件直接搜尋 MySQL。

基本上跟著官方教學跑就可以了,他這概念上是 MySQL 也要起一個 fly app 專門放置。

他有一個已經寫好的 docker image 用來配置 MySQL,所以只要在本機上開一個新資料夾作為空專案,直接初始化 fly app 就好。

# Make a directory for the mysql app
mkdir my-mysql
cd my-mysql

# Run `fly launch` to create an app
fly launch

區域設置跟前面網站 App 一樣的地方,初始化完之後會發現只有 fly.toml,不會有 Dockerfile

這邊要注意,官方教學會要你 create volumns,並設置 10gb,這邊很坑,記得最前面的免費額度是 3gb 嗎? 如果這跟著設 10gb 就會被收費了。

# 不要照官方教學,改設 3gb
fly volumes create mysqldata --size 3 # gb

接著設定你希望的 MySQL 帳號與密碼。

# Set secrets:
# MYSQL_PASSWORD - password set for user $MYSQL_USER
# MYSQL_ROOT_PASSWORD - password set for user "root"
fly secrets set MYSQL_PASSWORD=password MYSQL_ROOT_PASSWORD=password

這邊我實驗過,環境變數名稱不能改,改了就抓不到了。至於密碼可不可以為空,我就沒測了。

然後照官方文件,新起的 App 用 v2 的設定去重新配置 fly.toml,注意區域設定 primary_region 不要把他覆蓋掉了。

這邊因為設定太長,我就不貼上來了。要注意的是這一段,很坑。

[processes]
app = "--datadir /data/mysql
--default-authentication-plugin mysql_native_password
--performance-schema=OFF
--innodb-buffer-pool-size 64M"

照著他這樣寫,之後 deploy 會報錯說 string 不能有多行。但這邊把換行符去掉改單行會很難閱讀,可以參照 TOML 文件的多行寫法,改用三引號。

但要注意,要使用沒有斜線的版本,有斜線的會不 work。

# NG
app = """/
--datadir /data/mysql /
--default-authentication-plugin mysql_native_password /
--performance-schema=OFF /
--innodb-buffer-pool-size 64M /
"""

# OK
app = """
--datadir /data/mysql
--default-authentication-plugin mysql_native_password
--performance-schema=OFF
--innodb-buffer-pool-size 64M
"""

MySQL 版本官方文件有標註 8.0.33 有 bug,所以用 8.0.32。

# As of 04/25/2023:
# MySQL 8.0.33 has a bug in it
# so avoid that specific version
[build]
image = "mysql:8.0.32"

環境變數設定的 MYSQL_USER 要注意一下,不能設 root 之外,也不能設 user ,因為預設已經有這兩個 user 了。

[env]
MYSQL_DATABASE = "some_db"
MYSQL_USER = "non_root_user" # 不能設 "root" 或 "user"

改好之後就可以 deploy 了。

$ fly deploy

要注意官方文件會推薦你說,MySQL 8+ 最好擴充記憶體到 2048mb,千萬別設定這個,擴上去就直接收費了。

# 別設這個, 超過 256mb 會被收費
$ fly scale memory 2048

DB Migrate 與 Seed

MySQL deploy 上去之後,需要去初始化 table 以及上種子資料。

這邊網上查到的做法,其他人會推薦我們在「網站」的那個 App 上去設定 deploy 之後幫我們跑一次 migrate。

# fly.toml, 我用的 ORM 是 sequelize
[deploy]
release_command = "npx sequelize db:migrate"

但這有個問題,就是像 sequelize 這種 ORM,他得額外安裝一個 sequelize-cli 才能運行終端機指令。

也就是說,必須要讓虛擬機也裝上 sequelize-cli 才行,所以得把它裝到 package.json 的 dependencies 上面。(Fly.io 的 docker 配置,預設是不裝 devDependencies)

所以這邊要考量一下要不要這樣處理,得多裝一個 runtime 用不到的套件。

我們也可以選擇在本地直接連線 Fly.io 的 MySQL 去操作,flyctl 有幫我們做這方面的快速配置。可以在 MySQL 的 App 根目錄下,輸入以下指令設定反向代理。

# 反向代理 localhost:3306 到 Fly.io mysql app
$ flyctl proxy 3306 -a [mysql-app-name]

# 指定 local port
$ flyctl proxy 13306:3306 -a [mysql-app-name]

MySQL 預設的 port 都是 3306,所以可以指定代理 localhost:3306。這樣我們就能使用 DBeaver、Workbench 這些可以連線 MySQL 的 GUI,直接往該位置連線 (127.0.0.1:3306 也可以)。

如果 port 3306 已被占用,可以改用第二行指令,在冒號 : 左手邊去設定 localhost 要使用的 port,右手邊是 remote 的 port,維持不變就好。

當然,GUI 連的上,Node.js ORM 當然也可以,手動去改一下 sequelize 的設定。

// sequelize config
module.exports = {
"development": {
"username": process.env.MYSQL_USER,
"password": process.env.MYSQL_KEY,
"database": process.env.MYSQL_DATABASE,
"host": process.env.MYSQL_HOST, // 設為 localhost 或 127.0.0.1
"port": 13306, // 我的 3306 有被占用, 所以改 13306
"dialect": "mysql",
// ...
},
// ...
}

改完之後,就直接在本地專案跑 migrate 跟 seed 就好了。跑完之後可以用 GUI 確認資料有沒有都上去了。

# mysql
$ sequelize db:migrate
$ sequelize db:seed:all

修改專案 production DB 連線設定

MySQL 資料都上去之後,得改一下專案 sequelize production 的設定。

原本是 Heroku 的話,因為 Heroku 的 DB 會直接給一串包含許多訊息的 URI。所以 ORM 的 config 可能會這樣寫。

// sequelize config
module.exports = {
// ...
"production": {
"use_env_variable": "JAWSDB_URL", // 自己加的 prop, value 是環境變數 name
"dialect": "mysql",
// ...
}
}

// invoke
const env = process.env.NODE_ENV || 'development'
const config = require(./config.js')[env]
const DB_HOST = process.env[config.use_env_variable]

const sequelize = new Sequelize(DB_HOST, config)

這一段有點繞,反正就是指定說 production 的時候,MySQL 的 host 位置從環境變數 process.env.JAWSDB_URL 來拿取。JawsDB 是 Heroku 提供的其中一種 MySQL DB。這個環境變數可以自己隨便命名。

而 Fly.io 要在容器裡連接 MySQL App,不是用 URI 的方式,而是直接在 app name 屁股加一個尾綴 [mysql_app_name].internal

所以 sequelize config 要改回一般的設定方式。

// sequelize config
module.exports = {
// ...
"production": {
"username": process.env.MYSQL_USER,
"password": process.env.MYSQL_KEY,
"database": process.env.MYSQL_DATABASE,
"host": process.env.MYSQL_HOST, // 這邊最後要拿到 "my-mysql.internal"
"dialect": "mysql",
// ...
},
}

改完之後,把網站的 App 給 deploy 上去,應該就可以在 Server 上連線到 MySQL 了。

--

--

Lastor
Code 隨筆放置場

Web Frontend / 3D Modeling / Game and Animation. 設計本科生,前遊戲業 3D Artist,專擅日本動畫與遊戲相關領域。現在轉職為前端工程師,以專業遊戲美術的角度涉足 Web 前端開發。