在雲端運算時代,應用程式的交付方式正在經歷根本性的變革。傳統的虛擬機器雖然提供了良好的隔離性,但其重量級的特性導致資源利用率低下、啟動速度緩慢。容器技術的出現改變了這個格局,它提供了輕量級的應用封裝方式,在保證隔離性的同時大幅提升了資源效率與部署速度。Docker 作為容器技術的代表,已經成為現代應用開發與部署的標準工具。

容器化技術與微服務架構的結合,為複雜應用系統的構建提供了理想的技術基礎。微服務將單體應用拆解為一組小型、自治的服務,每個服務運行在獨立的容器中,透過標準化的 API 進行通訊。這種架構模式帶來了顯著的優勢,包含獨立部署、技術多樣性、彈性擴展與故障隔離。然而,容器的管理與編排也帶來了新的挑戰,如何在數百甚至數千個容器間協調資源、管理網路、處理故障,這些都需要專業的編排工具。

在台灣的軟體產業環境中,從新創公司到大型企業,容器化技術的採用率正在快速提升。電子商務平台使用容器實現快速迭代與彈性擴展,金融科技公司透過容器化確保開發與生產環境的一致性,物聯網應用利用容器在邊緣設備上部署服務。Kubernetes 已經成為容器編排的事實標準,而 Docker Compose、Helm、Istio 等工具則構成了完整的容器生態系統。

本文將從實踐的角度,系統性地探討容器化微服務的完整技術棧。我們將深入分析 Docker 容器技術的原理與應用,展示微服務架構在容器環境中的設計模式,探討 Kubernetes 編排平台的核心能力,並透過實戰案例說明從開發到生產的完整部署流程。

Docker 容器技術基礎

Docker 透過容器技術實現了應用程式的輕量級封裝與隔離。與虛擬機器不同,容器共享主機作業系統的核心,但在處理程序、檔案系統、網路等層面保持隔離。這種設計讓容器的啟動速度達到秒級,資源佔用遠低於虛擬機器。

@startuml
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 16
skinparam minClassWidth 100

package "容器化應用架構" {
  package "應用層" {
    component "Web 應用\n(Nginx/Apache)" as web
    component "後端服務\n(Node.js/Python)" as backend
    component "資料庫\n(PostgreSQL/MongoDB)" as db
  }
  
  package "容器層" {
    component "Web 容器" as web_container
    component "API 容器" as api_container
    component "DB 容器" as db_container
  }
  
  package "Docker 引擎" {
    component "容器運行時\n(containerd)" as runtime
    component "映像管理" as image_mgr
    component "網路管理" as network_mgr
    component "存儲管理" as storage_mgr
  }
  
  component "主機作業系統\n(Linux Kernel)" as os
  component "基礎設施\n(實體/虛擬機器)" as infra
}

web --> web_container
backend --> api_container
db --> db_container

web_container --> runtime
api_container --> runtime
db_container --> runtime

runtime --> image_mgr
runtime --> network_mgr
runtime --> storage_mgr

image_mgr --> os
network_mgr --> os
storage_mgr --> os

os --> infra

note right of web_container
  輕量級隔離
  快速啟動
  資源共享
end note

note bottom of runtime
  容器生命週期管理
  資源限制與配額
  安全隔離機制
end note

@enduml

Docker 映像是容器的基礎,它是一個唯讀的模板,包含了應用程式及其所有依賴。映像採用分層架構,每一層都是前一層的增量修改。這種設計讓映像的構建、分發與存儲都更加高效。基礎映像通常包含作業系統的核心檔案,應用映像則在基礎映像上添加應用程式碼、配置檔案與依賴函式庫。

#!/usr/bin/env python3
"""
Docker 容器管理工具

提供 Docker 容器的建立、管理與監控功能
"""

import subprocess
import json
from typing import List, Dict, Optional
from dataclasses import dataclass
from datetime import datetime

@dataclass
class ContainerInfo:
    """容器資訊"""
    id: str
    name: str
    image: str
    status: str
    ports: Dict[str, str]
    created: str

