我們前面已經完成的基礎的架構和程式碼撰寫,現在是時候將我們的應用程式容器化,並使用 Docker Compose 來管理多個服務(應用程式、MySQL 和 Redis)。
這樣不僅能確保我們的開發環境和生產環境一致,還能大幅簡化部署流程。
在開發過程中,我們經常會遇到「在我機器上可以運行」的問題,這通常是因為環境配置不一致所導致的。
而 Docker 就是這個解決方案。
在本文中,我們將:
Dockerfile
。docker-compose.yaml
檔案來定義和管理多個服務 - for local development.Makefile
中,簡化日常操作。Dockerfile
- 打造精簡的 Go 映像檔一個好的 Docker 映像檔應該是小巧且安全的。我們將使用**多階段建置(multi-stage build)**的模式來達成這個目標。
這能讓我們在一個包含完整 Go 工具鏈的「建置環境」中編譯程式,然後只將最終的執行檔複製到一個極小的「運行環境」中。
在專案根目錄下建立 Dockerfile
檔案:
# 第一階段:建置環境
# 使用官方的 Go 映像檔作為建置環境
FROM golang:1.24.6-alpine AS builder
# 設定工作目錄
WORKDIR /app
# 複製 go.mod 和 go.sum 並下載依賴
# 這樣可以利用 Docker 的層快取,只有在依賴變更時才重新下載
COPY go.mod go.sum ./
RUN go mod download
# 複製所有原始碼
COPY . .
# 編譯應用程式,並將二進位檔輸出到 /app/bin 目錄
ENV GOOS=linux
ENV CGO_ENABLED=0
RUN go build -o ./bin/server ./cmd/api
# 第二階段:運行環境
# 使用更小的映像檔作為運行環境
FROM alpine:3.21.2
# 設定工作目錄
WORKDIR /app
# 從 builder stage 複製已編譯的二進位檔
COPY --from=builder /app/bin/server .
# 複製任何必要的靜態檔案(例如資料庫遷移檔案)
COPY --from=builder /app/deployments/migrations /app/deployments/migrations
ENTRYPOINT ["./server"]
設定檔解釋:
FROM golang:1.24.6-alpine AS builder
:使用官方的 Go 映像檔作為建置環境。WORKDIR /app
:設定工作目錄為 /app
。COPY go.mod go.sum ./
和 RUN go mod download
: 複製 go.mod
和 go.sum
檔案並下載所有依賴,這樣可以利用 Docker 的層快取功能。COPY . .
:複製所有原始碼到容器中。ENV GOOS=linux
和 ENV CGO_ENABLED=0
:設定環境變數,確保編譯的二進位檔是針對 Linux 並且是靜態連結的。RUN go build -o ./bin/server ./cmd/api
:編譯應用程式,並將二進位檔輸出到 /app/bin
目錄。FROM alpine:3.21.2
:使用更小的 Alpine 映像檔作為運行環境。COPY --from=builder /app/bin/server .
:從建置環境複製已編譯的二進位檔到運行環境中。COPY --from=builder /app/deployments/migrations /app/deployments/migrations
:複製任何必要的靜態檔案(例如資料庫遷移檔案)。ENTRYPOINT ["./server"]
:設定容器的入口點為我們的應用程式。docker-compose.yaml
- 定義多個服務 - for local development接下來,我們需要一個 docker-compose.yaml
檔案來定義和管理我們的多個服務,包括 Go 應用程式、MySQL 和 Redis。
在專案根目錄下建立 docker-compose.yaml
檔案:
services:
app:
build:
context: .
dockerfile: Dockerfile
container_name: go-clean-project-app
restart: always
ports:
- "${LOCAL_SERVER_PORT}:${SERVER_PORT}"
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
env_file:
- .env
# Database Service (MySQL)
mysql:
image: mysql:8.0.37
container_name: go-clean-project-mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
ports:
- "${LOCAL_MYSQL_PORT}:${MYSQL_PORT}"
volumes:
- mysql_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
# Redis Service
redis:
image: redis:7.2.4-alpine
container_name: go-clean-project-redis
restart: always
volumes:
- redis_data:/data
ports:
- "${LOCAL_REDIS_PORT}:${REDIS_PORT}"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
volumes:
mysql_data:
redis_data:
設定檔解釋:
services.app
:
build
:
context: .
: 指定建置上下文為目前目錄。dockerfile: Dockerfile
: 指定使用的 Dockerfile 為 Dockerfile
。ports
:將主機的 ${LOCAL_SERVER_PORT}
埠映射到容器的 ${SERVER_PORT}
埠,這樣可以靈活設定應用程式的埠號。並且可以避免與其他服務的埠號衝突。depends_on
:確保 mysql
和 redis
服務會比 app
服務先啟動,並且只有在它們健康檢查通過後才啟動 app
服務。env_file: [.env]
:指示容器從 .env
檔案中讀取環境變數。services.mysql
:
image: mysql:8.0.37
:使用官方的 MySQL 8.0.37 映像檔。environment
: 設定 MySQL 容器需要的環境變數。ports
:將主機的 ${LOCAL_MYSQL_PORT}
埠映射到容器的 ${MYSQL_PORT}
埠。方便我們在本地機器上訪問 MySQL。並且可以避免與其他 mysql 服務的埠號衝突。volumes: [mysql_data:/var/lib/mysql]
: 這是極其重要的一步。它建立一個名為 mysql_data
的具名儲存卷,並將其掛載到容器中存放 MySQL 數據的目錄。這能確保即使容器被刪除,我們的資料庫數據也能被保留下來。healthcheck
:設定健康檢查,確保 MySQL 服務在啟動後能正常運行。services.redis
:
image: redis:7.2.4-alpine
:使用官方的 Redis 7.2.4-alpine 映像檔。ports
:將主機的 ${LOCAL_REDIS_PORT}
埠映射到容器的 ${REDIS_PORT}
埠。方便我們在本地機器上訪問 Redis。並且可以避免與其他 redis 服務的埠號衝突。volumes: [redis_data:/data]
:建立一個名為 redis_data
的具名儲存卷,並將其掛載到容器中存放 Redis 數據的目錄。healthcheck
:設定健康檢查,確保 Redis 服務在啟動後能正常運行。相信看到這裡,可能有人會疑惑,為什麼我們的 docker-compose.yaml
檔案中,MySQL 和 Redis 的環境變數是使用 ${VAR}
的語法,而不是直接寫死在檔案中?
這是因為我們希望將敏感資訊(例如資料庫密碼)和環境相關的設定(例如埠號)從程式碼中分離出來。
這樣做有幾個好處:
.env
檔案中,方便查看和修改。隱藏知識點:
Docker Compose 會自動尋找專案根目錄下的 .env
檔案,並將其中的變數載入到環境中。
因為這一點,所以我們可以在 docker-compose.yaml
檔案中使用 ${VAR}
的語法來引用 .env 設定的變數內容。
現在我們已經有了 Dockerfile
和 docker-compose.yaml
,我們可以使用 Docker Compose 來啟動和管理我們的應用程式。
在專案根目錄下,打開終端並執行以下指令:
docker compose up -d --build
這個指令會做以下幾件事:
--build
:強制重新建置映像檔,即使沒有變更。-d
:讓容器在背景執行。docker-compose.yaml
檔案的設定,啟動所有定義的服務。docker ps
如果你需要查看某個容器的日誌,可以使用以下指令:
docker compose logs -f app
這會持續輸出 app
服務的日誌,方便你進行除錯。
如果你需要停止並移除所有容器,可以使用以下指令:
docker compose down
這會停止並移除所有由 Docker Compose 啟動的容器,但不會刪除具名儲存卷,這樣你的資料庫數據仍然會被保留。
如果你需要刪除具名儲存卷,可以使用以下指令:
docker compose down -v
這會同時刪除所有由 Docker Compose 啟動的容器和具名儲存卷,請謹慎使用。
Makefile
中為了讓日常操作更加方便,我們可以將常用的 Docker 指令整合到 Makefile
中。
在專案根目錄下的 Makefile
中,Development 區域加入以下內容:
## 這一行是修改原本的 .PHONY: run
.PHONY: run dockerUp dockerDown dockerDownClean dockerLogs
dockerUp: ## 啟動並建置 Docker 容器
docker compose up -d --build
dockerDown: ## 停止並移除 Docker 容器
docker compose down
dockerDownClean: ## 停止並移除 Docker 容器和具名儲存卷
docker compose down -v
dockerLogs: ## 查看 app 服務的日誌
docker compose logs -f app
這樣一來,你就可以使用以下指令來管理你的 Docker 容器:
make dockerUp
make dockerDown
make dockerDownClean
app
服務的日誌:
make dockerLogs
通過將我們的 Go 應用程式容器化並使用 Docker Compose來管理多個服務,我們不僅確保了開發環境和生產環境的一致性,還大幅簡化了部署流程。
這樣的設定讓我們能夠更專注於開發,而不必擔心環境配置的問題。
此外,將常用的 Docker 指令整合到 Makefile
中,進一步提升了我們的工作效率。
本文添加的完整內容可以到 Github 觀看