Blog-SSR Docker 部署完整指南——从构建到上云

技术随笔

Blog-SSR Docker 部署完整指南——从构建到上云

1. 部署架构

                       互联网
                         │
                      HTTPS 443
                         │
                   ┌─────┴─────┐
                   │   Nginx    │  ← 反向代理 + SSL 终结 + 静态缓存
                   └─────┬─────┘
                         │
                   ┌─────┴─────┐
                   │ Rust App   │  ← Blog-SSR (Axum, 端口 5051)
                   └─────┬─────┘
                         │
                   ┌─────┴─────┐
                   │ PostgreSQL │  ← 数据库 (端口 5432)
                   └───────────┘

三个容器通过 blog-net 网络互通,Nginx 暴露 80/443 端口到公网。

2. 文件说明

2.1 Dockerfile

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

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

WORKDIR /app

# 缓存依赖层(重要:利用 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"]

关键设计:

| 阶段 | 基础镜像 | 大小 | 内容 | |------|---------|------|------| | Builder | rust:1.80-slim-bookworm | ~1.5GB | Rust 工具链、依赖编译 | | Runtime | debian:bookworm-slim | ~80MB | 仅二进制 + 静态文件 |

依赖缓存技巧:

第1次构建: 下载依赖 + 编译 (15分钟)
第2次构建: 仅编译改动的源码 (2分钟)
      ↑ 因为 Cargo.toml 没变,Docker 直接复用缓存层

实现方式:先复制 Cargo.toml + Cargo.lock,编译一个假的 main.rs 来生成依赖缓存层,再复制真实源码。这样除非依赖变化,否则 Docker 不会重新下载和编译 crate。

2.2 docker-compose.yaml

version: "3.8"

services:
  app:
    build: .                    # 使用当前目录的 Dockerfile
    container_name: rabitlogic-blog
    depends_on:
      db:
        condition: service_healthy  # 等待数据库就绪
    environment:
      - SERVER_HOST=0.0.0.0
      - SERVER_PORT=5051
      - DATABASE_URL=postgres://blog:${DB_PASSWORD:?err}@db:5432/blog
      - JWT_SECURITY_KEY=${JWT_SECURITY_KEY:?err}
      - RUST_LOG=info
    restart: unless-stopped
    networks:
      - blog-net

  db:
    image: postgres:16-alpine     # 仅 50MB 的 PostgreSQL
    container_name: blog-db
    environment:
      POSTGRES_USER: blog
      POSTGRES_PASSWORD: ${DB_PASSWORD:?err}
      POSTGRES_DB: blog
    volumes:
      - pgdata:/var/lib/postgresql/data  # 持久化数据库
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U blog"]
    restart: unless-stopped
    networks:
      - blog-net

  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                    # 挂载 SSL 证书
    depends_on:
      - app
    restart: unless-stopped
    networks:
      - blog-net

volumes:
  pgdata:                      # 命名卷,容器删除后数据不丢失

networks:
  blog-net:                    # 内部网络,三个容器互通

${DB_PASSWORD:?err} 语法:MySQL 的 ${VAR:?err} 表示如果该环境变量未设置,启动时直接报错,防止遗漏配置。

2.3 nginx.conf

events {}

