21.7 實戰案例:Go/Rust/資料庫/微服務

本節透過實際專案案例示範如何為不同型別的應用建立最佳化的 Docker 映象,以及如何使用 Docker Compose 建立完整的開發和生產環境。

21.7.1 Go 應用的最小化映象建立

Go 語言因其編譯為靜態二進位和快速啟動而特別適合容器化。以下展示如何建立極小的 Go 應用映象。

超小 Go Web 服務

應用程式碼(main.go):

package main

import (
	"fmt"
	"log"
	"net/http"
	"os"
)

func healthHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	fmt.Fprintf(w, `{"status":"healthy","version":"1.0.0"}`)
}

func helloHandler(w http.ResponseWriter, r *http.Request) {
	hostname, _ := os.Hostname()
	w.Header().Set("Content-Type", "application/json")
	fmt.Fprintf(w, `{"message":"Hello from %s","version":"1.0.0"}`, hostname)
}

func main() {
	http.HandleFunc("/health", healthHandler)
	http.HandleFunc("/hello", helloHandler)
	http.HandleFunc("/", helloHandler)

	port := ":8080"
	log.Printf("Server starting on %s", port)

	if err := http.ListenAndServe(port, nil); err != nil {
		log.Fatalf("Server failed: %v", err)
	}
}

多階段 Dockerfile:

# Stage 1: 建立階段
FROM golang:1.26-alpine AS builder

WORKDIR /build

# 安裝建立依賴
RUN apk add --no-cache git ca-certificates tzdata

# 複製模組檔案(利用快取)
COPY go.mod go.sum ./
RUN go mod download

# 複製原始碼
COPY . .

# 建立靜態二進位
# -ldflags="-w -s" 去除除錯符號減小體積
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
    -a -installsuffix cgo \
    -ldflags="-w -s -X main.Version=1.0.0 -X main.BuildTime=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \
    -o app .

# Stage 2: 執行階段(scratch 映象)
FROM scratch

# 複製 CA 證書(用於 HTTPS)
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

# 複製時區資料(用於時間處理)
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo

# 複製應用二進位
COPY --from=builder /build/app /app

EXPOSE 8080

# 使用絕對路徑作為 ENTRYPOINT
ENTRYPOINT ["/app"]

建立和測試:

# 建立映象
docker build -t go-app:latest .

# 檢查映象大小
docker images go-app

# 執行容器
docker run -d -p 8080:8080 --name go-demo go-app:latest

# 測試應用
curl http://localhost:8080/health | jq .

# 進入容器驗證
docker exec go-demo ls -la /

# 只包含 /app 和系統必要檔案

# 映象大小通常 < 10MB(相比 golang:1.26 基礎映象的 ~900MB)
docker history go-app:latest

go.mod 和 go.sum 範例:

module github.com/example/go-app

go 1.26

require (
    // 如果需要依賴
)

帶依賴的 Go 應用

應用程式碼(使用 Gin 框架):

package main

import (
	"github.com/gin-gonic/gin"
	"log"
)

func main() {
	router := gin.Default()

	router.GET("/health", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"status": "ok",
		})
	})

	router.GET("/api/users", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"users": []string{"alice", "bob"},
		})
	})

	log.Fatal(router.Run(":8080"))
}

最佳化的 Dockerfile:

FROM golang:1.26-alpine AS builder

WORKDIR /src

RUN apk add --no-cache git ca-certificates tzdata

COPY go.mod go.sum ./
RUN go mod download

COPY . .

RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
    go build -a -installsuffix cgo \
    -ldflags="-w -s" \
    -o app .

# 最終映象
FROM alpine:3.21

RUN apk add --no-cache ca-certificates tzdata

WORKDIR /root/

COPY --from=builder /src/app .

EXPOSE 8080

CMD ["./app"]

21.7.2 Rust 應用的最小化映象建立

Rust 因其效能和安全性在系統級應用中備受青睞。

應用程式碼(main.rs):

use actix_web::{web, App, HttpServer, HttpResponse};
use std::sync::Mutex;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    println!("Starting server on 0.0.0.0:8080");

    HttpServer::new(|| {
        App::new()
            .route("/health", web::get().to(health))
            .route("/hello", web::get().to(hello))
    })
    .bind("0.0.0.0:8080")?
    .run()
    .await
}

async fn health() -> HttpResponse {
    HttpResponse::Ok().json(serde_json::json!({
        "status": "healthy"
    }))
}