class DockerManager:
    """Docker 容器管理器"""
    
    def __init__(self):
        self._verify_docker_installed()
    
    def _verify_docker_installed(self) -> bool:
        """驗證 Docker 是否已安裝"""
        try:
            result = subprocess.run(
                ['docker', '--version'],
                capture_output=True,
                text=True,
                check=True
            )
            print(f"Docker 版本: {result.stdout.strip()}")
            return True
        except (subprocess.CalledProcessError, FileNotFoundError):
            print("錯誤: Docker 未安裝或無法執行")
            return False
    
    def pull_image(self, image_name: str, tag: str = 'latest') -> bool:
        """
        拉取 Docker 映像
        
        Args:
            image_name: 映像名稱
            tag: 映像標籤
            
        Returns:
            是否成功
        """
        full_image = f"{image_name}:{tag}"
        print(f"拉取映像: {full_image}")
        
        try:
            subprocess.run(
                ['docker', 'pull', full_image],
                check=True
            )
            print(f"映像 {full_image} 拉取成功")
            return True
        
        except subprocess.CalledProcessError as e:
            print(f"映像拉取失敗: {e}")
            return False
    
    def list_images(self) -> List[Dict]:
        """
        列出本地映像
        
        Returns:
            映像列表
        """
        try:
            result = subprocess.run(
                ['docker', 'images', '--format', '{{json .}}'],
                capture_output=True,
                text=True,
                check=True
            )
            
            images = []
            for line in result.stdout.strip().split('\n'):
                if line:
                    image_data = json.loads(line)
                    images.append({
                        'repository': image_data.get('Repository'),
                        'tag': image_data.get('Tag'),
                        'image_id': image_data.get('ID'),
                        'created': image_data.get('CreatedAt'),
                        'size': image_data.get('Size')
                    })
            
            return images
        
        except subprocess.CalledProcessError as e:
            print(f"獲取映像列表失敗: {e}")
            return []
    
    def create_container(
        self,
        image: str,
        name: Optional[str] = None,
        ports: Optional[Dict[str, str]] = None,
        env: Optional[Dict[str, str]] = None,
        volumes: Optional[Dict[str, str]] = None,
        detach: bool = True
    ) -> Optional[str]:
        """
        建立並啟動容器
        
        Args:
            image: 映像名稱
            name: 容器名稱
            ports: 埠映射 {'8080': '80'}
            env: 環境變數
            volumes: 卷掛載 {'/host/path': '/container/path'}
            detach: 是否後台運行
            
        Returns:
            容器 ID
        """
        cmd = ['docker', 'run']
        
        if detach:
            cmd.append('-d')
        
        if name:
            cmd.extend(['--name', name])
        
        if ports:
            for host_port, container_port in ports.items():
                cmd.extend(['-p', f"{host_port}:{container_port}"])
        
        if env:
            for key, value in env.items():
                cmd.extend(['-e', f"{key}={value}"])
        
        if volumes:
            for host_path, container_path in volumes.items():
                cmd.extend(['-v', f"{host_path}:{container_path}"])
        
        cmd.append(image)
        
        try:
            result = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                check=True
            )
            
            container_id = result.stdout.strip()
            print(f"容器已建立: {container_id[:12]}")
            return container_id
        
        except subprocess.CalledProcessError as e:
            print(f"容器建立失敗: {e}")
            return None
    
    def list_containers(self, all_containers: bool = False) -> List[ContainerInfo]:
        """
        列出容器
        
        Args:
            all_containers: 是否包含已停止的容器
            
        Returns:
            容器資訊列表
        """
        cmd = ['docker', 'ps', '--format', '{{json .}}']
        
        if all_containers:
            cmd.append('-a')
        
        try:
            result = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                check=True
            )
            
            containers = []
            for line in result.stdout.strip().split('\n'):
                if line:
                    data = json.loads(line)
                    
                    # 解析埠映射
                    ports = {}
                    ports_str = data.get('Ports', '')
                    if ports_str:
                        # 簡化的埠解析
                        for port_mapping in ports_str.split(','):
                            if '->' in port_mapping:
                                parts = port_mapping.split('->')
                                if len(parts) == 2:
                                    host_port = parts[0].strip()
                                    container_port = parts[1].strip()
                                    ports[container_port] = host_port
                    
                    container = ContainerInfo(
                        id=data.get('ID', ''),
                        name=data.get('Names', ''),
                        image=data.get('Image', ''),
                        status=data.get('Status', ''),
                        ports=ports,
                        created=data.get('CreatedAt', '')
                    )
                    
                    containers.append(container)
            
            return containers
        
        except subprocess.CalledProcessError as e:
            print(f"獲取容器列表失敗: {e}")
            return []
    
    def stop_container(self, container_id: str) -> bool:
        """
        停止容器
        
        Args:
            container_id: 容器 ID 或名稱
            
        Returns:
            是否成功
        """
        try:
            subprocess.run(
                ['docker', 'stop', container_id],
                check=True
            )
            print(f"容器 {container_id} 已停止")
            return True
        
        except subprocess.CalledProcessError as e:
            print(f"停止容器失敗: {e}")
            return False
    
    def remove_container(self, container_id: str, force: bool = False) -> bool:
        """
        刪除容器
        
        Args:
            container_id: 容器 ID 或名稱
            force: 是否強制刪除
            
        Returns:
            是否成功
        """
        cmd = ['docker', 'rm']
        
        if force:
            cmd.append('-f')
        
        cmd.append(container_id)
        
        try:
            subprocess.run(cmd, check=True)
            print(f"容器 {container_id} 已刪除")
            return True
        
        except subprocess.CalledProcessError as e:
            print(f"刪除容器失敗: {e}")
            return False
    
    def get_container_logs(
        self,
        container_id: str,
        tail: int = 100,
        follow: bool = False
    ) -> str:
        """
        獲取容器日誌
        
        Args:
            container_id: 容器 ID 或名稱
            tail: 顯示最後幾行
            follow: 是否持續追蹤
            
        Returns:
            日誌內容
        """
        cmd = ['docker', 'logs']
        
        if tail:
            cmd.extend(['--tail', str(tail)])
        
        if follow:
            cmd.append('-f')
        
        cmd.append(container_id)
        
        try:
            result = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                check=True
            )
            
            return result.stdout
        
        except subprocess.CalledProcessError as e:
            print(f"獲取日誌失敗: {e}")
            return ""
    
    def get_container_stats(self, container_id: str) -> Dict:
        """
        獲取容器資源使用統計
        
        Args:
            container_id: 容器 ID 或名稱
            
        Returns:
            統計資訊
        """
        try:
            result = subprocess.run(
                ['docker', 'stats', '--no-stream', '--format', '{{json .}}', container_id],
                capture_output=True,
                text=True,
                check=True
            )
            
            if result.stdout:
                stats = json.loads(result.stdout.strip())
                return {
                    'cpu_percent': stats.get('CPUPerc', '0%'),
                    'memory_usage': stats.get('MemUsage', '0B / 0B'),
                    'memory_percent': stats.get('MemPerc', '0%'),
                    'network_io': stats.get('NetIO', '0B / 0B'),
                    'block_io': stats.get('BlockIO', '0B / 0B')
                }
            
            return {}
        
        except subprocess.CalledProcessError as e:
            print(f"獲取統計資訊失敗: {e}")
            return {}
    
    def create_network(
        self,
        network_name: str,
        driver: str = 'bridge',
        subnet: Optional[str] = None
    ) -> bool:
        """
        建立 Docker 網路
        
        Args:
            network_name: 網路名稱
            driver: 網路驅動 (bridge/overlay/host)
            subnet: 子網路 (例如: 172.18.0.0/16)
            
        Returns:
            是否成功
        """
        cmd = ['docker', 'network', 'create', '--driver', driver]
        
        if subnet:
            cmd.extend(['--subnet', subnet])
        
        cmd.append(network_name)
        
        try:
            subprocess.run(cmd, check=True)
            print(f"網路 {network_name} 已建立")
            return True
        
        except subprocess.CalledProcessError as e:
            print(f"建立網路失敗: {e}")
            return False
    
    def connect_to_network(
        self,
        container_id: str,
        network_name: str
    ) -> bool:
        """
        將容器連接到網路
        
        Args:
            container_id: 容器 ID 或名稱
            network_name: 網路名稱
            
        Returns:
            是否成功
        """
        try:
            subprocess.run(
                ['docker', 'network', 'connect', network_name, container_id],
                check=True
            )
            print(f"容器 {container_id} 已連接到網路 {network_name}")
            return True
        
        except subprocess.CalledProcessError as e:
            print(f"連接網路失敗: {e}")
            return False

