第二十八章:CI/CD 与 Docker 部署

博客v1.0系列教程(Dioxus)博客 v1.0 系列教程 (Dioxus)

第二十八章:CI/CD 与 Docker 部署

1. 部署架构

┌──────────────┐    ┌──────────────┐    ┌──────────────┐
│  开发者推送   │ → │  GitHub Actions│ → │  Docker Hub   │
│  git push    │    │  CI/CD 流水线 │    │  镜像仓库     │
└──────────────┘    └──────┬───────┘    └──────┬───────┘
                           │                    │
                           ▼                    ▼
                    ┌──────────────┐    ┌──────────────┐
                    │  代码检查     │    │  生产服务器   │
                    │  单元测试     │    │  docker pull  │
                    │  构建编译     │    │  docker run   │
                    └──────────────┘    └──────────────┘

2. Dockerfile

2.1 多阶段构建

# ---- 构建阶段 ----
FROM rust:1.80-slim-bookworm AS builder

RUN apt-get update && apt-get install -y \
    pkg-config libssl-dev curl \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# 先复制 Cargo 文件,利用 Docker 缓存
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release --features server 2>/dev/null || true
RUN rm -rf src

# 复制真实源码
COPY . .

# 构建
RUN cargo build --release --features server

# ---- 运行阶段 ----
FROM debian:bookworm-slim

RUN apt-get update && apt-get install -y \
    ca-certificates curl \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# 复制构建产物
COPY --from=builder /app/target/release/rabitlogic-blog .
COPY --from=builder /app/templates ./templates
COPY --from=builder /app/assets ./assets
COPY --from=builder /app/articles ./articles

# 健康检查
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
    CMD curl -f http://localhost:5051/health || exit 1

EXPOSE 5051

CMD ["./rabitlogic-blog"]

2.2 docker-compose.yml

version: "3.8"

services:
  blog:
    build: .
    container_name: rabitlogic-blog
    ports:
      - "5051:5051"
    environment:
      - SERVER_HOST=0.0.0.0
      - SERVER_PORT=5051
      - DATABASE_URL=postgres://blog:password@db:5432/blog
      - JWT_SECURITY_KEY=${JWT_SECURITY_KEY}
      - RUST_LOG=info
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    container_name: blog-db
    environment:
      POSTGRES_USER: blog
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: blog
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U blog"]
      interval: 5s
      timeout: 3s
    restart: unless-stopped

  nginx:
    image: nginx:alpine
    container_name: blog-nginx
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./ssl:/etc/nginx/ssl:ro
    depends_on:
      - blog
    restart: unless-stopped

volumes:
  pgdata:

2.3 Nginx 配置

# nginx.conf
events {}

