Golang 專案實戰與部署 ── Docker 容器化
簡介
在現代軟體開發流程中,Docker 已成為最常見的部署工具之一。它能把應用程式、相依套件、作業系統環境全部封裝成一個可搬移的容器,讓「在我機器上跑得好」的問題徹底消失。對於使用 Golang 開發的服務,Docker 更能發揮其輕量、啟動快的特性,讓開發、測試、上線的流程變得一致且自動化。
本篇文章將從 Docker 基礎概念、實作範例、常見陷阱與最佳實踐,一路帶領讀者完成一個簡單的 Go Web API 的容器化,並說明在實務專案中如何運用 Docker 進行部署與擴充。
核心概念
1. Docker 什麼是容器?
Docker 容器是一種 輕量級、可執行的軟體包,內含程式碼、執行時、系統工具與相依套件。與傳統的虛擬機不同,容器共享宿主機的核心 (kernel),因此啟動時間通常在 毫秒級,資源開銷也相對較低。
重點:容器的可移植性來源於 Docker Image(鏡像檔),而 Dockerfile 則是描述如何建構這個鏡像的腳本。
2. Dockerfile 基本結構
| 指令 | 說明 |
|---|---|
FROM |
指定基礎映像,例如 golang:1.22-alpine |
WORKDIR |
設定工作目錄 |
COPY / ADD |
複製檔案到容器內 |
RUN |
在建構階段執行指令(如安裝套件) |
ENV |
設定環境變數 |
EXPOSE |
宣告容器對外的埠號 |
CMD / ENTRYPOINT |
容器啟動時執行的指令 |
3. 多階段建構(Multi‑Stage Build)
Go 程式編譯後的執行檔通常只有幾 MB,但若直接以 golang 官方映像作為基礎,最終鏡像會帶有整個編譯環境,體積會變大。多階段建構 允許我們在同一個 Dockerfile 中使用兩個(或以上)FROM,先在第一階段編譯,然後把產出的二進位檔拷貝到第二階段的精簡映像(如 scratch 或 alpine),大幅減少鏡像大小。
4. Docker Compose
在實務上,一個服務往往不只單一容器,還可能需要資料庫、快取、訊息佇列等。Docker Compose 透過 docker-compose.yml 定義多個服務的關係、網路、環境變數等,讓 一鍵啟動 成為可能。
程式碼範例
以下示範一個簡單的 Go RESTful API,並以 Docker 完成容器化。每段程式碼均附有說明,方便初學者快速掌握。
1. 建立 Go 專案
mkdir go-docker-demo
cd go-docker-demo
go mod init github.com/example/go-docker-demo
main.go
package main
import (
"encoding/json"
"log"
"net/http"
)
// User 表示簡易的使用者資料結構
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
// helloHandler 回傳一段文字
func helloHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, Docker!"))
}
// usersHandler 回傳 JSON 陣列
func usersHandler(w http.ResponseWriter, r *http.Request) {
users := []User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
}
func main() {
http.HandleFunc("/", helloHandler)
http.HandleFunc("/users", usersHandler)
// 監聽 8080 埠
log.Println("Server listening on :8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatalf("Server error: %v", err)
}
}
說明:此程式僅使用標準函式庫,無需額外相依,適合作為 Docker 教學的基礎範例。
2. Dockerfile(使用多階段建構)
# ---------- Build Stage ----------
FROM golang:1.22-alpine AS builder
# 安裝 git(若需要拉取私有模組)
RUN apk add --no-cache git
WORKDIR /app
# 先複製 go.mod、go.sum 以利用快取
COPY go.mod go.sum ./
RUN go mod download
# 複製所有原始碼
COPY . .
# 編譯成靜態二進位檔
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /go-docker-demo .
# ---------- Runtime Stage ----------
FROM alpine:3.20
# 建立非 root 使用者,提升安全性
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
WORKDIR /app
# 從 builder 階段拷貝二進位檔
COPY --from=builder /go-docker-demo .
# 暴露容器內部埠號
EXPOSE 8080
# 容器啟動指令
ENTRYPOINT ["./go-docker-demo"]
重點:
CGO_ENABLED=0產生純靜態二進位檔,讓最終映像可在scratch或alpine中執行。- 使用
alpine作為 Runtime Stage,可把最終鏡像大小控制在 約 10 MB 左右。- 建立非 root 使用者
appuser,減少安全風險。
3. 建置與測試 Docker 映像
# 建置映像,標記為 go-docker-demo:latest
docker build -t go-docker-demo:latest .
# 以本機 8080 埠對映容器 8080 埠,測試服務是否正常
docker run --rm -p 8080:8080 go-docker-demo:latest
在瀏覽器或使用 curl 測試:
curl http://localhost:8080/
# => Hello, Docker!
curl http://localhost:8080/users
# => [{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]
4. Docker Compose 範例(加入 PostgreSQL)
version: "3.9"
services:
api:
build: .
container_name: go_api
ports:
- "8080:8080"
environment:
- DB_HOST=db
- DB_PORT=5432
- DB_USER=postgres
- DB_PASSWORD=secret
- DB_NAME=demo
depends_on:
- db
db:
image: postgres:16-alpine
container_name: pg_demo
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: secret
POSTGRES_DB: demo
volumes:
- pg_data:/var/lib/postgresql/data
ports:
- "5432:5432"
volumes:
pg_data:
說明:
depends_on確保 API 服務在資料庫容器啟動後才執行。- 使用
volumes持久化 PostgreSQL 資料,避免容器重啟時資料遺失。- 只要在專案根目錄執行
docker-compose up --build,整個環境即會一次性啟動。
5. 在 CI/CD 中使用 Docker(GitHub Actions 範例)
name: CI
on:
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Build Docker image
run: |
docker build -t ghcr.io/${{ github.repository }}:${{ github.sha }} .
echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
docker push ghcr.io/${{ github.repository }}:${{ github.sha }}
重點:透過 CI 流程自動建置、推送 Docker 映像至容器註冊表,讓部署環境只需要拉取映像即可。
常見陷阱與最佳實踐
| 陷阱 | 可能的影響 | 解決方案 / 最佳實踐 |
|---|---|---|
使用 latest 標籤 |
部署時不易追蹤版本,升級可能導致不可預期的錯誤 | 固定版本號(如 v1.0.3)或使用 Git commit SHA 作為標籤 |
| 容器內部以 root 身份執行 | 安全風險,若容器被入侵,攻擊者可取得宿主機權限 | 建立非 root 使用者,並在 Dockerfile 中使用 USER 指令 |
未使用 .dockerignore |
建置時會把整個專案目錄(包括 .git、測試檔等)複製進容器,導致鏡像過大 |
在專案根目錄建立 .dockerignore,排除不必要的檔案 |
過度依賴 ADD |
ADD 會自動解壓縮檔案,可能產生意外行為 |
盡量使用 COPY,僅在需要解壓縮或下載遠端檔案時才使用 ADD |
| 忽略健康檢查 (HEALTHCHECK) | 容器崩潰但仍被認為是「Running」狀態,導致服務不可用 | 在 Dockerfile 中加入 HEALTHCHECK,或在 docker-compose.yml 使用 healthcheck 設定 |
| 未設定資源限制 | 容器可能耗盡宿主機資源(CPU、Memory) | 使用 docker run --memory、--cpus 或在 Compose 中設定 deploy.resources |
最佳實踐小結:
- 最小化映像:使用多階段建構、
scratch或alpine。 - 環境變數管理:將機密資訊(DB 密碼、API 金鑰)放在 Docker secret 或 環境變數檔,避免硬編碼。
- 日誌輸出:容器內部的日誌應直接寫到
stdout/stderr,讓 Docker 叢集(如 Kubernetes)可以集中收集。 - 版本化:每次建置都使用唯一標籤,配合 CI/CD 讓部署可回溯。
- 測試:在建置階段加入單元測試或整合測試,確保映像在推送前已通過驗證。
實際應用場景
| 場景 | 為何使用 Docker | 具體做法 |
|---|---|---|
| 微服務架構 | 每個服務獨立部署、升級、擴容 | 為每個 Go 微服務建立獨立 Dockerfile,使用 Docker Compose 或 Kubernetes 管理 |
| 本地開發環境 | 團隊成員環境一致,避免「在我電腦上可以跑」的問題 | 提供 docker-compose.yml,一條指令 docker-compose up -d 即可啟動所有依賴(DB、Cache) |
| CI/CD 自動化 | Build → Test → Deploy 全程自動化 | 在 GitHub Actions、GitLab CI 中使用 Docker BuildKit,推送至私有 Registry,最後在 Kubernetes 叢集滾動更新 |
| 資源受限的 Edge 裝置 | Go 程式本身執行效能佳,Docker 提供輕量化封裝 | 使用 scratch 映像,僅保留二進位檔與必要的 CA 證書,映像大小可低於 5 MB |
| 多租戶 SaaS 平台 | 每個租戶需要隔離的執行環境 | 為每個租戶建立獨立容器,配合 Docker Swarm 或 Nomad 的資源配額功能 |
總結
Docker 為 Golang 應用提供了 可移植、輕量、快速部署 的解決方案。透過 多階段建構 可以將編譯環境與執行環境徹底分離,讓最終映像保持極小;配合 Docker Compose 或 Kubernetes,即使是複雜的微服務系統也能以 宣告式 的方式管理。
在實務開發中,務必遵守 安全性(非 root、環境變數管理)與 可維護性(版本標籤、健康檢查、資源限制)的最佳實踐,才能讓 Docker 成為提升開發效率與部署可靠性的利器。
最後提醒:容器化不是一次性的工作,而是一個 持續迭代 的過程。隨著專案規模成長,持續優化 Dockerfile、調整資源配置,並將容器安全掃描納入 CI 流程,才能真正發揮 Docker 與 Go 的威力,打造穩定、可擴充的雲端服務。