# 使用範例
if __name__ == "__main__":
    print("=== Docker 容器管理示範 ===\n")
    
    # 初始化管理器
    docker_mgr = DockerManager()
    
    # 列出本地映像
    print("[本地映像列表]\n")
    images = docker_mgr.list_images()
    
    if images:
        for img in images[:5]:
            print(f"{img['repository']:30s} {img['tag']:15s} {img['size']:15s}")
    else:
        print("無映像")
    
    # 拉取映像(示範)
    print("\n\n[拉取 Nginx 映像]\n")
    print("執行: docker pull nginx:alpine")
    
    # 建立網路
    print("\n\n[建立自訂網路]\n")
    docker_mgr.create_network(
        'my_app_network',
        driver='bridge',
        subnet='172.18.0.0/16'
    )
    
    # 建立容器(示範)
    print("\n\n[建立 Nginx 容器]\n")
    print("執行: docker run -d --name my_nginx -p 8080:80 nginx:alpine")
    
    # 列出運行中的容器
    print("\n\n[運行中的容器]\n")
    containers = docker_mgr.list_containers()
    
    if containers:
        for container in containers:
            print(f"ID: {container.id[:12]}")
            print(f"名稱: {container.name}")
            print(f"映像: {container.image}")
            print(f"狀態: {container.status}")
            if container.ports:
                print(f"埠: {container.ports}")
            print()
    else:
        print("無運行中的容器")

這個 Docker 管理工具展示了容器的完整生命週期管理。在實務應用中,通常會搭配 Docker Compose 進行多容器應用的編排,或使用 Kubernetes 進行大規模容器集群的管理。