async fn hello() -> HttpResponse {
    HttpResponse::Ok().json(serde_json::json!({
        "message": "Hello from Rust"
    }))
}

Cargo.toml:

[package]
name = "rust-app"
version = "0.1.0"
edition = "2021"

[[bin]]
name = "rust-app"
path = "src/main.rs"

[dependencies]
actix-web = "4.13"
tokio = { version = "1.35", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

多階段建立 Dockerfile:

# Stage 1: 編譯
FROM rust:1.95-alpine AS builder

RUN apk add --no-cache musl-dev

WORKDIR /src

COPY Cargo.* ./
COPY src ./src

# 建立最佳化的發布版本
RUN cargo build --release

# Stage 2: 執行映象
FROM alpine:3.21

RUN apk add --no-cache ca-certificates

COPY --from=builder /src/target/release/rust-app /app

EXPOSE 8080

CMD ["/app"]

建立和驗證:

docker build -t rust-app:latest .
docker run -d -p 8080:8080 rust-app:latest
curl http://localhost:8080/health | jq .

# Rust 應用通常比 Go 更小:5-20MB(取決於依賴)
docker images rust-app

21.7.3 資料庫容器化最佳實踐

PostgreSQL 生產部署

自定義 PostgreSQL 映象:

FROM postgres:16-alpine

# 安裝額外工具
RUN apk add --no-cache \
    postgresql-contrib \
    pg-stat-monitor \
    curl

# 複製初始化指令碼(.sh 形式可從環境變數讀取密碼,避免在 SQL 中寫明文)
COPY init-db.sh /docker-entrypoint-initdb.d/
COPY health-check.sh /

RUN chmod +x /health-check.sh

HEALTHCHECK --interval=10s --timeout=5s --start-period=40s --retries=3 \
    CMD /health-check.sh

EXPOSE 5432

初始化指令碼(init-db.sh):

官方映象會執行 /docker-entrypoint-initdb.d/ 下的 .sh 指令碼,因此應用使用者的密碼可以從環境變數注入,而不必寫進 SQL;資料庫 myappdb 已由入口指令碼按 POSTGRES_DB 建立,初始化指令碼中不要重複 CREATE DATABASE(否則首次初始化會因衝突而中止)。

#!/bin/bash
set -e

psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" <<-EOSQL
    -- 建立應用使用者(密碼來自環境變數 APP_DB_PASSWORD)
    CREATE USER appuser WITH PASSWORD '$APP_DB_PASSWORD';

    -- 建立擴充套件
    CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
    CREATE EXTENSION IF NOT EXISTS hstore;
    CREATE EXTENSION IF NOT EXISTS pg_trgm;

    -- 建立表
    CREATE TABLE users (
        id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
        username VARCHAR(255) NOT NULL UNIQUE,
        email VARCHAR(255) NOT NULL UNIQUE,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    );

    -- 建立索引
    CREATE INDEX idx_users_username ON users (username);
    CREATE INDEX idx_users_email ON users (email);

    -- 授予許可權
    GRANT CONNECT ON DATABASE $POSTGRES_DB TO appuser;
    GRANT USAGE ON SCHEMA public TO appuser;
    GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO appuser;
    GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO appuser;
EOSQL

健康檢查指令碼(health-check.sh):

#!/bin/bash

PGPASSWORD=$POSTGRES_PASSWORD pg_isready \
  -h localhost \
  -U $POSTGRES_USER \
  -d $POSTGRES_DB \
  -p 5432 > /dev/null 2>&1

exit $?

Docker Compose 設定:

services:
  postgres:
    build:
      context: .
      dockerfile: Dockerfile.postgres
    container_name: postgres-db
    environment:
      POSTGRES_DB: myappdb
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
      APP_DB_PASSWORD: ${APP_DB_PASSWORD:?set APP_DB_PASSWORD in .env}
      POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=en_US.UTF-8"
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./backups:/backups
    ports:
      # 只暴露給本機除錯;生產環境優先不發布資料庫連接埠
      - "127.0.0.1:5432:5432"
    networks:
      - backend
    restart: unless-stopped
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  # 備份服務
  backup:
    image: postgres:16-alpine
    depends_on:
      - postgres
    environment:
      PGPASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
    volumes:
      - ./backups:/backups
    command: |
      sh -c 'while true; do
        pg_dump -h postgres -U postgres -d myappdb > /backups/backup_$$(date +%Y%m%d_%H%M%S).sql
        echo "Backup completed at $$(date)"
        sleep 86400
      done'
    networks:
      - backend

volumes:
  postgres_data:
    driver: local

networks:
  backend:
    driver: bridge

效能最佳化設定:

services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: myappdb
    command:
      - "postgres"
      - "-c"
      - "max_connections=200"
      - "-c"
      - "shared_buffers=256MB"
      - "-c"
      - "effective_cache_size=1GB"
      - "-c"
      - "maintenance_work_mem=64MB"
      - "-c"
      - "checkpoint_completion_target=0.9"
      - "-c"
      - "wal_buffers=16MB"
      - "-c"
      - "default_statistics_target=100"
      - "-c"
      - "random_page_cost=1.1"
      - "-c"
      - "effective_io_concurrency=200"
      - "-c"
      - "work_mem=1310kB"
      - "-c"
      - "min_wal_size=1GB"
      - "-c"
      - "max_wal_size=4GB"
      - "-c"
      - "max_worker_processes=4"
      - "-c"
      - "max_parallel_workers_per_gather=2"
      - "-c"
      - "max_parallel_workers=4"
      - "-c"
      - "max_parallel_maintenance_workers=2"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

volumes:
  postgres_data:

MySQL/MariaDB 部署

FROM mariadb:11

# 複製自定義設定
COPY my.cnf /etc/mysql/conf.d/custom.cnf

# 初始化指令碼
COPY init.sql /docker-entrypoint-initdb.d/

EXPOSE 3306

HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
    CMD mariadb-admin ping -h localhost || exit 1

自定義 my.cnf:

[mysqld]

# 效能最佳化
max_connections = 200
default_storage_engine = InnoDB
innodb_buffer_pool_size = 1GB
innodb_log_file_size = 256MB
query_cache_type = 0
query_cache_size = 0

# 日誌設定
log_error = /var/log/mysql/error.log
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2

# 複製設定
server_id = 1
log_bin = mysql-bin
binlog_format = ROW

Redis 快取部署

FROM redis:8-alpine

# 複製 Redis 設定
COPY redis.conf /usr/local/etc/redis/redis.conf

# 使用設定檔案啟動
CMD ["redis-server", "/usr/local/etc/redis/redis.conf"]

EXPOSE 6379

# redis.conf 啟用了 requirepass,健康檢查必須帶認證,否則只會收到 NOAUTH 錯誤
# REDISCLI_AUTH 可避免把密碼出現在程序引數中
HEALTHCHECK --interval=5s --timeout=3s --retries=5 \
    CMD sh -c 'REDISCLI_AUTH="$(awk "/^requirepass /{print \$2}" /usr/local/etc/redis/redis.conf)" redis-cli ping | grep -q PONG'

redis.conf 設定:

# 繫結地址
bind 0.0.0.0

# 連接埠
port 6379

# 密碼保護(範例佔位值,部署時應替換並避免送出到版本庫)
requirepass your_secure_password

# 記憶體管理
maxmemory 512mb
maxmemory-policy allkeys-lru

# 持久化
save 900 1
save 300 10
save 60 10000

# AOF 持久化
appendonly yes
appendfsync everysec

# 日誌
loglevel notice
logfile ""

# 用戶端輸出緩衝限制
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit replica 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60

21.7.4 微服務架構的 Docker Compose 編排

三層微服務架構範例:

下面範例聚焦服務拓撲、網路、健康檢查和資源限制。為便於閱讀,密碼仍透過環境變數串接;生產環境應改用 Compose secrets、外部金鑰系統或雲廠商金鑰服務,並避免把敏感值寫入 DATABASE_URL / REDIS_URL

services:
  # 前端服務
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      REACT_APP_API_URL: http://localhost:8000
      NODE_ENV: production
    depends_on:
      - api
    networks:
      - frontend-network
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000"]
      interval: 30s
      timeout: 10s
      retries: 3

  # API 服務
  api:
    build:
      context: ./api
      dockerfile: Dockerfile
    ports:
      - "8000:8000"
    environment:
      DATABASE_URL: postgresql://appuser:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}@postgres:5432/myappdb
      REDIS_URL: redis://:${REDIS_PASSWORD:?set REDIS_PASSWORD}@redis:6379
      LOG_LEVEL: info
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - frontend-network
      - backend-network
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
    deploy:
      resources:
        limits:
          cpus: '1'
          memory: 512M
        reservations:
          cpus: '0.5'
          memory: 256M

  # PostgreSQL 資料庫
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: myappdb
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
      # 此處的 init.sql 只放建表/索引等 schema 語句;
      # 使用者與資料庫已由 POSTGRES_USER/POSTGRES_DB 建立,指令碼中不要重複 CREATE USER/DATABASE
      - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql
    networks:
      - backend-network
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d myappdb"]
      interval: 10s
      timeout: 5s
      retries: 5

  # Redis 快取
  redis:
    image: redis:8-alpine
    command: ["sh", "-c", "redis-server --appendonly yes --requirepass \"$$REDIS_PASSWORD\""]
    environment:
      REDIS_PASSWORD: ${REDIS_PASSWORD:?set REDIS_PASSWORD}
    volumes:
      - redis_data:/data
    networks:
      - backend-network
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "redis-cli -a \"$$REDIS_PASSWORD\" ping | grep -q PONG"]
      interval: 10s
      timeout: 5s
      retries: 5

  # Nginx 反向代理
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./ssl:/etc/nginx/ssl:ro
    depends_on:
      - frontend
      - api
    networks:
      - frontend-network
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/health"]
      interval: 30s
      timeout: 10s
      retries: 3

