有些工具,一開始你覺得它就是個花瓶,擺在那邊,感覺有用但又懶得學。結果某一天,你被專案的複雜性狠狠揍了一拳,才跪下來喊:「大哥,原來你這麼香。」
Makefile 就是這種存在。
老實說,我以前看到 Makefile 這三個字母,腦袋裡第一個浮現的畫面是:某個 1980 年代穿吊帶褲的老工程師,邊抽煙邊敲 CRT 螢幕前的黑底綠字,然後很酷地打一行 make build。我心裡只想:好啦好啦,這東西應該跟 DOS 一樣,應該早就進博物館了吧?
結果呢?2025 年了,Docker 滿天飛,Kubernetes 把大家搞到 PTSD,CI/CD 自動到你以為自己會失業,Makefile 居然還活著。活得比誰都頑強。
它簡直是工程師界的小強:打不死、掃不掉,甚至還會在你最狼狽的時候拍拍你肩膀,淡淡地說:「要不要我幫你跑一下環境?」
人生就這樣,有些東西比你的戀情還穩定。
講什麼「團隊標準化流程」啦、「協作一致性」啦,都是漂亮話。其實大家用 Makefile 的原因很簡單:懶。
舉個例子,這串:
docker compose -f docker-compose.dev.yml -f docker-compose.dev.gpu.yml up -d
我跟你講,這指令長到像咒語,一個 -
打錯就會讓整個環境崩掉,崩得比你晚餐掉地上還慘。那一刻你會懷疑人生,想說:「幹,不能有個東西幫我把這串咒語,變成一個短短的指令嗎?」
結果 Makefile出場了。
你只要打 make up
,就像呼叫神燈。環境一秒起飛。爽。
它的結構很簡單:
一切都很直白,像極了你媽在你出門前給你的待辦清單:
如果你沒照做?對不起,make 會翻臉:
no rule to make target
語氣比你媽還兇,冷酷到讓你懷疑自己是不是親生的。
除了 target 和 command,你還要懂三個基本概念:
1️⃣ 延遲賦值 =
DOCKER_COMPOSE = docker compose -f docker-compose.dev.yml -f docker-compose.dev.gpu.yml
2️⃣ 立即賦值 :=
HAS_GPU := $(shell command -v nvidia-smi >/dev/null 2>&1 && echo 1 || echo 0)
3️⃣ .PHONY
.PHONY: clean
clean:
rm -rf build/
這三寶學會了,你就能少掉一半的白頭髮。
範例一:Lint + Test + Build + Deploy
你可以這樣寫:
deploy: lint test build
@echo "Deploying..."
這就像一個「人生流程表」:
整個過程像去醫院做全身檢查:驗血 → X 光 → 心電圖 → 醫生蓋章。
如果中間某一步爆掉?對不起,不能出院,繼續躺床上 debug。
範例二:判斷有沒有 GPU
有些人用 MacBook Air,硬要跑 CUDA,就像拿湯匙去敲石頭。這時候 Makefile 很懂事:
HAS_GPU := $(shell command -v nvidia-smi >/dev/null 2>&1 && echo 1 || echo 0)
ifeq ($(HAS_GPU),1)
DOCKER_COMPOSE = docker compose -f docker-compose.dev.yml -f docker-compose.dev.gpu.yml
else
DOCKER_COMPOSE = docker compose -f docker-compose.dev.yml
endif
結果就是:
Makefile 就像那種貼心朋友,不會因為你窮就不跟你吃飯,只會拍拍你說:「來啦,咱們吃滷肉飯也行。」
$():Makefile 的「變數展開魔法」
在 Makefile 裡,$() 代表「去把變數的值拿出來」。
有點像你家冰箱上的便條紙寫著「牛奶在下層門板」,每次看到 $(),Make 就會乖乖去冰箱拿牛奶給你。
範例:
DOCKER_COMPOSE = docker compose -f docker-compose.dev.yml
up:
$(DOCKER_COMPOSE) up -d
這裡的 $(DOCKER_COMPOSE) 就會被換成完整的 docker compose -f docker-compose.dev.yml。
所以當你打 make up,實際執行的指令就是:
docker compose -f docker-compose.dev.yml up -d
👉 簡單來說:$() 就是「把變數填進去」的意思,避免你一直重複貼長長的指令。
起容器
up:
$(DOCKER_COMPOSE) up -d
打 make up
,所有容器乖乖起來。比養寵物還聽話。
要是容器跑不動?別擔心,錯誤訊息會像你的前任一樣,毫不留情地提醒你:「你哪裡搞錯了。」
停止與重啟
down:
$(DOCKER_COMPOSE) down
restart:
$(DOCKER_COMPOSE) down
$(DOCKER_COMPOSE) up -d
工程師的信仰指令:restart
。
不管發生什麼,先重啟。
人生要是也能這樣多好,失戀了就 make restart
,乾乾淨淨回到初始狀態。
quality_checks: format lint
format:
isort $(PY_DIRS)
black $(PY_DIRS)
python -m ruff check $(PY_DIRS) --fix
這裡做了三件事:
想像一下:專案是一個家。
有時候你覺得 Makefile 很煩,限制一堆。但沒有它,整個家瞬間變成無政府狀態。小孩會炸鍋,媽媽會崩潰,而你可能會直接搬出去(也就是 rage quit 這個專案)。
一個 專案的 Makefile,大概會長這樣:
.PHONY: lint test build deploy
lint:
@echo "🔍 Linting..."
black --check $(PY_DIRS)
isort --check-only $(PY_DIRS)
test:
@echo "🧪 Running tests..."
pytest tests
build:
@echo "📦 Building..."
docker build -t $(DOCKER_IMAGE) .
deploy: lint test build
@echo "🚀 Deploying..."
docker push $(DOCKER_IMAGE)
@echo "✅ Done!"
執行 make deploy
時,流程如下:
lint
→ 程式碼檢查test
→ 單元測試build
→ 建置 Docker 映像deploy
→ 推送映像到 registry,印出完成訊息輸出示例:
🔍 Linting...
🧪 Running tests...
📦 Building...
🚀 Deploying...
✅ Done!
💡重點:
@
可以 suppress command 輸出,只印出結果TAB vs 空白地獄
Makefile 最陰險的坑就是:命令要用 TAB,不是四個空白。IDE 自動轉換?炸。你以為是 bug,結果是縮排。這坑害死過一代又一代工程師。
變數遞迴展開*=
跟 :=
搞錯,你可能得到一個遞迴到懷疑人生的變數值。常常跑到一半發現 $()
變數展開變成鬼打牆。
錯誤不顯示
command 前加了 @,輸出被抑制,然後 build 爆炸,你還看不到哪行掛了。人生就像跟對象冷戰一樣,對方什麼都不講,你只能自己猜哪裡做錯。
up:
$(OBS_COMPOSE) up -d
$(STORAGE_COMPOSE) up -d
$(DOCKER_COMPOSE) up -d
-d
表示背景執行down:
$(DOCKER_COMPOSE) down
$(STORAGE_COMPOSE) down
restart:
$(DOCKER_COMPOSE) down
$(DOCKER_COMPOSE) up -d
quality_checks: format lint
format:
isort $(PY_DIRS)
black $(PY_DIRS)
python -m ruff check $(PY_DIRS) --fix
lint:
pylint --rcfile=.pylintrc $(PY_DIRS) || true
python -m bandit -r $(PY_DIRS) || true
我發現,Makefile 就是工程師的「萬能遙控器」。
以前你要開電視、開冷氣、開電燈,每個要用不同的遙控器。現在只要一個 Makefile,全包。
make up
:環境起來。make down
:環境收掉。make test
:測試跑完。make deploy
:專案上線。make clean
:資料全炸掉(像按下「人生重置」)。最重要的是:這些東西團隊成員都能共用。大家不用再在 Slack 上問:「欸,要跑測試的指令是什麼來著?」你只要冷冷丟一句:「make test
」。
瞬間變成團隊裡的智者,或者說,懶得講廢話的智者。
分層管理 Docker Compose
前端、後端、存儲、監控等 Compose 拆開,方便局部重啟或 CI/CD 測試
利用變數與條件簡化操作
GPU 判斷、目標組合、網路建立等用變數管理,方便多人協作
容器化測試
測試命令直接進入容器,避免 host 環境干擾,CI/CD 無痛集成
品質檢查一鍵化make quality_checks
立即完成格式化 + lint + Bandit 安全掃描,減少人為疏漏
Makefile 不會幫你寫程式碼,不會幫你 debug,更不會幫你升職加薪。
但它能讓你少掉一堆無聊動作,幫你留點精力,去焦慮人生更大的麻煩。
而且每次我敲下 make deploy
,看到「🚀 Deploying… ✅ Done!」,內心都會冒出一種小確幸。
就像在這個充滿 bug、KPI、情感債務的世界裡,終於有一件事是「一鍵完成」的。
可惜,這世界上其他麻煩事,比如戀愛、健身、存錢,都沒有 make 指令。
不然我一定天天敲 make happy,然後坐在那裡等結果。
👉 好啦,說到這裡,你是不是也開始想回專案裡,加一個 Makefile 了?
不加也沒差,等你哪天被一堆 Docker 指令搞到懷疑人生,你自然會懂。
到時候別謝我,謝那個在 1976 年發明 make 的祖師爺吧。
完整 Makefile 內容如專案實例,涵蓋 network 建立、容器啟停、測試、建置、部署與 pipeline 操作,可直接參考附錄程式碼。
# .env 檔案會自動載入環境變數
ENV_FILE=.env
HAS_GPU := $(shell command -v nvidia-smi >/dev/null 2>&1 && echo 1 || echo 0)
OBS_COMPOSE = docker compose -f docker-compose.obs.yml
# 動態決定 docker compose 指令
ifeq ($(HAS_GPU),1)
DOCKER_COMPOSE = docker compose -f docker-compose.dev.yml -f docker-compose.dev.gpu.yml
else
DOCKER_COMPOSE = docker compose -f docker-compose.dev.yml
endif
STORAGE_COMPOSE = docker compose -f docker-compose.storage.yml
MONITOR_DEV_COMPOSE = docker compose -f docker-compose.monitor.dev.yml
MONITOR_COMPOSE = docker compose -f docker-compose.monitor.yml
DOCKER_FRONTEND_COMPOSE = docker compose -f docker-compose.frontend.yml
PY_DIRS = note apiGateway email arxiv
NETWORKS = monitor-net app-net langfuse-otel-net
.PHONY: test
net-create:
@for net in $(NETWORKS); do \
echo "🔌 檢查/建立 network $$net"; \
if ! docker network inspect $$net >/dev/null 2>&1; then \
docker network create $$net --driver bridge; \
echo "✅ 建立 $$net 完成"; \
else \
echo "✅ $$net 已存在"; \
fi \
done
# 啟動所有容器(背景執行)
up:
$(OBS_COMPOSE) up -d
$(STORAGE_COMPOSE) up -d
$(DOCKER_COMPOSE) up -d
$(MONITOR_DEV_COMPOSE) up -d
$(MONITOR_COMPOSE) up -d
$(DOCKER_FRONTEND_COMPOSE) up -d
up-front:
cd frontend && npm i && npm run dev
# 停止所有容器
down:
$(DOCKER_FRONTEND_COMPOSE) down
$(STORAGE_COMPOSE) down
$(MONITOR_DEV_COMPOSE) down
$(MONITOR_COMPOSE) down
$(DOCKER_COMPOSE) down
$(OBS_COMPOSE) down
# 重啟所有容器
restart:
$(DOCKER_COMPOSE) down
$(DOCKER_COMPOSE) up -d
# 查看容器日誌(預設看 apiGateway)
logs:
$(DOCKER_COMPOSE) logs -f apiGateway
# 查看所有容器日誌
logs-all:
$(DOCKER_COMPOSE) logs -f
# 重建全部服務
build:
$(MAKE) net-create
$(DOCKER_COMPOSE) build
$(MONITOR_DEV_COMPOSE) build
$(DOCKER_COMPOSE) exec ollama /bin/bash -c "ollama pull gpt-oss:20b"
# 進入 apiGateway 容器
shell:
$(DOCKER_COMPOSE) exec apiGateway bash
# 測試 (需先裝 pytest)
test-note:
$(DOCKER_COMPOSE) exec noteserver /bin/sh -c "PYTHONPATH=/app pytest tests"
test-apiGateway:
$(DOCKER_COMPOSE) exec apiGateway /bin/sh -c "PYTHONPATH=/app pytest tests"
# integration_test:
# $(DOCKER_COMPOSE) exec apiGateway /bin/sh -c "PYTHONPATH=/app pytest -v tests/integration"
# sample job
lanhchain:
$(DOCKER_COMPOSE) exec apiGateway /bin/bash -c "PYTHONPATH=/app python services/langchain_client.py"
ollama-client:
$(DOCKER_COMPOSE) exec noteserver /bin/bash -c "PYTHONPATH=/app python services/ollama/client.py"
sample_qdrant:
$(DOCKER_COMPOSE) exec noteserver /bin/bash -c "PYTHONPATH=/app python services/qdrant/sample_qdrant.py"
email-trial:
$(DOCKER_COMPOSE) exec email-flow /bin/bash -c "/opt/conda/envs/prefect/bin/python trial.py"
# pipeline
ingest-arxiv:
$(DOCKER_COMPOSE) exec arxiv-flow /bin/bash -c "/opt/conda/envs/prefect/bin/python arxiv_pipeline.py"
email-subscribe:
$(DOCKER_COMPOSE) exec email-flow /bin/bash -c "/opt/conda/envs/prefect/bin/python pipeline.py"
rag:
$(DOCKER_COMPOSE) exec noteserver /bin/bash -c "PYTHONPATH=/app python arxiv_rag_pipeline.py"
# 移除所有 volumes (⚠️會清除資料)
clean:
$(MAKE) down
sudo rm -rf ./data ./obs_data
up-dev:
$(DOCKER_COMPOSE) up -d note-qdrant noteserver
# 1️⃣ 一鍵檢查品質
quality_checks: format lint
# 2️⃣ 格式化程式碼
format:
isort $(PY_DIRS)
black $(PY_DIRS)
python -m ruff check $(PY_DIRS) --fix
# 3️⃣ 代碼檢查
lint:
pylint --rcfile=.pylintrc $(PY_DIRS) || true
python -m bandit -r $(PY_DIRS) || true
down-monitor:
$(MONITOR_COMPOSE) down
up-monitor:
$(MONITOR_COMPOSE) up -次