第二十八章: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