http {
    upstream blog_app {
        server blog:5051;
    }

    server {
        listen 80;
        server_name blog.example.com;
        return 301 https://$server_name$request_uri;
    }

    server {
        listen 443 ssl http2;
        server_name blog.example.com;

        ssl_certificate /etc/nginx/ssl/cert.pem;
        ssl_certificate_key /etc/nginx/ssl/key.pem;

        # 静态资源缓存
        location /assets/ {
            proxy_pass http://blog_app;
            expires 30d;
            add_header Cache-Control "public, immutable";
        }

        # API 请求
        location /api/ {
            proxy_pass http://blog_app;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }

        # 页面请求
        location / {
            proxy_pass http://blog_app;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
    }
}

3. GitHub Actions

3.1 CI 流水线

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

env:
  CARGO_TERM_COLOR: always

jobs:
  check:
    name: Code Check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Rust
        uses: dtolnay/rust-toolchain@stable
        with:
          components: rustfmt, clippy

      - name: Cache dependencies
        uses: Swatinem/rust-cache@v2

      - name: Format check
        run: cargo fmt --check

      - name: Clippy
        run: cargo clippy --features server -- -D warnings

      - name: Build
        run: cargo build --features server

      - name: Test
        run: cargo test --features server

3.2 CD 流水线

# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]
    paths-ignore:
      - "articles/**"
      - "docs/**"
      - "*.md"

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  deploy:
    name: Build & Deploy
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - uses: actions/checkout@v4

      - name: Login to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and Push Docker Image
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Deploy to Server
        uses: appleboy/ssh-action@v1.0
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /opt/rabitlogic-blog
            docker compose pull
            docker compose up -d --force-recreate blog
            docker image prune -f

4. 环境配置管理

4.1 多环境配置

// config.rs
#[derive(Clone)]
struct AppConfig {
    pub database_url: String,
    pub jwt_security_key: String,
    pub jwt_expires_hours: i64,
    pub server_host: String,
    pub server_port: u16,
    pub env: Environment,
}

enum Environment {
    Development,
    Staging,
    Production,
}

impl AppConfig {
    fn from_env() -> Self {
        let env = match std::env::var("APP_ENV")
            .unwrap_or_else(|_| "development".into())
            .as_str()
        {
            "production" => Environment::Production,
            "staging" => Environment::Staging,
            _ => Environment::Development,
        };

        Self {
            database_url: std::env::var("DATABASE_URL").expect("DATABASE_URL 必须设置"),
            jwt_security_key: std::env::var("JWT_SECURITY_KEY")
                .expect("JWT_SECURITY_KEY 必须设置"),
            jwt_expires_hours: std::env::var("JWT_EXPIRES_HOURS")
                .ok().and_then(|v| v.parse().ok()).unwrap_or(24),
            server_host: std::env::var("SERVER_HOST")
                .unwrap_or_else(|_| "0.0.0.0".into()),
            server_port: std::env::var("SERVER_PORT")
                .ok().and_then(|v| v.parse().ok()).unwrap_or(5051),
            env,
        }
    }
}

4.2 .env 文件

# .env.development
DATABASE_URL=postgres://blog:password@localhost:5432/blog
JWT_SECURITY_KEY=dev-secret-key
SERVER_HOST=127.0.0.1
SERVER_PORT=5051
RUST_LOG=debug
# .env.production
DATABASE_URL=postgres://blog:${DB_PASSWORD}@db:5432/blog
JWT_SECURITY_KEY=${JWT_SECURITY_KEY}
SERVER_HOST=0.0.0.0
SERVER_PORT=5051
RUST_LOG=info

5. 数据库迁移

-- migrations/001_init.sql
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    username VARCHAR(50) UNIQUE NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    role VARCHAR(20) DEFAULT 'reader',
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE articles (
    id BIGSERIAL PRIMARY KEY,
    slug VARCHAR(255) UNIQUE NOT NULL,
    title VARCHAR(255) NOT NULL,
    body TEXT NOT NULL,
    author_id BIGINT REFERENCES users(id),
    category VARCHAR(100),
    tags TEXT[],
    published BOOLEAN DEFAULT false,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- 更多表...

6. 监控与日志

6.1 日志收集

// 使用 tracing 进行结构化日志
use tracing_subscriber::{EnvFilter, fmt};

fn init_logging(env: &Environment) {
    let filter = match env {
        Environment::Production => EnvFilter::new("info"),
        Environment::Staging => EnvFilter::new("debug"),
        Environment::Development => EnvFilter::new("debug"),
    };

    fmt()
        .with_env_filter(filter)
        .json()  // JSON 格式,方便日志收集
        .init();
}

6.2 健康检查端点

async fn health_check() -> impl axum::response::IntoResponse {
    use axum::http::StatusCode;
    use axum::Json;

    // 检查数据库连接
    let db_ok = check_db().await.is_ok();

    let status = if db_ok {
        StatusCode::OK
    } else {
        StatusCode::SERVICE_UNAVAILABLE
    };

    Json(serde_json::json!({
        "status": if status.is_success() { "ok" } else { "degraded" },
        "timestamp": chrono::Utc::now().to_rfc3339(),
        "checks": {
            "database": if db_ok { "ok" } else { "error" },
        }
    }))
}

7. 性能压测

# 使用 wrk 进行压测
wrk -t12 -c400 -d30s http://localhost:5051/

# 结果示例
# Running 30s test @ http://localhost:5051/
#   12 threads and 400 connections
#   Thread Stats   Avg      Stdev     Max   +/- Stdev
#     Latency    45ms      12ms    198ms    72.00%
#     Req/Sec     728      89      1.2k     68.00%
#   262,000 requests in 30s, 4.2GB read
#   Requests/sec: 8,733.45
// 性能测试脚本
#[cfg(test)]
mod bench {
    // 使用 criterion 或自定义基准测试
}

8. 回滚策略

# docker-compose.rollback.yml
# 快速回滚到上一个版本
services:
  blog:
    image: ghcr.io/your-repo/rabitlogic-blog:previous
    # ... 其他配置保持不变
# 回滚命令
docker compose pull blog
docker compose up -d --force-recreate blog

# 如果新版本有问题
docker compose stop blog
docker compose run --rm blog \
    --image ghcr.io/your-repo/rabitlogic-blog:previous

9. 部署检查清单

fn deployment_checklist() -> Vec<&'static str> {
    vec![
        "✅ 所有测试通过",
        "✅ 构建成功",
        "✅ Docker 镜像已推送",
        "✅ 环境变量已配置",
        "✅ 数据库迁移已执行",
        "✅ SSL 证书有效",
        "✅ 健康检查通过",
        "✅ 日志收集正常",
        "✅ 监控告警已设置",
        "✅ 回滚方案就绪",
    ]
}

10. 小结

  • Docker 多阶段构建减少最终镜像体积
  • docker-compose.yml 编排 PostgreSQL + Rust 应用 + Nginx
  • GitHub Actions CI 流水线检查代码质量,CD 自动部署
  • 环境变量管理开发/生产配置
  • 结构化日志使用 JSON 格式,方便日志收集系统
  • 健康检查端点和监控确保服务稳定
  • wrk 压测验证性能指标
  • 容器化部署配合版本标签实现快速回滚
dioxuscicddockerdeploygithub-actions