volumes:
  postgres_data:
    driver: local
  redis_data:
    driver: local

networks:
  frontend-network:
    driver: bridge
  backend-network:
    driver: bridge

nginx.conf 設定:

upstream frontend {
    server frontend:3000;
}

upstream api {
    server api:8000;
}

server {
    listen 80;
    server_name localhost;
    client_max_body_size 100M;

    # 健康檢查端點
    location /health {
        access_log off;
        return 200 "OK\n";
        add_header Content-Type text/plain;
    }

    # 前端應用
    location / {
        proxy_pass http://frontend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # API 介面
    location /api/ {
        proxy_pass http://api/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_redirect off;

        # WebSocket 支援
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    # 靜態資源快取
    location ~* ^.+\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
        proxy_pass http://frontend;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
}

21.7.5 使用 VS Code Dev Containers

Dev Containers 讓整個開發環境容器化,提升團隊一致性。

.devcontainer/devcontainer.json:

{
  "name": "Python Dev Environment",
  "image": "mcr.microsoft.com/devcontainers/python:3.14",

  "features": {
    "ghcr.io/devcontainers/features/docker-in-docker:2": {},
    "ghcr.io/devcontainers/features/git:1": {}
  },

  "customizations": {
    "vscode": {
      "extensions": [
        "ms-python.python",
        "ms-python.vscode-pylance",
        "ms-python.pylint",
        "charliermarsh.ruff",
        "ms-vscode-remote.remote-containers"
      ],
      "settings": {
        "python.linting.enabled": true,
        "python.linting.pylintEnabled": true,
        "python.formatting.provider": "black",
        "[python]": {
          "editor.formatOnSave": true,
          "editor.defaultFormatter": "ms-python.python"
        }
      }
    }
  },

  "postCreateCommand": "pip install -r requirements.txt && pip install pytest black pylint",

  "forwardPorts": [8000, 5432, 6379],
  "portsAttributes": {
    "8000": {
      "label": "Application",
      "onAutoForward": "notify"
    },
    "5432": {
      "label": "PostgreSQL",
      "onAutoForward": "ignore"
    },
    "6379": {
      "label": "Redis",
      "onAutoForward": "ignore"
    }
  },

  "mounts": [
    "source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,readonly"
  ],

  "remoteUser": "vscode"
}

.devcontainer/Dockerfile:

FROM mcr.microsoft.com/devcontainers/python:3.14

# 安裝額外工具
RUN apt-get update && apt-get install -y \
    postgresql-client \
    redis-tools \
    curl \
    git \
    && rm -rf /var/lib/apt/lists/*

# 建立虛擬環境
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

WORKDIR /workspace

Docker Compose 用於 Dev Containers:

# .devcontainer/docker-compose.yml
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    environment:
      DATABASE_URL: postgresql://dev:dev@postgres:5432/myapp
      REDIS_URL: redis://redis:6379
    volumes:
      - ..:/workspace:cached
    ports:
      - "8000:8000"
    depends_on:
      - postgres
      - redis

  postgres:
    image: postgres:16-alpine
    environment:
      # 僅限本機 Dev Container 的一次性開發憑證,不要在任何共享/聯網環境複用
      POSTGRES_USER: dev
      POSTGRES_PASSWORD: dev
      POSTGRES_DB: myapp
    volumes:
      - postgres_data:/var/lib/postgresql/data

  redis:
    image: redis:8-alpine

volumes:
  postgres_data:
第 173 页,共 196 页
使用 mdPress 构建