Docker MySQL 資料存放位置

在 Windows 11 上使用 Docker 執行 MySQL 時,資料庫檔案的實際存放位置取決於你啟動容器的方式。

預設情況下,如果你沒有設定掛載點,Docker 會將資料存放於容器內部的虛擬檔案系統。在 Windows 的 Docker Desktop 環境中,這些資料實際上被封裝在名為 wsl.localhost/docker-desktop-data 的 WSL 2 虛擬磁碟檔案裡。這個檔案通常是一個 vhdx 格式的虛擬硬碟,位於使用者的 AppData 路徑下。這種方式的缺點是,一旦容器被刪除且沒有建立 Volume,資料就會隨之消失,且從 Windows 檔案總管很難直接存取或備份。

為了資料持久化與方便管理,開發者通常會使用兩種掛載方式。第一種是 Docker Volume,這是由 Docker 管理的儲存空間。在 WSL 2 模式下,這些資料會存放在 Linux 分發版的特定目錄內,路徑大約是 /var/lib/docker/volumes/。雖然這比虛擬磁碟容易管理,但依然不是直接存在 Windows 的資料夾中。

第二種是 Bind Mount,這是在執行 docker run 指令時,利用 -v 參數將 Windows 上的實體資料夾與容器內的 /var/lib/mysql 目錄對接。例如你指定 C:\mysql_data 作為掛載點,那麼所有的資料庫檔案、資料表與索引都會直接出現在這個 Windows 資料夾中。這種方式最直觀,你可以直接在 Windows 看到檔案,也方便進行手動備份或遷移。

總結來說,若不特別設定,資料會被藏在 WSL 2 的虛擬硬碟檔中。若有設定掛載,則會出現在你指定的 Windows 路徑或是 Docker 專用的 Linux 磁區內。


PostgreSQL 與 MySQL 在 Docker 上的處理邏輯幾乎完全相同。這兩者都遵循 Docker 的資料持久化機制,主要差異僅在於容器內部儲存資料的預設路徑不同。

當你在 Windows 11 執行 PostgreSQL 容器時,如果不指定掛載路徑,資料同樣會被存放在 WSL 2 的虛擬硬碟檔案中(通常是 vhdx 檔案)。這類資料庫映像檔的設計邏輯一致,都是將資料庫的實際檔案存放於容器內的某個特定目錄。對於 PostgreSQL 來說,這個內部路徑通常是 /var/lib/postgresql/data,而 MySQL 則是 /var/lib/mysql。

如果你希望在 Windows 檔案總管直接看到 PostgreSQL 的資料,處理方式與 MySQL 相同。你需要在執行 docker run 指令時,使用 -v 參數進行掛載。例如你可以將 Windows 上的 C:\pg_data 資料夾對接到容器內的 /var/lib/postgresql/data。這樣一來,無論是 PostgreSQL 產生的資料表檔案還是設定檔,都會直接出現在你指定的 Windows 資料夾中。

這種一致性是 Docker 的核心優勢。無論是 Redis、MongoDB 或 SQL Server 的容器版本,只要涉及到資料儲存,其核心概念都是透過 Volume 或 Bind Mount 將容器內的資料路徑映射到宿主機(也就是你的 Windows 環境)。這樣即使你刪除並重新建立容器,資料依然會保留在你的電腦硬碟上,不會隨著容器消失。


Docker Compose 配置示例

使用 Docker Compose 是管理多個資料庫最方便的方式。你可以在同一個專案資料夾下建立一個名為 docker-compose.yml 的檔案,內容如下:

YAML

services:
  db-mysql:
    image: mysql:latest
    container_name: mysql-container
    environment:
      MYSQL_ROOT_PASSWORD: your_password
    volumes:
      - ./mysql_data:/var/lib/mysql
    ports:
      - "3306:3306"

  db-postgres:
    image: postgres:latest
    container_name: postgres-container
    environment:
      POSTGRES_PASSWORD: your_password
    volumes:
      - ./postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

資料路徑的關鍵差異

雖然處理邏輯相同,但你必須留意容器內部的掛載路徑。MySQL 預設將資料放在 /var/lib/mysql,而 PostgreSQL 則是放在 /var/lib/postgresql/data。在上面的設定中,我使用了相對路徑(例如 ./mysql_data),這表示當你執行啟動指令後,Docker 會在你目前的資料夾下自動建立這兩個子資料夾,並把資料庫檔案寫入其中。

常用管理指令

在 Windows 終端機(PowerShell 或 CMD)進入該資料夾後,你可以使用以下指令:

  • 啟動所有資料庫:docker compose up -d
  • 停止並移除容器(但保留資料):docker compose down
  • 查看容器執行狀態:docker compose ps

這種做法的好處是,所有的資料庫設定與檔案都集中在一個專案目錄內。即使你更換電腦或重灌 Docker,只要把這個資料夾整份備份走,下次在別處執行同樣的指令,資料庫就會原封不動地跑起來。


寫一段帶有掛載參數的 Docker 啟動指令

這裡分別為你準備 MySQL 與 PostgreSQL 的 Docker 啟動指令。這些指令使用了 -v 參數(Bind Mount),將 Windows 實體資料夾對接到容器內部,確保資料持久化。

MySQL 啟動指令

在執行前,請先在 Windows 上建立一個資料夾(例如 C:\docker_db\mysql),然後在 PowerShell 執行:

PowerShell

docker run -d `
  --name mysql-server `
  -e MYSQL_ROOT_PASSWORD=your_password `
  -p 3306:3306 `
  -v C:\docker_db\mysql:/var/lib/mysql `
  mysql:latest

PostgreSQL 啟動指令

同樣地,先建立 Windows 資料夾(例如 C:\docker_db\postgres),再執行:

PowerShell

docker run -d `
  --name postgres-server `
  -e POSTGRES_PASSWORD=your_password `
  -p 5432:5432 `
  -v C:\docker_db\postgres:/var/lib/postgresql/data `
  postgres:latest

