2025/12/27

Setting Up Trusted SSL for Laravel Sail and Docker Environments

平常在本機開發時對應的通常是 http,但如果某些特殊原因,想要使用 https 的話,沒有憑證會看到令人礙眼的警告內容,今天紀錄一下怎麼可以在本地使用自簽憑證讓討厭的 ssl 警告消失,有兩種我目前開發的常用情境:

  1. 用 docker 自架 php + nginx
  2. 使用 laravel sail

安裝使用 mkcert

1. 安裝 mkcert

我們使用 Scoop 作為 Windows 的套件管理工具,打開 PowerShell,執行以下指令:

scoop install mkcert

2. 建立本地 CA (憑證授權單位)

這一步最為關鍵,它會讓你的 Windows 系統信任由 mkcert 簽發的憑證:

mkcert -install

注意:若彈出安全性警告,請點擊「是」。

套用自簽憑證

自架範例

1. 專案結構

├── docker/
│   ├── conf.d/
│   │   └── default.conf  # Nginx 設定
│   └── ssl/              # 存放 mkcert 憑證
├── public/
│   └── index.php             # 測試檔案
└── compose.yml           # Docker 配置

2. 產生憑證

# 建立目錄
mkdir -p docker/nginx/ssl

# 使用 mkcert 生成針對 demo.test 的憑證
# 檔案會直接產生在 docker/nginx/ssl 內
cd docker/nginx/ssl
mkcert demo.test

3. default.conf 配置

server {
    listen 80;
    server_name demo.test;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl;
    server_name demo.test;

    ssl_certificate /etc/nginx/ssl/demo.test.pem;
    ssl_certificate_key /etc/nginx/ssl/demo.test-key.pem;

    root /var/www/html/public;
    index index.php;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass php:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}

4. compose.yml

services:
  php:
    image: php:8.4-fpm
    volumes:
      - .:/var/www/html

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - .:/var/www/html
      - ./docker/conf.d:/etc/nginx/conf.d
      - ./docker/nginx/ssl:/etc/nginx/ssl
    depends_on:
      - php

sail 範例

1. 生成專屬憑證

切換到你的 Laravel 專案根目錄,建立存放憑證的資料夾並產生檔案:

mkdir -p docker/ssl
cd docker/ssl
mkcert demo.test

2. 配置 Docker 環境

我們將使用 Caddy 作為反向代理伺服器。
修改 docker-compose.yml
找到 services 區塊,加入 caddy 服務,並調整 laravel.test 的埠號對應:

services:
    laravel.test:
        # ... (其餘配置保持不變)
        ports:
            - '${VITE_PORT:-5173}:${VITE_PORT:-5173}' # 移除 80 埠映射,改由 Caddy 處理
        networks:
            - sail

    caddy:
        image: caddy:latest
        restart: unless-stopped
        ports:
            - "80:80"
            - "443:443"
        volumes:
            - ./Caddyfile:/etc/caddy/Caddyfile
            - ./docker/ssl:/etc/caddy/certs
            - sail-caddy-data:/data
            - sail-caddy-config:/config
        networks:
            - sail
        depends_on:
            - laravel.test

# 記得在最下方的 volumes 區塊新增
volumes:
    sail-caddy-data:
        driver: local
    sail-caddy-config:
        driver: local

3. 建立 Caddyfile

在專案根目錄建立 Caddyfile

demo.test {
    reverse_proxy laravel.test:80
    tls /etc/caddy/certs/demo.test.pem /etc/caddy/certs/demo.test-key.pem
}

4. 調整應用程式設定

1. 更新 .env

確保 Laravel 知道目前的通訊協定:

APP_URL=https://demo.test
2. 讓 Laravel 信任 Proxy

修改 bootstrap/app.php,讓 route() 函式能正確生成 https 連結:

->withMiddleware(function (Middleware $middleware) {
    $middleware->trustProxies(at: '*');
})
3. 配置 Vite (若有使用)

修改 vite.config.js 以支援 HMR (熱重載) 透過 HTTPS 運作:

import fs from 'fs';

export default defineConfig({
    server: {
        host: '0.0.0.0',
        hmr: { host: 'demo.test' },
        https: {
            key: fs.readFileSync('./docker/ssl/demo.test-key.pem'),
            cert: fs.readFileSync('./docker/ssl/demo.test.pem'),
        },
    },
    // ...
});

2025/12/23

How Github Copilot Works

身為一個重度 GitHub Copilot 使用者,一直很好奇它在 IDE 背後是如何運作的,因此整理了公開資料與工程角度的理解,嘗試還原 Copilot 在你按下 Enter 鍵後所經歷的完整流程。

本文內容結合 GitHub 官方文件、公開技術文章,以及工程上合理的系統設計推論。

1. 核心 Pipeline:當你按下 Enter 後的生命週期

每一則 Prompt 或每一行自動補全的背後,大致可拆解為以下六個關鍵階段。

第一步:Context Retrieval(上下文檢索)

  • 責任方: IDE 插件 (Client-side)
  • 動作: 啟動 Prompt Library 掃描
  • Neighboring Tabs: 透過「向量嵌入語義搜尋 (Embedding-based Semantic Search)」,在目前開啟的標籤頁中檢索語義相關的程式碼片段,GitHub 在 2025 年部署的新 embedding model 提升了 37.6% 的檢索品質,吞吐量提高 2 倍,且記憶體使用量減少 8 倍
  • LSP Data: 透過 Language Server Protocol 獲取符號定義 (Definitions)、類型資訊與作用域。
  • Path Context: 考慮當前檔案在專案目錄中的位置。
  • Git Context: 讀取 Git 歷史資訊,包括:
    • Recent Commits: 最近的 commit 訊息,理解程式碼變更意圖
    • Commit Diff: 查看哪些檔案經常一起修改,推斷相關性
    • Blame Information: 了解程式碼區塊的作者和修改時間
    • Branch Context: 當前分支名稱(如 feature/auth)可幫助 Copilot 理解開發目標

第二步:Ranking & Pruning(排序與剪裁)

  • 責任方: Local Indexer / GitHub Proxy

  • 動作: 為避免超過模型可處理的 Context Window,對蒐集到的內容進行篩選與壓縮

  • Context Window 考量:
    不同底層模型在理論上支援不同大小的 context window,但 Copilot 實際送入的內容會依任務型態與效能需求動態調整,而非單純使用最大值

  • 優先權排序(推測性設計):

    • 匯入關係(Imports)
    • 最近編輯或鄰近游標的程式碼
    • 命名或結構相似度
  • 結構化摘要(可視為 Outline 模式):
    當相關檔案過大時,系統會傾向:

    • 移除具體實作細節
    • 保留類別結構、函數簽名與型別定義,以結構化摘要的方式降低 Token 使用量

第三步:Instruction Injection(指令注入)

  • 責任方: GitHub Proxy Layer
  • 動作: 在請求中注入系統層級的行為約束
  • Custom Instructions: 自動讀取 .github/copilot-instructions.md,讓團隊可定義一致的開發規範
  • System Prompt(黑箱實作):
    Copilot 會針對不同語言與使用情境,加入內部的偏好設定與引導指令,以提升產出品質與一致性