http {
    upstream blog_app {
        server app:5051;          # 指向 docker-compose 中的 app 容器
    }

    # HTTP → HTTPS 强制跳转
    server {
        listen 80;
        server_name your-domain.com;
        return 301 https://$server_name$request_uri;
    }

    # HTTPS 服务
    server {
        listen 443 ssl http2;
        server_name your-domain.com;

        ssl_certificate     /etc/nginx/ssl/cert.pem;
        ssl_certificate_key /etc/nginx/ssl/key.pem;
        ssl_protocols       TLSv1.2 TLSv1.3;
        ssl_ciphers         HIGH:!aNULL:!MD5;

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

        # API 请求 —— 透传真实 IP
        location /api/ {
            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;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        # 页面请求 —— SSR 渲染
        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;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
}

3. 部署步骤

3.1 前置条件

# 云服务器需要安装 Docker 和 Docker Compose
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
# 重新登录使组生效

3.2 获取代码

git clone https://github.com/RabitLogic/rabitlogic-blog.git
cd rabitlogic-blog

3.3 配置环境变量

cp .env.production .env
vi .env

.env 文件内容:

DB_PASSWORD=你的强密码          # 建议: openssl rand -base64 32
JWT_SECURITY_KEY=你的随机密钥    # 建议: openssl rand -base64 64

3.4 配置域名和 SSL

# 1. 修改 nginx.conf 中的 your-domain.com 为你的域名
vi nginx.conf

# 2. 创建 ssl 目录
mkdir ssl

# 3. 上传证书(方式一:Let's Encrypt)
docker run --rm -it \
  -v $PWD/ssl:/etc/letsencrypt \
  -p 80:80 \
  certbot/certbot certonly --standalone \
  -d your-domain.com

# 上传证书(方式二:手动复制 cert.pem 和 key.pem)
# 将文件放到 ssl/ 目录

3.5 启动服务

# 首次启动(构建镜像 + 启动所有容器)
docker compose up -d

# 查看启动日志
docker compose logs -f

# 检查三个容器是否都在运行
docker compose ps

3.6 验证

# 健康检查
curl http://localhost:5051/health
# → OK

# 通过 Nginx 访问
curl https://your-domain.com/health
# → OK

# 刷新文章缓存
curl http://localhost:5051/api/refresh
# → {"status":"ok","message":"Articles cache refreshed"}

4. 镜像构建与分发(Build & Tag & Push)

如果你不想在服务器上编译(服务器 CPU 可能较慢),可以在本地构建好镜像,推送到镜像仓库,再到服务器拉取。

4.1 本地构建

# 在开发机上执行
docker build -t rabitlogic-blog:latest .

4.2 打 Tag

# 给镜像打版本标签
docker tag rabitlogic-blog:latest ghcr.io/rabitlogic/rabitlogic-blog:latest
docker tag rabitlogic-blog:latest ghcr.io/rabitlogic/rabitlogic-blog:v1.0.0

# 也可以推送到 Docker Hub
docker tag rabitlogic-blog:latest rabitlogic/rabitlogic-blog:latest

4.3 推送到镜像仓库

# 登录 GitHub Container Registry
echo $GITHUB_TOKEN | docker login ghcr.io -u rabitlogic --password-stdin

# 或登录 Docker Hub
docker login

# 推送
docker push ghcr.io/rabitlogic/rabitlogic-blog:latest
docker push ghcr.io/rabitlogic/rabitlogic-blog:v1.0.0

4.4 服务器上拉取并运行

修改 docker-compose.yaml,把 build: . 改为直接拉取镜像:

services:
  app:
    image: ghcr.io/rabitlogic/rabitlogic-blog:latest   # 直接拉取,不本地构建
    # build: .   ← 注释掉
    container_name: rabitlogic-blog
    # ... 其余不变
# 服务器上拉取最新镜像
docker compose pull

# 重新创建容器
docker compose up -d --force-recreate

4.5 一键部署脚本

#!/bin/bash
# deploy.sh —— 在服务器上执行

echo "=== 拉取最新代码 ==="
git pull

echo "=== 拉取最新镜像 ==="
docker compose pull

echo "=== 重启服务 ==="
docker compose up -d --force-recreate

echo "=== 清理旧镜像 ==="
docker image prune -f

echo "=== 完成 ==="

5. 常用运维命令

# 查看日志
docker compose logs -f app        # 应用日志
docker compose logs -f db         # 数据库日志
docker compose logs -f nginx      # Nginx 日志

# 进入容器内部
docker exec -it rabitlogic-blog /bin/sh
docker exec -it blog-db psql -U blog

# 重启单个服务
docker compose restart app

# 重新构建并启动
docker compose up -d --build

# 停止并删除所有容器(数据卷保留)
docker compose down

# 完全清理(包括数据卷)
docker compose down -v

# 查看资源占用
docker stats

6. 升级流程

# 1. 本地修改代码
git add .
git commit -m "fix: ..."

# 2. 本地构建并推送镜像
docker build -t ghcr.io/rabitlogic/rabitlogic-blog:latest .
docker push ghcr.io/rabitlogic/rabitlogic-blog:latest

# 3. 服务器上更新
ssh your-server
cd rabitlogic-blog
git pull                          # 同步 docker-compose.yaml 等配置
docker compose pull               # 拉取新镜像
docker compose up -d --force-recreate  # 重建容器

7. 常见问题

openssl-sys id="q-构建时提示-找不到">Q: 构建时提示 找不到
# 需要在构建阶段安装 libssl-dev,Dockerfile 中已有
RUN apt-get update && apt-get install -y pkg-config libssl-dev

Q: 数据库连接失败

# 检查数据库是否就绪
docker compose logs db

# 手动测试连接
docker exec -it blog-db psql -U blog -d blog -c "SELECT 1"

Q: 502 Bad Gateway

Nginx 连不上 app 容器。检查 app 是否正常启动:

docker compose logs app
docker compose restart app

Q: Let's Encrypt 证书续期

docker run --rm -it \
  -v $PWD/ssl:/etc/letsencrypt \
  -p 80:80 \
  certbot/certbot renew
# 然后重启 Nginx
docker compose restart nginx

8. 总结

| 步骤 | 命令 | 说明 | |------|------|------| | 构建 | docker build -t rabitlogic-blog . | 生成约 80MB 的镜像 | | 推送 | docker push ghcr.io/... | 推送到镜像仓库 | | 部署 | docker compose up -d | 启动所有服务 | | 更新 | docker compose pull && docker compose up -d | 零停机更新 | | 回滚 | docker compose down && docker compose up -d | 用旧版本重新部署 |

整个部署流程的核心思路:构建一次,到处运行。开发机编译好镜像,推送到仓库,服务器只需拉取和启动,无需安装 Rust 工具链。

Docker部署DevOpsCI/CDNginx教程