指令參數說明

  • -d:讓容器在背景執行,不會佔用目前的終端機視窗。
  • –name:為你的容器取一個好記的名字,方便後續管理。
  • -e:設定環境變數。對於資料庫映像檔,這通常用來設定初始的 root 或管理員密碼。
  • -p:連接埠對接。格式為 主機連接埠:容器內部連接埠。這讓你可以在 Windows 使用資料庫管理軟體(如 DBeaver 或 Navicat)連線至 localhost。
  • -v:掛載參數。這是最關鍵的部分,格式為 Windows路徑:容器內部路徑

注意事項

在 Windows 上使用 Bind Mount 時,Docker 會詢問是否允許存取該資料夾,請務必點選允許。另外,建議掛載的 Windows 資料夾路徑不要包含空格或中文字元,以避免某些舊版本 Docker 出現編碼辨識問題。


將這些指令轉換成可重複使用的 Shell Script 檔案。

好的,這裡提供這兩個資料庫在 PowerShell 上的啟動指令。這些指令使用了 -v 參數,將 Windows 的實體路徑直接對接到容器內部。

MySQL 啟動指令

在執行之前,請先在 Windows 上手動建立一個資料夾(例如 C:\docker_db\mysql),然後執行:

PowerShell

docker run -d `
  --name mysql-server `
  -e MYSQL_ROOT_PASSWORD=your_password `
  -p 3306:3306 `
  -v C:\docker_db\mysql:/var/lib/mysql `
  mysql:latest

PostgreSQL 啟動指令

同樣地,請先建立 Windows 資料夾(例如 C:\docker_db\postgres),再執行:

PowerShell

docker run -d `
  --name postgres-server `
  -e POSTGRES_PASSWORD=your_password `
  -p 5432:5432 `
  -v C:\docker_db\postgres:/var/lib/postgresql/data `
  postgres:latest

指令關鍵說明

-d 代表在背景執行,不會佔住終端機。–name 是給容器取個好記的名字。-e 用來設定資料庫的初始密碼。-p 是把容器的埠號對接到你的 localhost。

最關鍵的是 -v 參數,格式為 Windows實體路徑:容器內部路徑。透過這個設定,資料庫產生的所有實體檔案都會直接存放在你指定的 Windows 資料夾內。即使你之後用 docker rm 刪除容器,只要這個資料夾還在,資料就不會遺失。


SQLite 的處理方式與 MySQL 或 PostgreSQL 有顯著不同,因為 SQLite 本質上只是一個單一的資料庫檔案(例如 data.db),而不是一個需要持續執行背景服務的資料庫引擎。

在 Docker 環境中使用 SQLite 時,通常是將該資料庫檔案掛載到執行應用程式(如 Node.js、Python 或 Go)的容器中。

SQLite 啟動掛載指令

假設你的應用程式程式碼在容器內的 /app 目錄,而你希望將 Windows 上的資料夾 C:\docker_db\sqlite 對接到容器,讓應用程式產生的資料庫檔案直接存在 Windows 硬碟上。你可以使用以下指令:

PowerShell

