在 Azure Container Apps (ACA) 上使用 Node.js 搭配 SQLite 是可行的,但因為容器具備「重啟後資料會消失」的特性,你必須進行額外的設定才能確保資料庫不會遺失,並避免檔案鎖定的問題。
以下是實現方式與需要注意的重點:
必須使用持久化儲存 (Azure Files)
預設情況下,容器內部的檔案系統是暫時性的。如果你把 database.sqlite 放在容器內的資料夾,每次部署新版本或容器重啟,資料就會重置。
- 解決方案:你需要建立一個 Azure Storage Account (File Share),並將它掛載 (Mount) 到 ACA 的容器中。
- 掛載路徑:例如將 File Share 掛載到容器內的
/data目錄,然後將 SQLite 的連線路徑指向/data/database.sqlite。
關鍵:解決資料庫鎖定問題 (SQLITE_BUSY)
這是最常遇到的坑。因為 Azure Files 預設使用 SMB 協定,其檔案鎖定機制與 SQLite 不相容,常會導致 Error: SQLITE_BUSY: database is locked。
- 修正方法:在掛載設定中,必須加入
nobrl(No Byte Range Locks) 選項。 - 設定範例:在 ACA 的環境儲存設定中,確保掛載選項包含這個參數,這樣 SQLite 才能正常寫入檔案。
限制與建議
雖然技術上可行,但在雲端原生環境(如 ACA)使用 SQLite 有以下限制:
- 無法水平擴展:如果你將 Replicas (執行個體數量) 設為超過 1 個,多個容器同時寫入同一個 SQLite 檔案極易造成損壞或衝突。SQLite 只適合單一執行個體 (Single Instance) 的情境。
- 效能問題:因為資料庫檔案是透過網路掛載的 (Azure Files),讀寫速度會比本地磁碟慢,不適合高併發的應用。
- 部署中斷:ACA 在更新版本時,舊的容器還沒關閉,新的就可能已經啟動並嘗試存取同一個檔案,這也會引發鎖定錯誤。
總結建議
如果你只是做小型專案、個人工具或原型開發,SQLite + Azure Files (加上 nobrl) 是最省錢的選擇。
但如果你的應用預計會有較多流量,或者需要多台容器同時運作,建議改用 Azure Database for PostgreSQL 或 Cosmos DB,這類服務才是針對雲端擴展性設計的。
既然你打算在 Azure Container Apps (ACA) 實作,以下是針對 Node.js 搭配 Azure Files 掛載 SQLite 的具體步驟與程式碼範例。
第一步:準備 Azure 儲存體
你必須先在 Azure 建立一個 Storage Account,並在裡面建立一個 File Share。這是因為容器重啟後內部資料會被重置,只有掛載外部空間才能持久保存 sqlite 檔案。
- 在 Azure 入口網站建立 Storage Account。
- 建立一個 File Share(檔案共用),名稱可以叫
sqlitedata。 - 到 ACA 的「環境」設定中,點選「儲存體」,將剛才的 File Share 新增進去。重要提示:在掛載選項中,請務必確認支援
nobrl,這能防止 SQLite 的檔案鎖定錯誤。
第二步:在 ACA 容器設定掛載路徑
在部署 Container App 時,你需要將剛才定義的儲存體掛載到容器內的特定路徑。
- 儲存體名稱:選擇你剛才在環境層級定義的名稱。
- 掛載路徑:例如
/app/data。
這樣一來,你的 Node.js 程式只要存取 /app/data/database.sqlite,實際上就是在讀寫 Azure Files 上的檔案。
第三步:Node.js 程式碼範例
在 Node.js 中使用 sqlite3 或 better-sqlite3 套件。建議使用環境變數來管理資料庫路徑,方便開發與雲端環境切換。
JavaScript
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
// 從環境變數讀取掛載路徑,預設為本地路徑方便測試
const dbDir = process.env.DB_PATH || './data';
const dbPath = path.join(dbDir, 'database.sqlite');
// 確保連線時路徑正確
const db = new sqlite3.Database(dbPath, (err) => {
if (err) {
console.error('無法連線至 SQLite:', err.message);
} else {
console.log('成功連線至掛載空間中的 SQLite');
}
});
// 範例查詢
db.serialize(() => {
db.run("CREATE TABLE IF NOT EXISTS counts (id INTEGER, val INTEGER)");
db.run("INSERT INTO counts VALUES (1, 100)");
db.each("SELECT id, val FROM counts", (err, row) => {
console.log(`ID: ${row.id}, Value: ${row.val}`);
});
});
常見問題與解決方案
如果你的 Node.js backend 在啟動時噴出 SQLITE_CANTOPEN 或 SQLITE_BUSY:
- 權限問題:請確認 Dockerfile 中的 User 是否有權限讀寫
/app/data。如果使用非 root 使用者,記得在 Dockerfile 用CHOWN修改權限。 - 連線字串:在某些情況下,使用
file:/app/data/database.sqlite?cache=shared這種 URI 格式可以增加穩定性。 - 擴展限制:請務必將 ACA 的 Minimum Replicas 與 Maximum Replicas 都設為 1。如果設為多個,不同容器同時寫入同一個檔案會導致資料毀損。
部署此設定的 Dockerfile 範本
針對 Azure Container Apps (ACA) 掛載 SQLite 的 Node.js 環境,Dockerfile 的重點在於確保目錄權限正確,並讓容器具備讀寫掛載磁碟的能力。
以下是一個實用的 Dockerfile 範本,建議配合 node:18-slim 或 node:20-slim 以減少映像檔體積。
Dockerfile 範本
Dockerfile
# 使用輕量級的 Node.js 映像檔
FROM node:20-slim
# 安裝 SQLite3 執行環境所需的工具(部分 npm 套件編譯時需要)
RUN apt-get update && apt-get install -y \
python3 \
make \
g++ \
&& rm -rf /var/lib/apt/lists/*
# 設定工作目錄
WORKDIR /app
# 先複製 package.json 以利用 Docker 快取層
COPY package*.json ./
# 安裝依賴套件
RUN npm install --production
# 複製其餘程式碼
COPY . .
# 建立預計掛載的資料夾,並確保權限
# 假設你在 ACA 設定將 Azure Files 掛載到 /app/data
RUN mkdir -p /app/data && chmod 777 /app/data
# 定義環境變數,讓 Node.js 知道資料庫在哪裡
ENV DB_PATH=/app/data
# 暴露應用程式埠號
EXPOSE 8080
# 啟動應用程式
CMD ["node", "index.js"]
設定重點說明
- 權限處理:在 Dockerfile 中預先
mkdir -p /app/data是為了確保路徑存在。雖然 Azure Files 掛載時會覆蓋該目錄,但預設建立目錄能避免部分 Node.js 套件因找不到路徑而報錯。 - 環境變數
DB_PATH:這樣你在程式碼中可以用process.env.DB_PATH || './'來動態切換本地開發與雲端環境。 - 避免使用 root:為了安全,ACA 建議使用非 root 使用者執行。如果你在 Dockerfile 最後加上
USER node,請確保/app/data的權限已經透過chown -R node:node /app/data轉移給該使用者。
ACA 部署建議
當你使用這個 Dockerfile 部署到 ACA 時,請在 Azure 入口網站或透過 CLI 檢查以下設定:
- 儲存體掛載:將你的 Azure File Share 掛載點設為
/app/data。 - 連線字串:在 Node.js 中連線 SQLite 時,請使用絕對路徑
/app/data/database.sqlite。 - 單一執行個體:在 ACA 的「調整調整 (Scaling)」設定中,將 Min replicas 與 Max replicas 都設為 1。這是因為 SQLite 不支援多個容器同時寫入同一個檔案。
Azure CLI 部署指令
這是一份完整的 Azure CLI 部署腳本範本。這套指令會幫你建立所需的儲存體、檔案共用,並將其掛載到 Azure Container Apps (ACA) 中。
請將 < > 中的變數替換成你的實際名稱。
Azure CLI 部署指令
Bash
# 1. 設定變數
RESOURCE_GROUP="MyResourceGroup"
LOCATION="eastasia"
STORAGE_ACCOUNT="mystorage$(date +%s)" # 儲存體名稱需唯一
FILE_SHARE_NAME="sqlitedata"
ENVIRONMENT_NAME="my-aca-env"
APP_NAME="nodejs-sqlite-app"
IMAGE_NAME="<你的容器映像檔路徑,例如 myacr.azurecr.io/node-sqlite:v1>"
# 2. 建立儲存體帳戶與 File Share
az storage account create --name $STORAGE_ACCOUNT --resource-group $RESOURCE_GROUP --location $LOCATION --sku Standard_LRS
STORAGE_KEY=$(az storage account keys list --resource-group $RESOURCE_GROUP --account-name $STORAGE_ACCOUNT --query "[0].value" -o tsv)
az storage share create --name $FILE_SHARE_NAME --account-name $STORAGE_ACCOUNT --account-key $STORAGE_KEY
# 3. 在 ACA 環境中定義儲存體掛載(關鍵:設定 nobrl 防止 SQLite 鎖定)
az containerapp env storage set \
--name $ENVIRONMENT_NAME \
--resource-group $RESOURCE_GROUP \
--storage-name "my-sqlite-storage" \
--account-name $STORAGE_ACCOUNT \
--account-key $STORAGE_KEY \
--share-name $FILE_SHARE_NAME \
--access-mode ReadWrite
# 4. 建立並部署 Container App,同時掛載路徑
az containerapp create \
--name $APP_NAME \
--resource-group $RESOURCE_GROUP \
--environment $ENVIRONMENT_NAME \
--image $IMAGE_NAME \
--target-port 8080 \
--ingress external \
--min-replicas 1 \
--max-replicas 1 \
--set-env-vars DB_PATH=/app/data \
--storage-mounts target-path=/app/data storage-name=my-sqlite-storage
指令關鍵說明
- –min-replicas 1 與 –max-replicas 1:這非常重要。SQLite 不支援多個容器同時寫入同一個檔案。如果設為多個 Replicas,當兩個容器同時嘗試存取 Azure Files 上的資料庫時,會發生嚴重的資料錯誤或鎖定。
- –storage-mounts:這行指令將你在環境中定義的
my-sqlite-storage對應到容器內部的/app/data。這就是你在 Dockerfile 中預留的路徑。 - 環境變數 DB_PATH:透過指令傳入
/app/data,讓 Node.js 程式知道該去哪裡找.sqlite檔案。
後續維護建議
當你需要更新程式碼時,只需要執行更新映像檔的指令:
Bash
az containerapp update \
--name $APP_NAME \
--resource-group $RESOURCE_GROUP \
--image <新映像檔名稱>
更新過程中,ACA 會嘗試啟動新容器並關閉舊容器。因為 SQLite 檔案被鎖定的特性,更新期間可能會有短暫的幾秒鐘連線失敗(因為新舊容器可能同時在爭奪同一個檔案的控制權),這是使用 SQLite 搭配雲端容器環境的小小代價。
如果你發現 az containerapp env storage set 指令在你的環境中無法運作,通常是因為 Azure CLI 版本過舊,建議先執行 az extension add --name containerapp --upgrade 更新擴充功能。
在本地端驗證 Docker 映像檔(Image)與 SQLite 掛載是否正常,最重要的是模擬 Azure Container Apps 的掛載行為。你可以透過以下步驟進行測試:
使用 Docker Run 模擬掛載環境
在本地端,你可以使用 -v 參數將電腦上的資料夾掛載到容器內,這能確保你的 Node.js 程式可以正確讀寫外部目錄。
Bash
# 1. 在本地建立一個測試用的資料目錄
mkdir -p $(pwd)/test-data
# 2. 執行容器並掛載目錄
# 將本地的 test-data 對應到容器內的 /app/data
docker run -d \
--name node-sqlite-test \
-p 3000:8080 \
-v $(pwd)/test-data:/app/data \
-e DB_PATH=/app/data \
<你的映像檔名稱>
驗證步驟與檢查點
1. 檢查檔案是否產生
執行容器後,檢查你本地的 test-data 目錄。如果程式邏輯正確,你應該會看到目錄下多出了一個 database.sqlite(或你在程式中設定的檔名)。
Bash
ls -l ./test-data
2. 進入容器內部檢查
你可以直接進入執行中的容器,查看路徑與權限是否如預期:
Bash
docker exec -it node-sqlite-test sh
# 在容器內檢查
ls -la /app/data
# 測試手動寫入檔案看是否有權限
touch /app/data/test.txt
3. 驗證資料持久性(最重要的一步)
這是模擬雲端重啟的核心測試:
- 透過 API 或程式寫入一筆資料到 SQLite。
- 刪除並移除目前的容器:
docker rm -f node-sqlite-test。 - 重新執行相同的
docker run指令(保持掛載同一個test-data目錄)。 - 檢查剛才寫入的資料是否還在。如果資料還在,代表持久化儲存設定成功。
常見問題排除
- Permission Denied (權限拒絕):如果容器內出現無法寫入 SQLite 的錯誤,通常是因為本地資料夾的權限與容器內的使用者不符。你可以暫時將本地目錄設為
chmod 777 ./test-data來排除是否為權限問題。 - 路徑不一致:確保你的環境變數
DB_PATH確實傳遞到了 Node.js 程式中。你可以在程式啟動時加上console.log('Database path:', dbPath)來確認。
進階驗證:使用 Docker Compose
如果你覺得指令太長,可以使用 docker-compose.yml 來簡化驗證過程:
YAML
services:
app:
build: .
ports:
- "3000:8080"
environment:
- DB_PATH=/app/data
volumes:
- ./test-data:/app/data
直接執行 docker-compose up 就能快速啟動測試環境。
自動檢查 SQLite 資料表是否建立成功
這是一段簡單的 Node.js 測試腳本,你可以將它命名為 test-db.js。這段腳本會自動檢查資料庫檔案是否存在、能否寫入資料,以及讀取出的內容是否正確。
你可以將這段程式碼放入你的專案,或直接整合進 index.js 的啟動邏輯中。
自動化驗證腳本
JavaScript
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const fs = require('fs');
const dbDir = process.env.DB_PATH || './data';
const dbPath = path.join(dbDir, 'database.sqlite');
console.log('--- 開始資料庫驗證 ---');
console.log(`目標路徑: ${dbPath}`);
// 1. 檢查目錄權限
try {
if (!fs.existsSync(dbDir)) {
fs.mkdirSync(dbDir, { recursive: true });
}
fs.accessSync(dbDir, fs.constants.W_OK);
console.log('目錄檢查: 成功 (可寫入)');
} catch (err) {
console.error('目錄檢查: 失敗', err.message);
process.exit(1);
}
// 2. 測試資料庫操作
const db = new sqlite3.Database(dbPath);
db.serialize(() => {
// 建立測試資料表
db.run("CREATE TABLE IF NOT EXISTS test_log (id INTEGER PRIMARY KEY, ts TEXT)");
// 寫入當前時間
const now = new Date().toISOString();
db.run("INSERT INTO test_log (ts) VALUES (?)", [now], function(err) {
if (err) {
console.error('寫入測試: 失敗', err.message);
process.exit(1);
}
console.log(`寫入測試: 成功 (ID: ${this.lastID})`);
});
// 讀取並驗證
db.all("SELECT * FROM test_log ORDER BY id DESC LIMIT 1", (err, rows) => {
if (err) {
console.error('讀取測試: 失敗', err.message);
process.exit(1);
}
if (rows.length > 0) {
console.log('讀取測試: 成功', rows[0]);
console.log('--- 驗證完成,SQLite 運作正常 ---');
}
db.close();
});
});
如何在 Docker 中執行此測試
你可以修改 Dockerfile 的 CMD,或是在執行 docker run 時覆蓋啟動指令,直接執行這段測試腳本。
方法 A:透過 docker run 覆蓋指令
假設你的 Image 叫 my-node-app:
Bash
docker run --rm \
-v $(pwd)/test-data:/app/data \
-e DB_PATH=/app/data \
my-node-app node test-db.js
方法 B:觀察執行結果
如果腳本輸出「驗證完成,SQLite 運作正常」,請到你電腦的 test-data 資料夾查看:
- 你應該會看到
database.sqlite檔案。 - 如果你重複執行該指令,你會看到
test_log的 ID 持續增加,這代表掛載與持久化機制完全正常。
最終檢查清單
| 檢查項目 | 預期結果 |
| 本地目錄 | 執行後出現 database.sqlite 檔案 |
| 容器日誌 | 出現「寫入測試: 成功」字樣 |
| 容器重啟 | 再次執行後,舊的 ID 資料依然存在 |
| 檔案權限 | 本地檔案的擁有者應與容器執行使用者相容 |
完成本地驗證後,你就可以放心地使用先前提供的 Azure CLI 指令 將映像檔推送到雲端執行。需要我幫你把這段測試邏輯寫進 GitHub Actions 的 CI 流程中嗎?