第四步:Model Routing(模型路由)

  • 責任方: GitHub Cloud Gateway

  • 動作: 根據任務性質選擇適合的模型:

    • 自動補全(Autocomplete) → 延遲極低的程式碼專用模型
    • Chat、複雜推理、Agent 任務 → 大型通用模型
  • 從系統架構角度來看,可以將 Copilot 視為建立了一層模型抽象/序列化層,負責將統一的上下文轉換為不同模型可理解的請求格式

第五步:Post-Processing & Validation(後處理與驗證)

  • 責任方: GitHub Proxy / Local IDE

  • 動作:

    • 語法與結構檢查: 對產出的程式碼進行快速的語法或結構驗證,以避免明顯錯誤
    • Safety & License Filter: 檢查是否涉及敏感資訊或潛在版權風險

第六步:Rendering(UI 渲染)

  • 動作:FIM(Fill-In-the-Middle) 形式顯示 Ghost Text,或在 Chat 視窗中輸出結構化 Markdown 內容

2. 核心技術解析

為什麼 Autocomplete 能做到毫秒級回應?

  • FIM 技術: Copilot 的模型能同時理解游標前(Prefix)與游標後(Suffix)的內容,因此能精準填補中間缺口,而不重複生成已存在的結構
  • 小型專用模型: 自動補全通常使用高度優化、延遲極低的程式碼模型,而非大型對話模型,以確保輸入時的即時性

如何降低幻覺風險?

  • 符號資訊作為 Grounding 訊號: Copilot 會利用 LSP 提供的符號與型別資訊,作為建議排序與可信度判斷的輔助
  • 語法層級的預過濾: 明顯無法通過語法解析的建議,通常不會顯示給使用者

MCP(Model Context Protocol)的角色

隨著 MCP 的導入,Copilot 開始支援「工具導向」的互動模式:

  • Tool Calling: 在支援的環境中,Copilot 可透過 MCP 與外部工具或系統互動
  • 跨工具整合:
    • MCP 提供一個標準化介面,讓 Copilot 能與開發環境中的各種工具協同運作
    • 相關支援仍在逐步擴展中,實際可用功能會依 IDE 與方案而有所差異

3. 給資深工程師的實戰建議

1️⃣ 管理 Context

Copilot 會優先參考目前開啟的檔案,當建議品質下降時,關閉不相關的 Tab 往往是最有效的調整方式。

2️⃣ 善用 .github/copilot-instructions.md

將團隊的風格、安全與工具使用規範明確寫入,能顯著提升一致性。

3️⃣ 使用 Few-shot Prompt

在 Chat 中提供輸入/輸出的範例,能快速對齊程式碼風格。

4️⃣ 明確負向約束

清楚告知「不要使用哪些技術或套件」,比事後修正更有效。

5️⃣ 分段互動 / 校正

  • 一次性完整描述(one-shot prompt)

    • 優點:上下文集中、token 使用效率高、理論上成本最低

    • 風險:

      • 需求理解一旦有偏差,整個輸出一起歪
      • 中途無法校正方向
      • 對複雜專案而言,不確定性很高
  • 分段 prompt(multi-step / iterative prompting)

    • 優點:

      • 可以逐步確認理解是否正確
      • 每一段都能修正假設
      • 對需求仍在釐清、會變動的專案特別安全
    • 缺點:token 成本較高、流程較慢

6️⃣ 先做規劃,再進入實作(Plan-first)

  • 在真正寫程式之前,先要求 AI 產出完整規劃,再進入執行階段
  • 不論是使用 Spec Kit、或是 Agent 的 plan 工具,都能有效降低實作期的不確定性
  • 規劃階段可涵蓋:
    • 功能拆解與模組邊界
    • 資料結構與介面設計
    • 關鍵技術決策與限制條件
    • 風險點與替代方案
  • 將這份規劃當作後續 vibe coding 的依據,你會明顯感受到:
    • 實作時幾乎不需要反覆來回修正方向
    • Copilot / Agent 的建議更貼近預期
    • 執行效率與最終成果品質都顯著提升

對資深工程師而言,AI 的最大價值往往不在「幫你寫幾行 code」,而是在 把模糊的需求,轉換成可執行的 roadmap

總結

GitHub Copilot 並不是單純「把程式碼丟給模型」,而是一套結合 IDE 上下文蒐集、結構化壓縮、模型路由與後處理驗證的完整系統,理解這些設計,有助於我們更有效地與 Copilot 協作,而不是把它當成黑箱魔法。

4. 推薦資源與延伸閱讀

📺 精選影片 (YouTube)

1. Context Engineering Clearly Explained (Tina Huang)

這部影片闡述了語境工程在建構 AI Agent 時的核心地位與實作架構。

  • 定義與區別

    • 語境工程 (Context Engineering):旨在設計動態系統,確保在對的時間以對的格式提供對的資訊給模型,這與單次對話式的「提示工程」不同,是專為構建 AI 應用程式(如 Agent)服務的技術
    • 核心目標:即是優化並打包上下文視窗(Context Window),為 AI Agent 撰寫一份詳盡的操作說明書
  • AI Agent 的六大組成(漢堡比喻)
    影片將 Agent 比喻為漢堡,必須具備以下組件,而語境工程則是整合這些組件的說明書:

    1. 模型 (Model):核心大腦(如 GPT, Claude 等)
    2. 工具 (Tools):與外部系統互動的能力(如讀取日曆、搜尋網頁)
    3. 知識與記憶 (Knowledge & Memory):儲存對話歷史或特定領域知識庫
    4. 音訊與語音 (Audio & Speech):提供更自然的互動介面
    5. 護欄 (Guardrails):確保行為安全與合規的機制
    6. 編排 (Orchestration):監控、部署與改進 Agent 的系統
  • 實作技巧

    • 結構化提示:使用 XML 標籤(如 <user query>)區分區塊,並要求 AI 以 JSON 格式輸出,清晰定義角色、任務步驟與限制
    • 多代理系統 (Multi-agent):對於複雜任務,建議拆解為多個 Agent(如一個負責搜尋,一個負責總結),並透過共享語境(Context)傳遞資訊

2. The Model Context Protocol (MCP) (Anthropic)

這部影片由 Anthropic 團隊介紹 MCP 協議,旨在解決 AI 工具與數據源連接的標準化問題。

  • MCP 的核心概念

    • 標準化協議:MCP 是一個開放標準,解決了 AI 應用程式(Client)與資料來源(Server)之間連線繁瑣的問題,避免開發者需針對每個工具單獨寫整合
    • 生態系效益:開發者只需建立一次 Server,就能對接所有支援 MCP 的客戶端(如 Claude Desktop、IDE),大幅降低整合門檻
  • MCP 的三大要素

    1. 工具 (Tools):模型可以執行的動作,例如執行程式碼或呼叫 API
    2. 資源 (Resources):提供給模型的原始數據或上下文,如檔案內容、日誌或資料庫數據
    3. 提示 (Prompts):預定義的提示模板(通常透過 slash command 觸發),讓使用者能快速載入特定任務設定
  • 發展與未來功能

    • 開源策略:透過開源不封閉生態系,讓產業界專注於構建模型智慧與工作流
    • Registry API:未來將允許模型主動搜尋並發現可用的 Server
    • 長時運行任務與反問 (Long-running & Elicitation):支援更長時間的任務執行,並允許 Server 反問使用者以獲取更多資訊

