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 工具链。