Docker Compose 多容器編排

當應用系統包含多個容器時,手動管理每個容器的啟動、網路連接與配置會變得繁瑣且容易出錯。Docker Compose 提供了聲明式的配置方式,透過 YAML 檔案定義整個應用的容器組成、網路與卷配置,一條命令即可啟動完整的應用環境。

# docker-compose.yml 完整範例
version: '3.8'

services:
  # Web 前端服務
  web:
    image: nginx:alpine
    container_name: helpdesk_web
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./static:/usr/share/nginx/html:ro
      - ./ssl:/etc/nginx/ssl:ro
    networks:
      - frontend
    depends_on:
      - api
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/health"]
      interval: 30s
      timeout: 3s
      retries: 3
  
  # API 後端服務
  api:
    build:
      context: ./api
      dockerfile: Dockerfile
    container_name: helpdesk_api
    ports:
      - "8080:8080"
    environment:
      - DATABASE_URL=postgresql://postgres:password@db:5432/helpdesk
      - REDIS_URL=redis://cache:6379
      - JWT_SECRET=${JWT_SECRET}
      - API_KEY=${API_KEY}
    volumes:
      - ./api:/app
      - api_logs:/var/log/api
    networks:
      - frontend
      - backend
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 3s
      retries: 3
  
  # PostgreSQL 資料庫
  db:
    image: postgres:14-alpine
    container_name: helpdesk_db
    environment:
      - POSTGRES_DB=helpdesk
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=password
    volumes:
      - db_data:/var/lib/postgresql/data
      - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
    networks:
      - backend
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
  
  # Redis 快取服務
  cache:
    image: redis:7-alpine
    container_name: helpdesk_cache
    command: redis-server --appendonly yes
    volumes:
      - cache_data:/data
    networks:
      - backend
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true

volumes:
  db_data:
    driver: local
  cache_data:
    driver: local
  api_logs:
    driver: local

這個 Docker Compose 配置展示了一個典型的 Web 應用架構。前端 Nginx 服務對外提供 HTTP/HTTPS 存取,後端 API 服務處理業務邏輯,PostgreSQL 存儲持久化資料,Redis 提供快取支援。透過網路隔離,資料庫與快取服務僅能被後端 API 存取,增強了安全性。

Kubernetes 容器編排平台

Kubernetes 是雲端原生時代的容器編排標準,它抽象了底層基礎設施的複雜性,提供了聲明式的 API 來管理容器化應用。在 Kubernetes 的世界中,Pod 是最小的部署單元,通常包含一個或多個緊密耦合的容器。Service 提供穩定的網路端點,Deployment 管理 Pod 的生命週期與滾動更新。

@startuml
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 16
skinparam minClassWidth 100

package "Kubernetes 集群架構" {
  package "控制平面" {
    component "API Server\n統一入口" as apiserver
    component "Scheduler\n調度器" as scheduler
    component "Controller Manager\n控制器管理" as controller
    component "etcd\n配置存儲" as etcd
  }
  
  package "工作節點 1" {
    component "Kubelet\n節點代理" as kubelet1
    component "Kube-proxy\n網路代理" as proxy1
    
    package "Pod 1" {
      component "容器 A" as c1a
      component "容器 B" as c1b
    }
  }
  
  package "工作節點 2" {
    component "Kubelet" as kubelet2
    component "Kube-proxy" as proxy2
    
    package "Pod 2" {
      component "容器 C" as c2a
    }
  }
}

cloud "外部流量" as external

external --> apiserver
apiserver --> etcd
apiserver --> scheduler
apiserver --> controller

scheduler --> kubelet1
scheduler --> kubelet2

controller --> kubelet1
controller --> kubelet2

kubelet1 --> c1a
kubelet1 --> c1b
kubelet2 --> c2a

note right of apiserver
  REST API 端點
  認證與授權
  請求驗證
end note

note bottom of scheduler
  Pod 調度決策
  資源分配
  節點選擇
end note

@enduml

Kubernetes 的強大之處在於其自我修復能力。當容器崩潰時,Kubelet 會自動重啟容器。當節點故障時,Controller Manager 會在其他健康節點上重新調度 Pod。這種內建的韌性機制大幅降低了維運負擔。

總結

容器化技術與微服務架構的結合,為現代應用系統的構建提供了強大的技術基礎。Docker 簡化了應用的封裝與分發,Kubernetes 實現了大規模容器的自動化管理。在台灣的軟體產業環境中,掌握容器技術已成為雲端原生開發的必備技能。

然而,容器化並非適用於所有場景。對於小型應用或傳統系統,容器化帶來的複雜性可能超過其收益。組織需要評估自身的技術能力、團隊規模與業務需求,選擇適合的技術路線。容器安全、網路性能、存儲管理等問題也需要持續關注。