3. Your codebase, your rules: Customizing Copilot with context engineering (GitHub)

這部影片專注於如何在 VS Code 中透過語境工程客製化 GitHub Copilot,使其更貼合專案需求。

  • Copilot「開箱即用」的視野

    • Copilot 不僅讀取當前檔案,還能感知終端機輸出Linting 錯誤 (紅字波浪線)測試結果以及 VS Code 的 Tasks
    • 建議:確保專案安裝了正確的擴充套件並配置好 Linting,因為 Agent 會直接利用這些錯誤訊息來自我修正程式碼
  • 語境工程的三個層次

    1. Copilot Instructions:透過 .github/copilot-instructions.md 定義全專案通用的規則(如:不進行角色扮演、指定的 Coding Style),這是最基礎的客製化
    2. 領域特定指令 (Domain Specific):針對特定模式(如 Observable pattern)提供微型教學,僅在相關程式碼出現時載入,避免 AI 產生不存在的 API
    3. 提示與模式 (Prompts & Modes):設計可重複使用的指令(如 TDD RedTDD Green),將複雜的開發流程自動化
  • 進階技巧

    • 子代理 (Sub-agents) 與 Context 壓縮:利用子代理進行大量文件研究,但只回傳「執行任務所需的核心資訊」給主流程,以減少 Context window 的負擔
    • 計畫模式 (Plan Mode):限制 Agent 可使用的工具權限,使其專注於產生架構或實作計畫,而非直接寫碼

🔗 深度技術文檔

2025/12/20

Permanently Set Docker Socket Permissions to 666

從之前的 Vagrant 改到使用 wsl + docker 後,PhpStorm 的開發會連到 wsl 裡面的 Docker image 測試,速度還可以接受,Docker 預設情況下,/var/run/docker.sock 的權限為 660(srw-rw----),只有 root 用戶和 docker 組成員才能訪問。如果需要讓所有用戶都能訪問 Docker socket,需要將權限改為 666,通過 systemd 的 override 機制來永久設置 Docker socket 的權限。

步驟 1:創建 systemd 覆蓋配置

sudo mkdir -p /etc/systemd/system/docker.socket.d

步驟 2:創建配置文件

sudo tee /etc/systemd/system/docker.socket.d/override.conf > /dev/null << 'EOF'
[Socket]
SocketMode=0666
EOF

或者手動創建文件 /etc/systemd/system/docker.socket.d/override.conf,內容如下:

[Socket]
SocketMode=0666

步驟 3:應用配置

sudo systemctl daemon-reload
sudo systemctl restart docker.socket docker.service

步驟 4:驗證權限

ls -la /var/run/docker.sock

應該看到輸出類似:

srw-rw-rw- 1 root docker 0 Dec 20 00:15 /var/run/docker.sock

原理說明

  • Docker 的 socket 是由 systemd 的 docker.socket 單元管理的
  • 通過在 /etc/systemd/system/docker.socket.d/ 目錄下創建 override 配置文件,可以覆蓋默認的 socket 設置
  • SocketMode=0666 指定 socket 文件的權限為 666(所有用戶可讀寫)
  • 每次 Docker 啟動時,systemd 會自動應用這個權限設置

安全注意事項

⚠️ 警告:將 Docker socket 權限設置為 666 意味著系統上的任何用戶都可以訪問 Docker,這等同於給予所有用戶 root 權限。請確保你了解這樣做的安全風險:

  • 任何用戶都可以運行容器
  • 可以掛載宿主機的任何目錄
  • 可以以 root 權限執行命令

建議僅在開發環境或你完全信任所有用戶的環境中使用此配置。

配置文件位置

  • 配置文件:/etc/systemd/system/docker.socket.d/override.conf
  • Socket 文件:/var/run/docker.sock

恢復默認權限

如果需要恢復默認的 660 權限,刪除 override 配置文件並重啟服務:

sudo rm /etc/systemd/system/docker.socket.d/override.conf
sudo systemctl daemon-reload
sudo systemctl restart docker.socket docker.service

2025/12/07

How to Use Vim Modeline to Force Filetype and Formatting

在使用 Vim / Neovim 編輯設定檔(例如 nginx、Docker、env、conf)時,常常會遇到:

  • 副檔名看不出類型
  • 語法高亮錯誤
  • 縮排混亂
  • LSP / 補全失效

這時候可以使用一個超好用但很多人沒用過的功能:

👉 Vim Modeline

🔹 什麼是 Vim Modeline?

Modeline 是一種寫在檔案開頭或結尾的註解,用來告訴 Vim「這個檔案要用什麼設定」

Vim 開啟檔案時會自動讀取並套用。

最常見用途:

  • 指定語法類型(filetype)
  • 設定 tab 寬度
  • 控制縮排風格
  • 強制編碼

🔹 基本語法

# vim: set ft=nginx :

結構說明:

區段 說明
# 註解符號
vim: 宣告這是 Modeline
set 設定
ft=nginx 指定 filetype
: 結尾標記

等同於在 Vim 內手動輸入:

:set ft=nginx

✅ 精簡寫法

下面這三種「效果完全一樣」:

# vim: set ft=nginx :
# vim: ft=nginx :
# vim:ft=nginx

優點:

  • ✅ 超短
  • ✅ 不佔版面
  • ✅ 非常適合放在第一行
  • ✅ 設定檔實務最常用

✅ 精簡寫法也可以組合多個設定

# vim:ft=nginx ts=2 sw=2
# vim:ft=sh ts=2 sw=2 expandtab
# vim:ft=python ts=4 sw=4 expandtab

全部都是 ✅ 合法 Modeline。


✅ 最常用 Modeline 範例整理

✅ 強制指定語法類型(最常見用途)

# vim: set ft=nginx :
# vim: set ft=sh :
# vim: set ft=php :
# vim: set ft=python :
# vim: set ft=javascript :
# vim: set ft=typescript :
# vim: set ft=go :
# vim: set ft=lua :
# vim: set ft=json :
# vim: set ft=yaml :
# vim: set ft=dockerfile :

✅ 指定 Tab 與縮排格式

# vim: set ts=2 sw=2 :
# vim: set ts=4 sw=4 :
# vim: set expandtab :
# vim: set noexpandtab :
設定 功能
ts Tab 顯示寬度
sw 自動縮排寬度
expandtab Tab 轉為空白
noexpandtab 保留真實 Tab

✅ 組合技(實務最常用)

Nginx 設定檔

# vim: set ft=nginx ts=2 sw=2 :

Shell Script

# vim: set ft=sh ts=2 sw=2 expandtab :

Python

# vim: set ft=python ts=4 sw=4 expandtab :

Frontend 專案

// vim: set ft=javascript ts=2 sw=2 :

✅ 指定檔案編碼

# vim: set fileencoding=utf-8 :

✅ 放在檔案結尾的寫法(也合法)

# vim: set ft=nginx :

🔐 Modeline 安全性補充

某些 Vim 會預設關閉 Modeline,避免被惡意檔案注入指令。

你可以檢查:

:set modeline?

啟用方式:

:set modeline

永久啟用(vimrc / init.lua):

set modeline

✅ 什麼時候「一定要用」Modeline?

  • 副檔名錯誤的設定檔(.conf.env.tpl
  • 後端 mix 多種語言
  • Docker / K8s / Nginx / CI 設定
  • 團隊統一格式規範

2025/12/03

Using AI Turn My Requirement Into A Spec

LLM 最強大的就是你給予適當的內容,它可以幫你延伸很多你沒注意到的問題,最近常常把需求稍微梳理過後,加上一段 prompt 讓他整理成 spec,再將結果調整一下變可以開 redmine 的票。

widget 設定完成後,若使用者將 widget 的欄位刪除,有機率會造成計算或使用錯誤,經討論後端要先行驗證欄位是否存在,查找有問題後一律回覆 422 給前端

---

你的角色是網路工程師。請協助將我提供的功能簡述,轉換成規格(spec)格式。轉換後的 spec 需包含以下幾個內容:

- 功能標題
- 功能簡述
- 實作建議
- 測試建議
- 其他注意事項

允許使用 emoji 輔助

AI 產生的內容:

📌 Widget 欄位存在性驗證(後端)

📝 功能簡述

當使用者完成 widget 設定後,如果後續手動刪除了 widget 所需的欄位,可能導致運算錯誤或使用體驗異常。後端需在每次相關操作前進行欄位存在性驗證,若必填欄位不存在,統一回傳 HTTP 422 錯誤給前端。


🛠️ 實作建議

  1. 欄位驗證邏輯

    • 在 widget 運算、讀取前加入一段欄位檢查邏輯。
    • 驗證所有 widget 設定中宣告的欄位是否存在於資料來源(如資料庫欄位、配置檔或動態欄位列表)。
    • 若任一欄位不存在,立即中止流程並回傳 422。
  2. 統一錯誤格式

    • 使用統一格式,例如:
      {
        "message": "missing field",
        "code": "CO-ME108",
        "status": false
      }
      
      此 message 以及 code 為示意。

🧪 測試建議

  1. 正常情境

    • 所有欄位都存在 → 回傳 200 並正常執行邏輯。
  2. 單一欄位缺失

    • 刪除一個 widget 必要欄位 → 應回傳 422,且錯誤訊息不包含該欄位名稱。
  3. 多個欄位缺失

    • 模擬多個欄位被刪除 → 應回傳 422,且錯誤訊息不包含該欄位名稱。

其實他寫了相當多建議,這是我整理過後的結果,那些建議不一定符合我們需求,但可以當作參考也是相當不錯。

2025/10/08

Laravel Sail On WSL

緣由

我開發 PHP,不管是純 PHP 或 Laravel 都很喜歡用 Homestead,他優點是可以起一台 VM 就可以同時使用多個專案,而且透過修改一個簡單的 Homestead.yml 就可以做完全部環境的設定,但官方已經宣布不維護了,身為資訊人,你要嘛 fork 一份自己維護,不然就要跟著時代走。

不維護的原因也很簡單,VM 真的很肥很吃資源,現在 docker 實在是太香了,而且 Windows 引入 WSL 以後,Docker Desktop 也是秒級啟動,Laravel Sail 也是 base on Docker Compose,是時候跟 Homestead 分手了 。°(°¯᷄◠¯᷅°)°。。

SMB

WSL 有一個痛點,你用他預設掛載進去的目錄執行任何程式碼都慢到哭,目前我實作比較好的方式是透過 SMB 把你的工作目錄掛進去,跑起來跟在本機執行相差無幾,首先是在你要分享的目錄上按右鍵選內容,共用 -> 進階共用 -> 共用此資料夾,共用名稱建議字尾加個 $,這樣在網路芳鄰上會看不到這個目錄,像是 code$,這樣我打 \\HOST\\code$ 可以連到,在公網上是看不到的,另外是我在本機多設定了一組 smb/smb 帳號,這樣我在 demo 的時候也不會因為被看到密碼尷尬,SMB 沒設定的話預設是你這台 Windows 的登入帳號密碼。

WSL

我並沒有使用 Docker Desktop,因為他開機即啟動,而且無法手動 terminate,WSL 資源雖然吃不多,但我沒有要開發的時候並不需要你來吃我這份資源,我知道網路上有許多改機碼之類的方式可以達成這個目的,但官方沒給的我認為都是邪門歪道,我很不喜歡用非官方的方式解決問題,所以我自己起一台 Ubuntu wsl --install -d Ubuntu --name develop,之後再進去自己安裝 Docker,自由自在~

進去 WSL 以後我建議一律 sudo su,用 root 可以少遇到很多奇奇怪怪的權限問題,再來就是手動掛載 smb 了,我將指令放到 ~/.bashrc,因為 WSL 沒有固定 IP,所以你沒辦法把掛載寫死,要的話也是要寫一堆 script 去判斷東西,我是覺得沒必要。

在 Powershell 內可以執行 Get-SmbShare 得到你 smb 的目錄名稱。

Get-SmbShare

Name   ScopeName Path               Description
----   --------- ----               -----------
ADMIN$ *         C:\WINDOWS         遠端管理
C$     *         C:\                預設共用
code$  *         C:\Users\chan\code
IPC$   *                            遠端 IPC
Users  *         C:\Users

/scripts/mount_smb.sh

_get_windows_ip() {
    # 方法 1:透過 /proc/net/route (最穩定)
    # 取得預設閘道,就是 Windows host IP
    local gateway_ip
    gateway_ip=$(awk '$1 == "00000000" {print strtonum("0x"substr($3,7,2))"."strtonum("0x"substr($3,5,2))"."strtonum("0x"substr($3,3,2))"."strtonum("0x"substr($3,1,2))}' /proc/net/route | head -1)
    
    if [[ -n "$gateway_ip" ]]; then
        echo "$gateway_ip"
        return 0
    fi
    
    # 方法 2:透過 ip route (備選)
    gateway_ip=$(ip route show | awk '/^default via/ {print $3}' | head -1)
    
    if [[ -n "$gateway_ip" ]]; then
        echo "$gateway_ip"
        return 0
    fi
    
    # 方法 3:透過 resolv.conf (Windows 10 相容)
    gateway_ip=$(awk '/^nameserver/ {print $2; exit}' /etc/resolv.conf)
    
    if [[ -n "$gateway_ip" ]]; then
        echo "$gateway_ip"
        return 0
    fi
    
    echo "" >&2
    return 1
}

_mount_smb() {
    local smb_share="$1"
    local mount_point="$2"
    local smb_username="smb"
    local smb_password="smb"
    local smb_host
    
    # 檢查必要參數
    if [[ -z "$smb_share" ]] || [[ -z "$mount_point" ]]; then
        echo "Usage: _mount_smb <share> <mount_point>" >&2
        return 1
    fi
    
    # 檢查是否已掛載
    if mountpoint -q "$mount_point"; then
        echo "Already mounted at $mount_point"
        return 0
    fi
    
    # 確保掛載點存在
    [[ -d "$mount_point" ]] || mkdir -p "$mount_point"
    
    # 動態取得 Windows host IP
    smb_host=$(_get_windows_ip)
    
    if [[ -z "$smb_host" ]]; then
        echo "Error: Cannot determine Windows host IP" >&2
        return 1
    fi
    
    echo "Detected Windows host IP: $smb_host"
    echo "Attempting to mount //$smb_host/$smb_share to $mount_point"
    
    # 測試連線
    if ! timeout 3 bash -c "</dev/tcp/$smb_host/445" 2>/dev/null; then
        echo "Error: Cannot connect to $smb_host:445 (SMB port)" >&2
        return 1
    fi
    
    echo "SMB port is reachable, mounting..."
    
    # 執行掛載 (優先使用 SMB 3.0,失敗則試 2.1)
    if sudo mount -t cifs "//${smb_host}/${smb_share}" "$mount_point" \
        -o username="$smb_username",password="$smb_password",vers=3.0,uid=$(id -u),gid=$(id -g),file_mode=0777,dir_mode=0777 2>/dev/null; then
        echo "✓ SMB share mounted at $mount_point"
        return 0
    else
        echo "SMB 3.0 failed, trying SMB 2.1..."
        
        if sudo mount -t cifs "//${smb_host}/${smb_share}" "$mount_point" \
            -o username="$smb_username",password="$smb_password",vers=2.1,uid=$(id -u),gid=$(id -g),file_mode=0777,dir_mode=0777 2>/dev/null; then
            echo "✓ SMB share mounted at $mount_point (SMB 2.1)"
            return 0
        fi
        
        echo "✗ Failed to mount SMB share" >&2
        return 1
    fi
}

~/.bashrc

# ~/.bashrc - SMB Mount Configuration with Dynamic Windows IP

source /scripts/mount_smb.sh

# 只在互動式 shell 中執行
if [[ $- == *i* ]] && [[ $SHLVL -eq 1 ]]; then
    _mount_smb "code$" /mnt/code
fi

這段程式碼簡單講就是把我在本機 share 的 \\HOST\\code$ 掛到 ~/mnt/code,需要多掛幾個多執行幾次 _mount_smb 即可,每次進入 WSL 便抓取 Host Gateway IP 來連結,目錄跟檔案權限都設定成 777 避免一堆怪事發生,反正開發機不怕。

實際測試

這邊獻上幾個環境跑 phpunit 測試結果,測試數量 1364:

Host

real 1m 37.34s
real 1m 37.83s
real 1m 36.08s


平均:1m 37.08s

Vagrant + SMB

real 3m58.906s
real 3m32.119s
real 3m17.965s


平均:3m 36.33s

WSL + SMB

real 1m48.497s
real 1m57.464s
real 1m50.013s


平均:1m51.991s

其實在 Vagrant 裡面即便使用 SMB 執行速度還是悲劇,我已經這樣操作很久了,但因為 Homestead 的便利性一直選擇忽視這個問題,反正也不是整天跑 full test,另外有趣的一件事情,在 WSL 裡面使用 docker build 一個 php 環境跑測試,我預計應該要掉個 3% 的速度,因為它是 Host -> WSL -> Docker Volume,多一層轉換理應要消耗吧,但沒想到跑了幾次的結果它居然跟在 Windows Host 跑的時間差不多,php.ini 除了調整 memory_limit = -1 以外都沒動,不確定是 Docker 預設拉取的 OS 是 Alpine Linux 效能很好,還是 PHP 官方的 container php.ini 的配置有優化,總之蠻意外的。

之後預計會使用 WSL + Docker + Laravel/Sail 開發測試專案啦,謝謝你 Homestead。

2025/09/13

Docker Compose

原本以為 docker composedocker-compose 指令官方的套件整合,後來發現其實是全新的東西。

差異說明

  • docker-compose:是原先獨立安裝的 Compose 工具,使用 Python 實作,指令是用 docker-compose(中間有連字號),這是 Docker Compose v1 的常用形式。

  • docker compose:是 Docker Compose v2 版本,整合進 Docker CLI 中,採用 Go 語言重寫,使得 Compose 命令成為 docker 命令的一部分,指令使用空格隔開,即 docker compose,這是官方現階段推薦的新作法,提升整合度與效能。

Compose 檔案名稱的使用

歷史上預設檔名是 docker-compose.yml,由於 docker-compose 指令而來,仍被廣泛使用以確保向後相容。

新推薦的檔名是 compose.yaml(或 compose.yml 同樣被接受),更符合 Compose 規範,且映射到 docker compose 指令。官方文件中多建議使用 compose.yaml 作為現代 Compose 的標準檔名。

也可以用 -f 選項指定自訂檔名,靈活在不同目錄或專案間使用多個 Compose 配置檔。

建議改動

  1. 版本號 version 已經不需要寫
  2. deploy 只在 Swarm 模式才有用
  3. 官方推薦改成 compose.yaml
  4. 支援 profiles(服務分組)
services:
  db:
    image: mysql:8
    profiles: ["dev"]

  redis:
    image: redis:6
    profiles: ["prod", "staging"]
docker compose --profile dev up # 這樣執行只有 profiles 含 dev 的 container 會啟動
  1. 官方不建議用 links,用 depends_on 取代
  2. 多檔案合併,常用於「開發」跟「生產」環境分離

compose.yml

services:
  app:
    image: nginx:alpine
    ports:
      - "80:80"
    environment:
      - APP_ENV=production

compose.override.yaml

services:
  app:
    environment:
      - APP_ENV=development
    ports:
      - "8080:80" # 開發改用 8080 port
    volumes:
      - ./src:/usr/share/nginx/html:ro
docker compose -f compose.yaml -f compose.override.yaml up
  1. docker compose cp 已支援
  2. docker compose config 支援扁平化配置檔顯示
  3. 支援 Include
# compose.yaml
include:
  - ./database/compose.yaml
  - ./monitoring/compose.yaml
  
services:
  web:
    image: nginx
  1. 支援單一 updown service
docker compose up web fpm -d
docker compose down fpm

但有 depends_on 的會被連帶啟動。

2025/07/22

Gcloud Common Commands

☁️ gcloud 常用指令小抄

🧑‍💼 account:帳號管理

功能 指令
登入 gcloud auth login
登出 gcloud auth revoke
查看目前帳號 gcloud config get-value account
設定預設帳號 gcloud config set account <ACCOUNT>
列出所有帳號 gcloud auth list

🏢 project:專案管理

功能 指令
列出所有可用的專案 gcloud projects list
查看目前使用中的專案 gcloud config get-value project
切換專案 gcloud config set project <PROJECT_ID>
查看目前所有 config gcloud config list

💻 compute:VM 實例管理

一般操作

功能 指令
列出所有 instance gcloud compute instances list
查看 instance 詳細資訊 gcloud compute instances describe <INSTANCE> --zone=<ZONE>
啟動 instance gcloud compute instances start <INSTANCE> --zone=<ZONE>
關閉 instance gcloud compute instances stop <INSTANCE> --zone=<ZONE>
刪除 instance gcloud compute instances delete <INSTANCE> --zone=<ZONE>

SSH & 檔案傳輸

功能 指令
SSH 連線 gcloud compute ssh <INSTANCE> --zone=<ZONE>
上傳檔案 gcloud compute scp <LOCAL> <INSTANCE>:<REMOTE> --zone=<ZONE>
下載檔案 gcloud compute scp <INSTANCE>:<REMOTE> <LOCAL> --zone=<ZONE>
rsync 資料夾 gcloud compute rsync <LOCAL_DIR> <INSTANCE>:<REMOTE_DIR> --zone=<ZONE>

📦 scp 常用參數

參數 說明
--recurse-r 遞迴傳輸資料夾
--compress 傳輸時壓縮(較快)
--scp-flag="-C" 額外傳入 scp flag,例如啟用壓縮

📦 storage:Cloud Storage 操作

功能 指令
列出 bucket gcloud storage buckets list
建立 bucket gcloud storage buckets create <BUCKET> --location=<REGION>
上傳檔案 gcloud storage cp <LOCAL> gs://<BUCKET>/
下載檔案 gcloud storage cp gs://<BUCKET>/<FILE> <LOCAL>
刪除檔案 gcloud storage rm gs://<BUCKET>/<FILE>
刪除 bucket gcloud storage buckets delete <BUCKET>

🔁 Cloud Storage rsync

gcloud storage 支援類似 rsync 的功能來同步本地與 GCS 目錄:

上傳資料夾到 bucket

gcloud storage rsync -r ./local-folder gs://my-bucket/remote-folder

從 bucket 下載資料夾

gcloud storage rsync -r gs://my-bucket/remote-folder ./local-folder

🛠️ 常用參數

參數 說明
-r 遞迴處理目錄(必要)
-d 刪除目標端中不存在的檔案(mirror 同步)
-n 模擬執行(dry run,不實際動作)
-x "<REGEX>" 排除符合正則的檔案

🧩 其他常用指令

設定與服務

功能 指令
查看目前所有 config gcloud config list
清除設定 gcloud config unset <PROPERTY>
啟用服務 gcloud services enable <SERVICE>
停用服務 gcloud services disable <SERVICE>

IAM 權限操作

功能 指令
查看 IAM 成員 gcloud projects get-iam-policy <PROJECT>
新增 IAM 成員與角色 gcloud projects add-iam-policy-binding <PROJECT> --member="user:<EMAIL>" --role="roles/<ROLE>"

🧠 小提醒

  • 使用 get-value 取得設定值比 list 簡潔:
gcloud config get-value project
gcloud config get-value account
gcloud config get-value compute/zone
gcloud config get-value compute/region

2025/06/02

Neovim Copilot Chat Plugin

Github Copilot 已經是我工作上的好幫手,它的好處是你付一次月租他可以支援所有你有支援的軟體,我安裝在 Jetbrains 全套、vscode、neovim,一魚多吃相當方便,這邊介紹一下 vim/neovim 用來跟 copilot 對話的好用插件 CopilotChat.nvim,當然你要使用之前必須先有 copilot 帳號,以及安裝開通 copilot.vim

網站中有許多 plugin 安裝的方式,選自己常用的安裝即可,在你的 vim/neovim 配置檔案要加入啟動的語法:

lua << EOF
    require("CopilotChat").setup { }
EOF

" 用 cc 叫出視窗
nnoremap <silent> cc :CopilotChatToggle<CR>

我習慣是盡量不太修改插件預設值,避免有轉換環境的痛苦,套用 cc 叫出視窗是我針對 CopilotChat 唯一的設定了,我們用撰寫一個 docker-compose.yml 當作範例。

nvim docker-compose.yml 建立一個檔案,進入 vim/neovim 按下 cc,可以看到 CopilotChat 視窗在左邊出現。

以下是常用的快捷鍵以及指令:

插入模式 普通模式 功能說明
<Tab> - 觸發或接受補全選單(tokens 自動完成)
<C-c> q 關閉聊天視窗
<C-l> <C-l> 重設並清空聊天視窗
<C-s> <CR> 提交當前的提示內容
- grr 切換目前游標所在行是否為「固定提示」
- grx 清除所有「固定提示」
<C-y> <C-y> 接受最近的一個差異(diff)
- gj 跳轉到最近差異的區塊
- gqa 將所有回覆加入 quickfix 清單
- gqd 將所有差異加入 quickfix 清單
- gy 將最近的差異複製(yank)到暫存器
- gd 顯示原始內容與最近差異的比對
- gi 顯示目前聊天的資訊
- gc 顯示目前聊天的上下文
- gh 顯示說明訊息
指令 說明
:CopilotChat <輸入內容>? 開啟聊天視窗,並可選擇性地輸入問題或指令
:CopilotChatOpen 開啟聊天視窗
:CopilotChatClose 關閉聊天視窗
:CopilotChatToggle 切換聊天視窗(開啟與關閉間切換)
:CopilotChatStop 停止目前的輸出(中斷正在生成的內容)
:CopilotChatReset 重設聊天視窗(清除目前對話)
:CopilotChatSave <名稱>? 儲存聊天記錄,可選擇性指定一個名稱
:CopilotChatLoad <名稱>? 載入先前儲存的聊天記錄
:CopilotChatPrompts 查看並選擇提示範本(例如:解釋代碼、重構等用途)
:CopilotChatModels 查看並選擇可使用的 AI 模型
:CopilotChatAgents 查看並選擇可用的 AI 助手代理(不同風格或角色)
:CopilotChat<PromptName> 使用特定提示範本開啟對話(例如 :CopilotChatExplain

現在位於左邊的 CC 視窗,輸入寫一份 docker-compose.yml,需要安裝 nginx 以及 php-fpm,先按 esc 回到 visual mode 以後再按下 enter

我們可以看到他幫我們產生了許多內容,下面包含了基礎的 nginx 配置,更下方還有測試用的 php 程式碼,但我需要的只有 docker-compose 的部分,所以我將 cursor 移到我需要的區塊,然後按下 ctrl + y,此時他便將該區塊的程式碼移到了右邊去。

我接著輸入 prompt,現在的 docker-compose 不需要使用 version,他會依照上文幫我重產內容。

我們可以使用 gd 快速看出差異。

按下 q 回來以後,一樣使用 ctrl + y 將改變套用,這樣就可以套用新的格式,假設你已經脫離上下文,需要透過 cc 給你的檔案建議呢,這時就要用 vim/neovim 的選取功能,將需要他幫忙的地方選取起來,再去問一下 cc。

一樣用 ctrl + y 套用結果,如果你是要他參考整個文件就得使用全選了,另外一種方式是在命令列輸入 :CopilotChat 將 nginx 改為 ubuntu 版本 #buffer,這樣他就會把該檔案的內容當作緩衝使用,我自己是覺得用選的比較快。

另外一個主動參考整個文件的方法就是在第一次打開視窗問問題時,加上 #buffer,例如:

#buffer 將 $name 取代為 $userName

這樣他便會去直接參考整個檔案。

當我們使用 :CopilotChatModels 時,他會列出所有你可以使用的 model,可以記一下編號,移動到最下面後可以填入該編號,便可以切換模型。

但這個變動只有在這次有效,要永久使用該模型可以設定 config。

lua << EOF
require("CopilotChat").setup {
    model = 'claude-3.7-sonnet'
}
EOF

2025/05/26

Homestead SSL Issue

Laravel Homestead 在使用 nginx 創建網頁 config 時是有幫他配置 ssl key 的,但為什麼我們使用 https 還是會被警告呢,因為我們的瀏覽器是不認這個憑證的,在 Windows 上可以手動安裝該憑證讓你測試的時候可以透過 https 連到你的測試機,有時候開發會遇到跨網站連結 https 的狀況,如果你是 http 的話可能會被擋下來。

配置 Homestead.yml

folders:
  - map: ./ssl
    to: /ssl
  - map: ./code
    to: /home/vagrant/code

sites:
  - map: ssl.test
    to: /home/vagrant/code/
    php: "8.2"

這是我添加的測試網站,將根目錄開一個 ssl 目錄,在 code 目錄下放一個有 echo hello world 的 PHP 檔案,修改本機的 hosts 將 ssl.test 指到該 ip。

vm 起來後,vagrant ssh 進去該 vm,執行 sudo cp /etc/ssl/certs/ca.homestead.homestead.pem /ssl,將憑證複製到 /ssl 下,exit 退出 vm。

前往該 ssl 目錄,執行以下步驟:

  1. 右鍵 點擊憑證檔,選擇 「安裝憑證」
  2. 點選 「下一步」
  3. 選擇 「將所有憑證放入以下的存放區」
  4. 點選 「瀏覽」
  5. 選擇 「受信任的根憑證授權單位」
  6. 點選 「下一步」
  7. 點選 「完成」

此時瀏覽 https://ssl.test 應該就不會有警告了,如果還是有的話可能是 nginx cache,重開 server 或瀏覽器應該就解決了,可以先用無痕驗證一下。

該憑證過期時間為十年,應該是蠻夠用的,過期的話要重產一個新的,但我想應該在過期之前就會重置測試機了。

Homestad PHP PPA Issue

最近執行 Homesteadvagrant reload --provision 會遇到錯誤。

E: Repository 'https://ppa.launchpadcontent.net/ondrej/php/ubuntu jammy InRelease' changed its 'Label' value from '***** The main PPA for supported PHP versions with many PECL extensions *****' to 'PPA for PHP'

這是因為該 PPA 的 Label 產生了變化,解決方法也挺簡單。

vagrant ssh 進去 vm 後,執行下面指令:

sudo rm -rf /var/lib/apt/lists/*
sudo apt-get clean
sudo apt-get update --allow-releaseinfo-change

記得遇到這個問題處理過後要退出 vm 執行 vagrant reload --provision,才會有變動。

2025/05/13

Tmux Auto Split Window Syntax

tmux 是很強大的視窗分割工具,他可以透過語法建立初始化的視窗結構,今天的案例是我在我的 vagrant 虛機裡面有許多 laravel 專案,每個專案都有使用 horizon queue,每次要工作的時候我先 vagrant ssh 進去虛機以後,再手動割四個視窗,進入四個專案目錄後執行 php artisan horizon 啟動 queue server,這個過程可以透過語法讓 tmux 自己幫你完成。

horizon.sh

#!/bin/bash

# API 專案目錄
PROJECTS=(
    "/project1"
    "/project2"
    "/project3"
    "/project4"
)

# 取得目前 tmux Session 名稱
SESSION=$(tmux display-message -p '#S')

# 開啟新視窗並處理第一個 pane
FIRST_DIR="${PROJECTS[0]}"
tmux new-window -n "horizon" -c "$FIRST_DIR"
tmux select-window -t "${SESSION}:horizon"

# 設定第一個 pane 並執行命令
tmux select-pane -t 0
tmux send-keys -t 0 "php artisan horizon" C-m

# 處理其餘 panes
for i in "${!PROJECTS[@]}"; do
    if [ "$i" -eq 0 ]; then continue; fi

    DIR="${PROJECTS[$i]}"

    # 分割方向
    if [ "$i" -eq 1 ]; then
        tmux split-window -h -c "$DIR"
    else
        tmux select-pane -t $((i - 1))
        tmux split-window -v -c "$DIR"
    fi

    # 執行命令
    tmux select-pane -t "$i"
    tmux send-keys -t "$i" "php artisan horizon" C-m
done

# 整理 pane 排列
tmux select-layout tiled

該 script 需要在開啟了一個 tmux window 後執行。

2025/05/02

Log Handle In Docker

Docker 現在已經是可以當 production server 用的工具了,來聊一下他對 logs 有哪些配置方式。

根據 docker 官方文件,目前有以下幾個配置方法:

Driver 說明
none 容器沒有可用的日誌,且 docker logs 不會返回任何輸出。
local 日誌以一種為了最小化開銷而設計的自定義格式儲存。
json-file 日誌格式為 JSON。Docker 的預設日誌驅動程式。
syslog 將日誌消息寫入 syslog 設施。主機上必須運行 syslog 守護程序。
journald 將日誌消息寫入 journald。主機上必須運行 journald 守護程序。
gelf 將日誌消息寫入 Graylog 擴展日誌格式 (GELF) 端點,如 Graylog 或 Logstash。
fluentd 將日誌消息寫入 fluentd(轉發輸入)。主機上必須運行 fluentd 守護程序。
awslogs 將日誌消息寫入 Amazon CloudWatch Logs。
splunk 使用 HTTP 事件收集器將日誌消息寫入 Splunk。
etwlogs 將日誌消息寫入 Windows 事件追蹤 (ETW) 事件。僅適用於 Windows 平台。
gcplogs 將日誌消息寫入 Google Cloud Platform (GCP) Logging。

如果你能擁有自己的 log server,會建議把 log 打進 log api server,擁有權限的人就可以在 web tool 上做所有的查詢以及決定資料怎麼保留跟處理,畢竟如果 log 停留在各 server,以現在很多服務走 HA 或是 k8s 情況下,遇到狀況你到底要進哪台服務查詢都會成為問題。

不過現實生活中很多人都只有一台 server,而且也沒有 api log server 這種資源,只是要有 log 即可的話,我們一般會用 json-file 這個選項,這也是 docker 預設的 log driver。

docker-compose json-file 範例

services:
    nginx:
        image: nginx
        container_name: nginx
        ports:
            - "80:80"
        volumes:
            - ./nginx.conf:/etc/nginx/nginx.conf
        logging:
            driver: json-file
            options:
                max-size: "10m"
                max-file: "3"

這個範例會把 nginx 的 log 儲存到 /var/lib/docker/containers/<container-id>/<container-id>-json.log,當 log 檔案大小超過 10MB 時,docker 會自動將舊的 log 檔案重新命名為 <container-id>-json.log.1,並開始寫入新的 log 檔案,最多會保留 3 個 log 檔案,當你使用指令 docker logs 時,docker 會自動讀取最新的 log 檔案,並顯示在終端機上。

一般 image 如 nginx,他會把 log 設定成 stdout,這樣 docker 才能夠讀取到 log,然後寫入到你設定的 log driver 裡面,選擇 json-file 時 docker 會自動幫你把 stdout 或 stderr 的 log 轉成 json 格式,然後寫入到你設定的 log driver 裡面。

nginx 的話,他的配置會是這樣:

access_log /dev/stdout;
error_log /dev/stderr;

這樣的缺點是,access log 會跟 error log 都寫在同一個檔案裡面,會造成你在查詢 log 的時候不方便,假設我們想要使用 docker,又想要產生跟傳統 web server 一樣的 log,我們可以修改 nginx.conf,讓他把 log 寫到 /var/log/nginx/access.log/var/log/nginx/error.log

Nginx Config 範例

nginx.conf

http {
    server {
        listen       80;
        server_name  localhost;

        access_log /var/log/nginx/access.log;
        error_log /var/log/nginx/error.log;
    }
}

然後把 log 目錄掛載出來。

docker-compose.yml

services:
    nginx:
        image: nginx
        container_name: nginx
        ports:
            - "80:80"
        volumes:
            - ./nginx.conf:/etc/nginx/nginx.conf
            - ./logs:/var/log/nginx

我們在 /etc/logrotate.d/nginx 新增一個檔案,然後把以下的內容貼上去:

logrotate conf

/var/log/nginx/*.log {
    daily
    missingok
    rotate 7
    compress
    notifempty
    sharedscripts
    lastaction
        docker exec nginx nginx -s reopen
    endlaction
}

docker exec nginx nginx -s reopen 的用意是 logrotate 在 rotate 之後需要呼叫 docker 重新開啟 nginx 的 log,不這樣的話 nginx in docker 會使用你 rename 後的 log 檔案繼續寫 log。

當我們使用 compress,並且會針對壓縮過後的檔案做後續處理的話,一定要有 sharedscripts 以及使用 lastaction,如果使用 postrotate 的話,他不會等所有的檔案都執行完壓縮就直接作動了。

這邊有幾個 logrotate 的指令可以使用:

指令 說明
prerotate/endscript 在進行日誌輪轉之前執行的指令,每個匹配的日誌檔案都會執行一次
postrotate/endscript 在進行日誌輪轉之後執行的指令,每個匹配的日誌檔案都會執行一次
firstaction/endscript 在所有匹配日誌檔案進行輪轉之前執行的指令,只執行一次
lastaction/endscript 在所有匹配日誌檔案進行輪轉之後執行的指令,只執行一次
preremove/endscript 在舊的日誌檔案被移除之前執行的指令

另外有一種不用重新開啟檔案的配置參數 copytruncate,這個參數會在 logrotate 開始 rotate 的時候複製原本的 log 檔案,然後 truncate 原本的 log 檔案,這樣 nginx 就不需要重新開啟檔案了,但這樣有機會漏寫紀錄,所以不建議使用這個參數。

Apache2 Config 範例

apache2 的原始 log 配置會是:

ErrorLog /proc/self/fd/2
CustomLog /proc/self/fd/1 common

我們要修改成:

ErrorLog /var/log/apache2/error.log
CustomLog /var/log/apache2/access.log combined

然後把 log 目錄掛載出來。

docker-compose.yml

services:
    apache2:
        image: httpd
        container_name: httpd
        ports:
            - "80:80"
        volumes:
            - ./httpd.conf:/usr/local/apache2/conf/httpd.conf
            - ./logs:/var/log/apache2

httpd.conf

<VirtualHost *:80>
    ServerName localhost
    DocumentRoot /var/www/html

    ErrorLog /var/log/apache2/error.log
    CustomLog /var/log/apache2/access.log combined
</VirtualHost>

logrotate conf

/var/log/apache2/*.log {
    daily
    missingok
    rotate 7
    compress
    notifempty
    sharedscripts
    lastaction
        docker exec apache2 apachectl -k graceful
    endscript
}

這個範例跟 nginx 的範例一樣,只是把 nginx -s reopen 改成 apachectl -k graceful

假設你 logrotate 後要執行其他 bash 做其他處理的話。

/var/log/apache2/*.log {
    daily
    missingok
    rotate 7
    compress
    notifempty
    sharedscripts
    lastaction
        docker exec httpd apachectl -k graceful
        /usr/bin/bash /usr/local/bin/other_script.sh
    endscript
}

2025/04/02

Neovim Installation

neovim 是我即便是在 windows 下也會開來使用的 command line editor,來 memo 一下 Windows 跟 Linux 怎麼安裝。

Windows

我在 Windows 上都是使用 scoop 來管理套件,所以 neovim 也是用 scoop 來安裝,這樣好處是我用 scoop update 時都可以保持拿到最新的 neovim

scoop install neovim

powershell 可以執行指令安裝 vim-plug

iwr -useb https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim | ni $HOME\AppData\Local\nvim\autoload\plug.vim -Force

接下來就是配置你的個人檔案,位置在 C:\Users\你的使用者名稱\AppData\Local\nvim\init.vim,可以使用任何編輯軟體包括 neovim 開啟他,把 config 都配好以後,執行 neovim,首次執行會有很多錯誤是正常的,因為你的 plugin 並沒有真的安裝,開啟後執行 :PluginInstall,關閉再開啟 neovim 應該就正常了。

Linux

Linux 的部分有兩種方法,一個是透過你自己的 OS 去找最新的 repo 安裝,另一個是可以去 https://github.com/neovim/neovim/releases/,neovim 的 github 官網找最新版本的 appimage 檔案,只要你的 OS 有 FUSE 就可以直接使用,以 ubuntu 22 為例,此文章撰寫時最新的穩定版本是 NVIM v0.11.0

curl -LO https://github.com/neovim/neovim/releases/download/v0.11.0/nvim-linux-x86_64.appimage
chmod +x nvim-linux-x86_64.appimage
sudo mv nvim-linux-x86_64.appimage /usr/local/bin/nvim

執行完這段就有 nvim 可以執行了,接著裝套件。

sh -c 'curl -fLo "${XDG_DATA_HOME:-$HOME/.local/share}"/nvim/site/autoload/plug.vim --create-dirs \
       https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim'

mkdir -p ~/.config/nvim
touch ~/.config/nvim/init.vim
nvim ~/.config/nvim/init.vim

接著就跟 Windows 一樣安裝套件後就可以用了,之後要更新 neovim 版本只要抓最新版本的 appimage 替換即可。

2025/03/03

Pyinstaller With Tobix Pywine

Python 可以透過 PyInstaller 將執行檔編譯成 Windows 可以使用的 exe,很好用,不同的 python 版本編譯出來的 exe 對目前的 Windows 有不同的支援度:

Python 版本 Windows 7 Windows 8 Windows 10 Windows 11
Python 3.6 ✅ (最後支援)
Python 3.7
Python 3.8
Python 3.9
Python 3.10 ⚠️ (可能有問題) ⚠️ (可能有問題)
Python 3.11 ❌ (不支援) ❌ (不支援)
Python 3.12 ❌ (不支援) ❌ (不支援)

所以編譯的時候要考量 python 的版本,Pyinstaller 只有支援 Windows,在 Linux like 的環境要執行必須透過 Wine 這種指令驅動,如果你要跑 CI/CD 沒那麼方便,後來我選擇了 tobix/pywine - Docker Image | Docker Hub 這個 docker 專案,只要選擇你要的 python 版本,如 tobix/pywine:3.8 ,將外部的 python 檔案 mount 進去 container,執行 docker exec -it builder wine pyinstaller.exe -F xxx.py 便可以編譯了,如果有相關要安裝的套件,也可以事先執行 docker exec -it builder wine python.exe -m pip install -r /tmp/requirements.txt