docker run -d `
  --name my-app-with-sqlite `
  -v C:\docker_db\sqlite:/app/data `
  your-app-image:latest

關鍵差異與特性

MySQL 與 PostgreSQL 的掛載對象是整個資料夾,裡面包含複雜的系統檔案與日誌。而 SQLite 的掛載重點在於確保應用程式讀寫的是你主機上的那個 .db 檔案。

如果你的應用程式直接讀取特定檔案路徑,你甚至可以只掛載單一檔案而非整個資料夾,但通常建議掛載資料夾(如 /app/data),這樣 SQLite 產生的臨時日誌檔案(如 -wal 或 -shm 檔案)也能一併被持久化,避免資料損壞。

持久化優勢

將 SQLite 掛載到 Windows 實體路徑後,即使容器毀損或刪除,你依然可以使用 Windows 上的 SQLite 工具(如 SQLite Browser)直接打開該檔案進行檢視或修改。這對於開發階段的除錯非常有幫助,也讓備份變得極為簡單,只需要複製那個 .db 檔案即可。


寫一段 Node.js 連接掛載路徑下 SQLite 檔案的範例程式碼

這是一段簡單的 Node.js 範例,示範如何連接到掛載路徑下的 SQLite 資料庫檔案。這裡假設你已經按照前面的指令,將 Windows 的 C:\docker_db\sqlite 掛載到容器內的 /app/data 目錄。

Node.js 連接 SQLite 範例

你可以使用 sqlite3better-sqlite3 套件來達成。以下使用較為常見的 sqlite3 作為範例:

JavaScript

const sqlite3 = require('sqlite3').verbose();
const path = require('path');

// 指向容器內掛載的資料夾路徑
const dbPath = path.resolve(__dirname, 'data', 'database.sqlite');

// 建立或開啟資料庫連線
const db = new sqlite3.Database(dbPath, (err) => {
  if (err) {
    console.error('連線失敗:', err.message);
  } else {
    console.log('成功連線至掛載路徑下的 SQLite 資料庫');
  }
});

// 建立一個簡單的資料表測試
db.serialize(() => {
  db.run("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)");
  db.run("INSERT INTO users (name) VALUES ('Test User')");
  
  db.each("SELECT id, name FROM users", (err, row) => {
    console.log(`用戶 ID: ${row.id}, 姓名: ${row.name}`);
  });
});

db.close();

Dockerfile 配合設定

為了確保容器啟動時該目錄存在,你的 Dockerfile 可以這樣寫:

Dockerfile

FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .

# 確保建立資料掛載點目錄
RUN mkdir -p /app/data

CMD ["node", "index.js"]

執行建議

當你執行這個容器時,請務必加上 -v 參數。這樣一來,程式碼中對 /app/data/database.sqlite 的所有讀寫動作,實際上都會反應在 Windows 上的 C:\docker_db\sqlite\database.sqlite 檔案。

這樣做最大的好處是,即使你把 Docker 容器停掉,你仍然可以用 Windows 上的 SQLite 瀏覽器直接打開那個檔案檢查資料是否正確。


完整的 docker-compose.yml 整合版本

這是一份整合了 Node.js 應用程式與 SQLite 掛載設定的 docker-compose.yml 範本。這個配置會將你的程式碼與資料庫檔案完全分離,確保資料持久化。

專案結構建議

在你的專案目錄下,結構應該長這樣:

  • project-root/
    • docker-compose.yml
    • Dockerfile
    • index.js (你的 Node.js 程式碼)
    • package.json
    • data/ (掛載後會在此自動產生資料庫檔案)

docker-compose.yml 完整內容

YAML

version: '3.8'

services:
  app:
    build: .
    container_name: node-sqlite-app
    volumes:
      # 將主機的 ./data 目錄掛載到容器內的 /app/data
      - ./data:/app/data
    environment:
      - NODE_ENV=production
      - DB_PATH=/app/data/database.sqlite
    restart: always

搭配的 Dockerfile 內容

為了配合上述配置,你的 Dockerfile 需要確保工作目錄與權限正確:

Dockerfile

FROM node:18-slim

# 安裝 SQLite3 編譯時需要的基本工具
RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*

WORKDIR /app

COPY package*.json ./
RUN npm install --only=production

COPY . .

# 建立資料存放目錄並確保權限
RUN mkdir -p /app/data

CMD ["node", "index.js"]

啟動與驗證

在終端機執行 docker compose up -d 啟動後,你可以觀察以下幾點:

  1. 實體檔案出現:你會看到專案目錄下的 data/ 資料夾內多出了一個 database.sqlite(或你在程式中定義的檔名)。
  2. 直接讀取:你可以直接用 Windows 上的 SQLite 軟體打開這個 data/database.sqlite 檔案,裡面就是容器內程式寫入的資料。
  3. 環境變數應用:在 Node.js 程式中,建議使用 process.env.DB_PATH 來取得資料庫路徑,這樣之後如果要修改掛載位置,只需要改 compose 檔案即可。

如何透過環境變數在 Node.js 中動態切換開發與生產環境的資料庫路徑

好的,這裡提供一份完整的配置建議,讓你能透過環境變數動態切換本地開發與 Docker 容器內的資料庫路徑。

專案目錄結構

建議的目錄結構如下,這樣可以確保程式碼、設定與資料完全分離:

  • project-root/
    • docker-compose.yml
    • Dockerfile
    • index.js (主程式)
    • package.json
    • .env (環境變數設定檔)
    • data/ (此目錄會自動產生,存放 SQLite 檔案)

1. .env 環境設定

在 Windows 本地開發時,你可能想直接放在目前的 data 資料夾;在 Docker 內,則固定掛載到 /app/data

Plaintext

# 本地開發建議設定
DB_PATH=./data/database.sqlite
PORT=3000

2. index.js 程式碼實作

使用環境變數來決定資料庫位置,增加彈性。

JavaScript

const sqlite3 = require('sqlite3').verbose();
const path = require('path');

// 優先讀取環境變數,若無則預設在當前目錄的 data 資料夾
const dbPath = process.env.DB_PATH || path.join(__dirname, 'data', 'database.sqlite');

console.log(`正在連線至資料庫位置: ${dbPath}`);

const db = new sqlite3.Database(dbPath, (err) => {
  if (err) {
    console.error('資料庫連線錯誤:', err.message);
    return;
  }
  console.log('已成功連接 SQLite。');
});

// 初始化資料表
db.serialize(() => {
  db.run("CREATE TABLE IF NOT EXISTS logs (id INTEGER PRIMARY KEY, msg TEXT, ts DATETIME DEFAULT CURRENT_TIMESTAMP)");
  db.run("INSERT INTO logs (msg) VALUES ('Container started')");
});

// 保持程序執行
setInterval(() => {
  db.all("SELECT * FROM logs ORDER BY ts DESC LIMIT 1", (err, rows) => {
    console.log('最新紀錄:', rows[0]);
  });
}, 5000);

3. docker-compose.yml 整合配置

這份配置會覆蓋 .env 中的路徑,強制容器使用掛載的路徑。

YAML

services:
  sqlite-app:
    build: .
    container_name: node-sqlite-service
    volumes:
      # 將 Windows 當前目錄下的 data 資料夾掛載到容器內的 /app/data
      - ./data:/app/data
    environment:
      # 強制指定容器內的資料庫檔案路徑
      - DB_PATH=/app/data/database.sqlite
    restart: unless-stopped

如何驗證與操作

  1. 啟動服務:在專案根目錄執行 docker compose up -d
  2. 檢查檔案:你會發現 Windows 的 data/ 資料夾出現了 database.sqlite
  3. 直接讀取:你可以直接用 Windows 上的 SQLite 軟體(如 DB Browser for SQLite)打開這個檔案。
  4. 刪除容器測試:執行 docker compose down 移除容器後,再重新 up 啟動,你會發現之前的資料紀錄依然存在,這證明了 Bind Mount 運作正常。

Docker 權限設定的細節

在 Windows 11 上透過 Docker Desktop 使用 WSL 2 模式時,權限處理邏輯與純 Linux 環境有所不同。這主要是因為 Windows 檔案系統(NTFS)與 Linux 檔案系統(Ext4)之間的權限映射機制。

WSL 2 的權限映射機制

當你使用 -vvolumes 將 Windows 資料夾掛載到容器內時,WSL 2 會自動處理權限轉換。預設情況下,掛載進去的檔案在容器內部通常會顯示為 root 使用者所有,且權限通常是 777755。這是為了確保 Windows 上的開發者可以直接編輯檔案,而容器內的服務也能讀寫。

常見的權限問題與解決方案

雖然自動映射很方便,但在某些嚴格檢查權限的資料庫(如 PostgreSQL)或需要特定 User ID (UID) 的應用中,可能會遇到問題。

  • 指定 User 執行:如果你希望容器以特定身分執行,可以在 docker-compose.yml 中加入 user: "1000:1000"。但在 Windows 掛載目錄下,這有時會導致無法寫入,因為 Windows 檔案系統不完全支援 Linux 的 UID/GID 概念。
  • 自動建立目錄的權限:如果掛載的 Windows 資料夾尚不存在,Docker 啟動時會以 root 權限自動建立該目錄。這可能導致你在 Windows 上手動刪除該目錄時需要管理員權限。建議先在 Windows 手動建立好資料夾,再啟動 Docker。
  • chmod 與 chown 的限制:在 Bind Mount(掛載 Windows 路徑)的情況下,你在容器內執行 chmodchown 通常不會產生實質影響,因為底層是 NTFS 檔案系統。如果你需要嚴格的 Linux 權限控制,建議使用 Named Volumes(由 Docker 管理的空間),而不是直接掛載 Windows 路徑。

實務建議:Named Volumes vs Bind Mounts

如果你對權限有嚴格要求,或者發現資料庫因為權限問題無法啟動,可以考慮切換成 Named Volumes:

YAML

services:
  db:
    image: postgres:latest
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata: # 這會由 Docker 在 WSL 2 內部管理,支援完整的 Linux 權限

Windows 檔案共享設定

在較舊版本的 Docker Desktop 中,你需要在設定中手動勾選 File Sharing 來允許存取特定的 C 槽或 D 槽路徑。在目前的 WSL 2 模式下,這通常是自動開啟的,但如果遇到 Permission Denied,請檢查 Docker Desktop 設定中的 Resources > File Sharing 是否涵蓋了你的專案路徑。

總結來說,在 Windows 上開發時,Bind Mount(掛載實體路徑)最適合開發除錯,因為方便直接看檔案;而 Named Volume 最適合生產環境或對權限敏感的資料庫,因為效能更好且權限穩定。


調整之前的 docker-compose.yml 改用 Named Volumes 方式

這是一份將 SQLite 轉換為 Named Volume 方式的 docker-compose.yml 配置。

Named Volume 與之前的 Bind Mount(直接掛載 Windows 資料夾)最大的不同在於,資料會交由 Docker 在 WSL 2 的 Linux 檔案系統中管理,這能提供完整的 Linux 權限支援與更好的讀寫效能。

改用 Named Volume 的 docker-compose.yml

YAML

services:
  sqlite-app:
    build: .
    container_name: node-sqlite-service
    volumes:
      # 使用具名磁碟卷掛載到容器內的 /app/data
      - sqlite_storage:/app/data
    environment:
      - DB_PATH=/app/data/database.sqlite
    restart: unless-stopped

volumes:
  # 定義具名磁碟卷,由 Docker 統一管理
  sqlite_storage:

Named Volume 的特性與權限優點

使用 Named Volume 處理權限時,有以下幾個關鍵細節:

  1. 自動初始化權限:當你啟動容器時,Docker 會自動將映像檔中 /app/data 的權限與內容複製到 sqlite_storage 中。這解決了在 Windows 掛載時常遇到的 Permission Denied 問題,因為資料是在 Linux 原生檔案系統(Ext4)中運作。
  2. 效能優勢:在 Windows 11 上,Bind Mount 需要跨越 Windows 與 Linux 檔案系統的轉換層,讀寫頻繁時效能較差。Named Volume 直接跑在 WSL 2 內部,這對於需要頻繁寫入日誌或進行大量查詢的 SQLite 來說,速度會明顯提升。
  3. 資料存放位置:這類資料不再直接顯示在你的 Windows 專案資料夾中。在 Windows 11 的 Docker Desktop 環境下,這些資料實體存放在:\\wsl$\docker-desktop-data\data\docker\volumes\sqlite_storage\_data

如何在 Windows 存取 Named Volume 的資料?

雖然它不像 Bind Mount 那樣直觀,但你依然可以透過以下方式存取:

  • 透過 WSL 路徑:在檔案總管網址列輸入 \\wsl$\docker-desktop-data 即可進入查看(具體路徑可能因版本略有不同)。
  • 使用 Docker 指令備份:你可以透過指令將資料從 Volume 導出來:docker cp node-sqlite-service:/app/data/database.sqlite ./backup.sqlite

總結建議

  • 開發階段:建議使用 Bind Mount (- ./data:/app/data),因為你可以直接在 Windows 用 GUI 工具開啟資料庫進行除錯。
  • 測試或正式環境:建議使用 Named Volume (- sqlite_storage:/app/data),權限最穩定,效能也最優。

寫一段自動化備份 Named Volume 資料到 Windows 指定路徑的指令碼

既然決定改用 Named Volume,這裡提供一段簡單的 PowerShell 指令碼,讓你可以隨時把容器內部的 SQLite 檔案「撈」出來備份到 Windows 指定路徑。

1. 自動化備份指令 (PowerShell)

你可以將這段程式碼存成 backup_db.ps1,或是直接在終端機執行:

PowerShell

# 設定變數
$CONTAINER_NAME = "node-sqlite-service"
$DB_SOURCE_PATH = "/app/data/database.sqlite"
$BACKUP_DEST_PATH = "C:\backups\sqlite\database_$(Get-Date -Format 'yyyyMMdd_HHmm').sqlite"

# 建立備份資料夾(如果不存在)
New-Item -ItemType Directory -Force -Path (Split-Path $BACKUP_DEST_PATH)

# 從容器複製檔案到 Windows
docker cp "${CONTAINER_NAME}:${DB_SOURCE_PATH}" "$BACKUP_DEST_PATH"

Write-Host "備份完成!檔案路徑:$BACKUP_DEST_PATH" -ForegroundColor Green

2. 為何 Named Volume 需要這段備份?

在之前的 Bind Mount 模式下,你的檔案就在專案資料夾裡,備份很直觀。但在 Named Volume 模式下,資料被鎖在 Docker 的虛擬磁碟中。

使用 docker cp 的好處是:

  • 不干擾執行:你不需要停止容器就能進行快照備份。
  • 路徑彈性:你可以把檔案丟到任何 Windows 支援的路徑(如雲端同步資料夾或 NAS)。
  • 權限隔離:備份出來的檔案會自動轉為 Windows 目前使用者的權限,方便你用 GUI 工具開啟。

3. 進階:從備份還原

如果哪天資料壞了,想把備份推回去,指令也異曲同工:

PowerShell

# 停止容器以確保資料一致性
docker stop node-sqlite-service

# 把 Windows 上的備份檔覆蓋回容器內的路徑
docker cp "C:\backups\sqlite\database_backup.sqlite" "node-sqlite-service:/app/data/database.sqlite"

# 重新啟動
docker start node-sqlite-service

4. 權限設定的最後提醒

在 Windows 11 上使用 Named Volume 時,雖然不需要處理 Linux 的 chownchmod,但要注意:

  • 多容器共享:如果你有多個容器同時讀寫同一個 Named Volume,請確保你的應用程式(如 Node.js)有處理好檔案鎖定(File Locking),特別是 SQLite 在多程序讀寫時容易發生 SQLITE_BUSY 錯誤。

備份邏輯直接整合進 docker-compose.yml 的指令

要在 docker-compose.yml 中直接整合備份邏輯,最優雅的做法是利用 Docker 的 profiles 功能或是建立一個專門負責備份的輔助服務。

這樣你不需要額外寫 PowerShell 腳本,直接透過 docker compose 指令就能觸發備份任務。

1. 修改後的 docker-compose.yml

我在原本的配置中增加了一個 backup 服務,並設定為 manual 模式(透過 profiles 達成),這樣它平時不會自動啟動,只有在你下指令時才會執行。

YAML

services:
  # 主應用程式
  sqlite-app:
    build: .
    container_name: node-sqlite-service
    volumes:
      - sqlite_storage:/app/data
    environment:
      - DB_PATH=/app/data/database.sqlite
    restart: unless-stopped

  # 備份輔助服務
  backup:
    image: alpine # 使用輕量級的 Linux 映像檔
    profiles: ["tools"] # 設定 profile,讓它平時不自動啟動
    volumes:
      - sqlite_storage:/source:ro # 以唯讀模式掛載資料來源
      - ./backups:/dest          # 掛載主機的備份資料夾
    entrypoint: >
      sh -c "cp /source/database.sqlite /dest/database_$(date +%Y%m%d_%H%M%S).sqlite && echo '備份成功!'"

volumes:
  sqlite_storage:

2. 如何執行備份

當你想備份時,只需要在專案目錄下輸入這行指令:

PowerShell

docker compose run --rm backup

3. 邏輯說明與優點

  • Profiles 隔離:使用 profiles: ["tools"] 可以確保你平常執行 docker compose up -d 時,備份服務不會被啟動,避免佔用資源。
  • 唯讀掛載 (ro):備份服務對 sqlite_storage 採用 :ro (read-only) 唯讀掛載,這能保證備份過程不會意外更動到正在運行的資料庫。
  • 自動清理:使用 --rm 參數代表備份任務執行完畢後,Docker 會自動刪除這個臨時的備份容器,保持系統乾淨。
  • 跨平台通用:這套邏輯寫在 YAML 裡,無論是在 Windows、Mac 或 Linux 上執行,指令與行為完全一致。

4. 權限細節提醒

由於 backup 服務使用的是 Alpine Linux (root 權限),它在 Windows 的 ./backups 資料夾建立檔案時,通常不會有權限問題。但如果你發現檔案無法刪除,請確認該資料夾在 Windows 上的安全性設定。


備份任務能定時自動執行(例如每小時一次)

如果要將備份邏輯直接整合進 docker-compose.yml 並實現定時自動執行,最常見的做法是在 Compose 中加入一個輕量級的 Cron 服務。這個服務會持續在背景執行,並根據你設定的時間表(例如每小時或每天)自動執行備份。

1. 整合定時備份的 docker-compose.yml

這裡使用 alpine 映像檔內建的 crond 功能。你可以直接將以下內容蓋掉原本的檔案:

YAML

services:
  # 主應用程式
  sqlite-app:
    build: .
    container_name: node-sqlite-service
    volumes:
      - sqlite_storage:/app/data
    environment:
      - DB_PATH=/app/data/database.sqlite
    restart: unless-stopped

  # 定時備份服務
  backup-cron:
    image: alpine
    container_name: sqlite-backup-cron
    volumes:
      - sqlite_storage:/source:ro
      - ./backups:/dest
    # 設定 Cron 排程 (例如每小時執行一次)
    # 格式:分 時 日 月 週 指令
    entrypoint: >
      sh -c "echo '0 * * * * cp /source/database.sqlite /dest/database_$$(date +\%Y\%m\%d_\%H\%M\%S).sqlite' > /etc/periodic/hourly/backup && chmod +x /etc/periodic/hourly/backup && crond -f -l 2"
    restart: unless-stopped

volumes:
  sqlite_storage:

2. 指令與權限細節說明

  • 雙錢字號 ($$):在 docker-compose.yml 中,如果你要在指令中使用變數(如 date),必須使用兩個 $ 符號,否則 Compose 會誤以為那是主機的環境變數。
  • crond -f -l 2:這會讓 Cron 服務在前台執行(防止容器結束),並將日誌輸出等級設為 2,方便你透過 docker logs 觀察備份是否成功。
  • 自動清理舊備份:如果你擔心硬碟被塞滿,可以修改 entrypoint 指令,加入刪除 7 天前舊檔的邏輯:find /dest -name "*.sqlite" -mtime +7 -exec rm {} \;

3. Windows 上的實務操作

當你執行 docker compose up -d 後,這個備份服務就會安靜地在背景運作。

  • 檢查備份日誌:執行 docker logs -f sqlite-backup-cron 即可看到它是否有準時觸發。
  • 手動觸發測試:如果你不想等一小時,可以直接進容器執行一次備份指令:docker exec sqlite-backup-cron sh -c "cp /source/database.sqlite /dest/manual_test.sqlite"

4. 權限與檔案鎖定補充

雖然是在 Windows 11 的 Docker Desktop 環境,但因為備份服務使用的是 Alpine (Linux),它在寫入 ./backups 時會遵循 Linux 權限。在 Windows 檔案總管看這些檔案時,通常不會有問題。

唯一要注意的是,SQLite 在備份瞬間如果剛好有極大量的寫入動作,雖然 cp 指令通常能運作,但最保險的做法是讓 Node.js 應用程式在閒置時才進行大規模變動。

這套自動化備份方案讓你即使使用 Named Volume,也能享有跟 Bind Mount 一樣的資料可視性與安全性。


把「自動刪除舊檔」的完整邏輯寫成一個獨立的 shell 腳本放在容器裡

為了讓邏輯更清晰且易於維護,建議將備份與清理舊檔的邏輯寫成一個獨立的 backup.sh 腳本,並透過 Dockerfile 放入容器中。這樣 docker-compose.yml 會變得非常乾淨,且執行時更穩定。

1. 撰寫備份與清理腳本 (backup.sh)

在你的專案目錄下建立 backup.sh,內容如下:

Bash

#!/bin/sh

# 設定變數
SOURCE="/source/database.sqlite"
DEST_DIR="/dest"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$DEST_DIR/database_$TIMESTAMP.sqlite"

# 1. 執行備份
if [ -f "$SOURCE" ]; then
    cp "$SOURCE" "$BACKUP_FILE"
    echo "[$TIMESTAMP] 備份成功: $BACKUP_FILE"
else
    echo "[$TIMESTAMP] 錯誤: 找不到來源檔案 $SOURCE"
    exit 1
fi

# 2. 刪除超過 7 天的舊檔 (可自行調整天數)
echo "正在檢查並清理 7 天前的舊備份..."
find "$DEST_DIR" -name "database_*.sqlite" -mtime +7 -exec rm {} \; -print

echo "清理完成。"

2. 修改 Dockerfile 納入腳本

你需要確保這個腳本被複製進去,並且擁有執行權限。

Dockerfile

FROM alpine

# 安裝必要工具(如果需要的話,alpine 內建有 find 與 cp)
RUN apk add --no-cache tzdata
ENV TZ=Asia/Taipei

WORKDIR /scripts
COPY backup.sh .
RUN chmod +x /scripts/backup.sh

# 啟動 crond 並設定排程
RUN echo "0 * * * * /scripts/backup.sh" > /etc/periodic/hourly/backup
RUN chmod +x /etc/periodic/hourly/backup

CMD ["crond", "-f", "-l", "2"]

3. 精簡後的 docker-compose.yml

現在 docker-compose.yml 不需要寫一長串指令,只需定義掛載點即可。

YAML

services:
  sqlite-app:
    build: .
    container_name: node-sqlite-service
    volumes:
      - sqlite_storage:/app/data
    environment:
      - DB_PATH=/app/data/database.sqlite
    restart: unless-stopped

  backup-cron:
    build:
      context: .
      dockerfile: Dockerfile.backup # 建議將備份用的 Dockerfile 另取名
    container_name: sqlite-backup-cron
    volumes:
      - sqlite_storage:/source:ro
      - ./backups:/dest
    restart: unless-stopped

volumes:
  sqlite_storage:

運作原理與優勢

這套架構將「備份」與「清理」邏輯封裝在腳本中,有以下優點:

  • 自動維護空間find ... -mtime +7 -exec rm {} \; 會自動掃描 /dest 資料夾,刪除修改時間超過 7 天的 .sqlite 檔案。這能防止你的 Windows 硬碟空間被無限增加的備份檔塞滿。
  • 時區正確:在 Dockerfile 中設定了 TZ=Asia/Taipei,確保檔名上的時間戳記與台灣時間一致,方便你對照。
  • 手動測試更簡單:如果你想立刻執行一次,只需輸入 docker exec sqlite-backup-cron /scripts/backup.sh 即可,不需要重啟容器。

目前的設定是每小時備份一次並保留七天,這對多數開發需求已經非常足夠。


把這套邏輯也應用到 MySQL 或 PostgreSQL 的環境

將這套「自動備份 + 舊檔清理」邏輯應用到 MySQL 或 PostgreSQL 時,最大的差別在於不能直接使用 cp 複製檔案。因為這些資料庫在運作時,檔案處於鎖定或寫入狀態,直接複製可能會導致資料損毀。

我們必須改用資料庫專用的傾印工具(如 mysqldumppg_dump),這能確保備份資料的一致性。

1. MySQL 自動備份腳本 (mysql_backup.sh)

這個腳本會連線到 MySQL 容器執行備份,並自動刪除超過 7 天的舊檔。

Bash

#!/bin/sh

# 設定變數
DB_HOST="db-mysql" # 對應 docker-compose 中的服務名稱
DB_USER="root"
DB_PASS="your_password"
DEST_DIR="/dest"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$DEST_DIR/mysql_backup_$TIMESTAMP.sql"

# 1. 執行 mysqldump (使用 -h 指定主機)
mysqldump -h $DB_HOST -u$DB_USER -p$DB_PASS --all-databases > "$BACKUP_FILE"

if [ $? -eq 0 ]; then
    echo "[$TIMESTAMP] MySQL 備份成功: $BACKUP_FILE"
    # 壓縮檔案以節省空間
    gzip "$BACKUP_FILE"
else
    echo "[$TIMESTAMP] MySQL 備份失敗"
    exit 1
fi

# 2. 清理 7 天前的舊檔
find "$DEST_DIR" -name "mysql_backup_*.sql.gz" -mtime +7 -exec rm {} \;

2. PostgreSQL 自動備份腳本 (pg_backup.sh)

PostgreSQL 的邏輯類似,但使用的工具不同。

Bash

#!/bin/sh

# 設定變數
DB_HOST="db-postgres"
DB_USER="postgres"
export PGPASSWORD="your_password"
DEST_DIR="/dest"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$DEST_DIR/pg_backup_$TIMESTAMP.sql"

# 1. 執行 pg_dumpall
pg_dumpall -h $DB_HOST -U $DB_USER > "$BACKUP_FILE"

if [ $? -eq 0 ]; then
    echo "[$TIMESTAMP] PostgreSQL 備份成功: $BACKUP_FILE"
    gzip "$BACKUP_FILE"
else
    echo "[$TIMESTAMP] PostgreSQL 備份失敗"
    exit 1
fi

# 2. 清理 7 天前的舊檔
find "$DEST_DIR" -name "pg_backup_*.sql.gz" -mtime +7 -exec rm {} \;

3. 整合的 docker-compose.yml 結構

在 Compose 中,備份容器需要與資料庫容器處於同一個網路(network),這樣才能透過服務名稱連線。

YAML

services:
  db-mysql:
    image: mysql:latest
    environment:
      MYSQL_ROOT_PASSWORD: your_password
    volumes:
      - mysql_data:/var/lib/mysql

  backup-service:
    build: 
      context: .
      dockerfile: Dockerfile.backup
    volumes:
      - ./backups:/dest
    depends_on:
      - db-mysql
    restart: unless-stopped

volumes:
  mysql_data:

關鍵差異與注意事項

  • 映像檔選擇:備份容器的 Dockerfile 需要安裝對應的用戶端工具。例如備份 MySQL 就需要 apk add mysql-client,備份 PostgreSQL 則需要 apk add postgresql-client
  • 資料一致性:使用 mysqldumppg_dumpall 會在備份時建立一致性的快照,這比 SQLite 直接 cp 檔案安全得多,也不會影響資料庫運行。
  • 網路連線:在 Docker Compose 中,備份腳本的 DB_HOST 必須設定為資料庫的 service 名稱(例如 db-mysql),Docker 會自動處理內部 IP 的轉向。
  • 空間管理:由於 SQL 傾印檔通常比 SQLite 檔案大很多,腳本中加入了 gzip 壓縮指令,這能顯著減少備份佔用的磁碟空間。

包含用戶端工具安裝指令的完整 Dockerfile.backup

這裡提供一個完整的 Dockerfile.backup 範例,它會根據你的需求安裝 MySQL 與 PostgreSQL 的用戶端工具,並設定好排程執行環境。

1. 完整的 Dockerfile.backup

這個 Dockerfile 使用 Alpine 作為基底,體積小且安全。它會安裝必要的資料庫工具、時區設定,並將備份腳本排入 Cron。

Dockerfile

FROM alpine:3.18

# 安裝 MySQL, PostgreSQL 用戶端工具與時區套件
RUN apk add --no-cache \
    mysql-client \
    postgresql-client \
    tzdata \
    gzip

# 設定台灣時區
ENV TZ=Asia/Taipei

WORKDIR /scripts

# 複製備份腳本 (請確保 backup.sh 與 Dockerfile 在同一目錄)
COPY backup.sh .
RUN chmod +x /scripts/backup.sh

# 建立備份存放目錄
RUN mkdir -p /dest

# 設定 Cron 排程:每小時執行一次 (0 * * * *)
# 將指令寫入 Alpine 的 periodic 目錄
RUN echo "0 * * * * /scripts/backup.sh > /proc/1/fd/1 2>&1" > /etc/periodic/hourly/db-backup
RUN chmod +x /etc/periodic/hourly/db-backup

# 啟動 crond 並保持在前台執行,日誌輸出至標準輸出
CMD ["crond", "-f", "-l", "2"]

2. 整合型的備份腳本 (backup.sh)

你可以將 MySQL 與 PostgreSQL 的備份邏輯寫在一起,或者根據你的資料庫類型選擇性使用。

Bash

#!/bin/sh

TIMESTAMP=$(date +%Y%m%d_%H%M%S)
DEST_DIR="/dest"

# --- MySQL 備份邏輯 (如有使用) ---
if [ ! -z "$MYSQL_HOST" ]; then
    echo "開始備份 MySQL..."
    mysqldump -h "$MYSQL_HOST" -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" --all-databases | gzip > "$DEST_DIR/mysql_$TIMESTAMP.sql.gz"
fi

# --- PostgreSQL 備份邏輯 (如有使用) ---
if [ ! -z "$PGHOST" ]; then
    echo "開始備份 PostgreSQL..."
    export PGPASSWORD="$PGPASSWORD"
    pg_dumpall -h "$PGHOST" -U "$PGUSER" | gzip > "$DEST_DIR/pg_$TIMESTAMP.sql.gz"
fi

# --- 清理 7 天前的舊檔 ---
echo "清理舊備份檔案..."
find "$DEST_DIR" -name "*.sql.gz" -mtime +7 -exec rm {} \;

echo "[$TIMESTAMP] 備份與清理任務完成。"

3. 如何在 docker-compose.yml 中配置

在 Compose 檔案中,你只需要透過環境變數傳入連線資訊即可。

YAML

services:
  db-mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: root_password
    volumes:
      - mysql_data:/var/lib/mysql

  backup-service:
    build:
      context: .
      dockerfile: Dockerfile.backup
    environment:
      - MYSQL_HOST=db-mysql
      - MYSQL_USER=root
      - MYSQL_PASSWORD=root_password
      # 如果有 PG 也可以一併加入
      # - PGHOST=db-postgres
      # - PGUSER=postgres
      # - PGPASSWORD=pg_password
    volumes:
      - ./backups:/dest
    depends_on:
      - db-mysql
    restart: unless-stopped

volumes:
  mysql_data:

關鍵細節與運作方式

  • 環境變數驅動:腳本內使用 if [ ! -z "$VARIABLE" ] 來判斷。這表示你只要在 Compose 裡有設定該資料庫的環境變數,它就會自動執行該類型的備份。
  • 日誌觀察:在 crond 指令中加入了 > /proc/1/fd/1,這能讓你在 Windows 終端機執行 docker logs -f backup-service 時,直接看到 Cron 執行的輸出結果。
  • 安全性:使用 gzip 壓縮後的 SQL 檔通常只有原始大小的 10% 到 20%,對於每天多次備份的場景非常節省空間。

寫一段自動恢復(Restore)的指令範例,以備不時之需

這裡提供一套針對 MySQL 與 PostgreSQL 的自動恢復(Restore)指令範例。當你需要從備份檔案還原資料時,可以使用這些指令將 .sql.gz 檔案重新匯入容器中。

1. MySQL 恢復指令

假設你的備份檔案名為 mysql_20260322_220000.sql.gz,且存放在 Windows 的 backups 資料夾內。

PowerShell

# 1. 解壓縮備份檔(在 Windows 或容器內皆可,這裡示範直接透過管道匯入)
# 2. 使用 docker exec 將資料流導回 mysql 容器
zcat .\backups\mysql_20260322_220000.sql.gz | docker exec -i db-mysql /usr/bin/mysql -u root -p'your_password'

2. PostgreSQL 恢復指令

PostgreSQL 的還原邏輯相似,建議使用 psql 工具來執行匯入。

PowerShell

# 1. 使用 zcat 解壓並透過管道傳送
# 2. 使用 docker exec 執行 psql
zcat .\backups\pg_20260322_220000.sql.gz | docker exec -i db-postgres psql -U postgres

3. SQLite 恢復指令

SQLite 最簡單,因為它只是個檔案。你只需要停止容器,覆蓋檔案後重啟即可。

PowerShell

# 1. 停止應用程式容器
docker stop node-sqlite-service

# 2. 將備份檔複製並覆蓋到掛載的路徑(假設你使用 Named Volume,可用 docker cp)
docker cp .\backups\database_20260322_220000.sqlite node-sqlite-service:/app/data/database.sqlite

# 3. 重新啟動容器
docker start node-sqlite-service

關鍵注意事項

  • 資料覆蓋風險:恢復指令會根據 SQL 檔內的指令執行(通常包含 DROP TABLEINSERT),這會覆蓋目前的資料。執行前建議先對目前的資料庫手動執行一次備份。
  • 連線參數:在 docker exec 指令中,-i (interactive) 是必須的,這樣才能讓解壓縮後的資料流順利進入容器的標準輸入。
  • 版本相容性:還原時,建議目標容器的資料庫版本與備份時的版本一致(例如都是 MySQL 8.0),以避免語法不相容的問題。

總結

現在你已經擁有一套完整的 Docker 資料庫管理方案:

  1. 執行環境:透過 docker-compose.yml 啟動。
  2. 資料持久化:使用 Named VolumeBind Mount
  3. 自動備份:透過 backup-service 每小時定時產出壓縮檔。
  4. 空間維護:自動清理 7 天前的舊備份。
  5. 災難復原:透過 zcatdocker exec 快速還原。

這套架構不論是在你目前的 Windows 11 開發環境,或是未來遷移到雲端 VPS,都能直接延用。


把這些恢復指令也寫成一個簡單的 restore.sh 腳本放入工具箱

為了方便你隨時執行還原任務,我們將「解壓與匯入」的邏輯寫成 restore.sh 腳本。這份腳本會列出所有備份檔案供你選擇,讓你不用手動輸入冗長的檔名。

1. 撰寫還原腳本 (restore.sh)

這個腳本會讀取 /dest 目錄下的所有壓縮檔,讓你透過編號選擇要還原哪一份。

Bash

#!/bin/sh

DEST_DIR="/dest"

echo "=== 資料庫還原工具 ==="
echo "可用備份清單:"
ls -1 $DEST_DIR/*.sql.gz | nl

echo -n "請輸入要還原的備份編號: "
read num

# 取得選定的檔案路徑
SELECTED_FILE=$(ls -1 $DEST_DIR/*.sql.gz | sed -n "${num}p")

if [ -z "$SELECTED_FILE" ]; then
    echo "錯誤:無效的編號"
    exit 1
fi

echo "正在準備還原檔案: $SELECTED_FILE"

# 判斷是 MySQL 還是 PostgreSQL (根據檔名關鍵字)
case "$SELECTED_FILE" in
    *mysql*)
        echo "偵測為 MySQL 備份,正在還原至 $MYSQL_HOST..."
        zcat "$SELECTED_FILE" | mysql -h "$MYSQL_HOST" -u"$MYSQL_USER" -p"$MYSQL_PASSWORD"
        ;;
    *pg*)
        echo "偵測為 PostgreSQL 備份,正在還原至 $PGHOST..."
        export PGPASSWORD="$PGPASSWORD"
        zcat "$SELECTED_FILE" | psql -h "$PGHOST" -U "$PGUSER"
        ;;
    *)
        echo "錯誤:無法辨識的備份格式"
        exit 1
        ;;
esac

if [ $? -eq 0 ]; then
    echo "還原成功!"
else
    echo "還原過程中發生錯誤。"
fi

2. 更新 Dockerfile.backup

將還原腳本也放入備份容器中,這樣你的備份工具箱就完整了。

Dockerfile

# 在原本的 Dockerfile.backup 加入這行
COPY restore.sh .
RUN chmod +x /scripts/restore.sh

3. 如何執行還原

當你需要還原資料時,直接進入該備份容器執行腳本即可:

PowerShell

docker exec -it sqlite-backup-cron /scripts/restore.sh

運作邏輯說明

這套工具的核心優勢在於環境一致性。因為 restore.sh 跑在與 backup.sh 相同的容器內,它擁有完整的資料庫用戶端工具(mysql-client, postgresql-client),且與資料庫服務處於同一個 Docker 網路。

這代表你不需要在自己的 Windows 主機安裝任何資料庫軟體,就能完成備份與還原的所有維護工作。

總結建議

  • 安全性:這套腳本會直接覆蓋目標資料庫的現有資料,執行前請務必確認選定的備份檔日期。
  • SQLite 提醒:由於 SQLite 是直接操作實體檔案,還原時建議維持手動覆蓋檔案的方式(如前述的 docker cp),這樣最穩定。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *