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
}