Dioxus SSR 项目 Docker 部署实战:本地编译 + Docker 打包

技术随笔

Dioxus SSR 项目 Docker 部署实战:本地编译 + Docker 打包

背景

rabitlogic-blog 是一个基于 Dioxus 0.7 fullstack 的 SSR 博客项目。技术栈:

  • Rust + Dioxus 0.7(SSR 渲染)
  • PostgreSQL 17(数据存储)
  • Redis(可选,JWT 黑名单)
  • Tailwind CSS(样式)

最初打算将所有构建放入 Docker 内部一次性完成,结果遇到了严重的性能问题。


痛点:Docker 内编译 Rust 为什么慢?

初始方案

最初的 Dockerfile 使用三段式构建:

# Stage 1: Node — 构建 Tailwind CSS
FROM node:20-alpine AS tailwind-builder
# ... npm install && npm run tw:build

# Stage 2: Rust — 编译 Rust 二进制
FROM rust:1.85-slim-bookworm AS builder
# ... cargo build --release --features "server"

# Stage 3: Runtime — 最小运行镜像
FROM debian:bookworm-slim
# ... 复制二进制 + 静态资源

性能对比

| 步骤 | Docker 内编译 | 本地编译 | |------|:-----------:|:-------:| | 拉取 Rust 镜像 (rust:1.85-slim) | ~3 min | — | | 拉取 Rust 依赖 (crates.io) | ~2 min | ~5s (缓存) | | 编译 Rust 依赖 | ~8 min | ~30s (增量) | | 编译项目代码 | ~5 min | ~15s (增量) | | 总计(首次) | 25 min+ | 2 min |

核心原因: Rust 编译器(rustc)本身性能优秀,但:

  1. Rust 工具链镜像约 1.2GB,首次拉取极慢
  2. Docker 层缓存基于文件变动,改一行代码也要重新编译整个 crate
  3. crates.io 下载依赖,在 Docker 内每次都要重新下载

第一次构建跑了 25 分钟还没完成(最终超时失败),用户的一句话点醒了我:

"why not compile it on own system and do it in docker instead?"

于是有了这个方案。


方案:本地编译 + Docker 打包

核心思路

┌─────────────────────────────────────────────────┐
│                  开发机                           │
│  cargo build --release --features "server"       │
│         ↓                                        │
│  target/release/rabitlogic-blog  (14MB 静态二进制)      │
└──────────────────┬──────────────────────────────┘
                   │ COPY 进镜像
                   ↓
┌─────────────────────────────────────────────────┐
│              Docker 镜像 (仅运行时)               │
│  debian:bookworm-slim + rabitlogic-blog + assets       │
│  ≈ 170MB                                        │
└─────────────────────────────────────────────────┘

最终 Dockerfile

# ============================================================
# Stage 1: Node — 构建 Tailwind CSS(轻量,< 30s)
# ============================================================
FROM node:20-alpine AS tailwind

WORKDIR /app

COPY package.json ./
RUN npm install --only=production

COPY tailwind.config.js ./
COPY assets/tailwind-input.css ./assets/tailwind-input.css
COPY src/ ./src/
RUN npm run tw:build

# ============================================================
# Stage 2: Runtime — 最小运行镜像
# ============================================================
FROM debian:bookworm-slim

WORKDIR /app

# 仅安装运行时依赖(无需 Rust 工具链)
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
    ca-certificates \
    libssl3 \
    wget \
    && rm -rf /var/lib/apt/lists/*

# 复制预编译的 Rust 二进制(宿主机编译,非 Docker 内编译)
COPY target/release/rabitlogic-blog /app/rabitlogic-blog

# 复制 Tailwind CSS
COPY --from=tailwind /app/assets/tailwind.css ./assets/tailwind.css

# 复制其他静态资源
COPY assets/main.css    ./assets/main.css
COPY assets/admin.css   ./assets/admin.css
COPY assets/inline.js   ./assets/inline.js
COPY assets/login.js    ./assets/login.js
COPY assets/favicon.ico ./assets/
COPY assets/robots.txt  ./assets/
COPY assets/sitemap.xml ./assets/

# 复制文章(Markdown 内容)
COPY articles/ ./articles/

# 非 root 用户运行
RUN groupadd -r blog && useradd -r -g blog -d /app -s /sbin/nologin blog && \
    chown -R blog:blog /app

USER blog

EXPOSE 5051

ENV SERVER_HOST=0.0.0.0
ENV SERVER_PORT=5051
ENV DIOXUS_PUBLIC_PATH=/app/assets

HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
    CMD wget --no-verbose --tries=1 --spider http://localhost:5051/ || exit 1

ENTRYPOINT ["/app/rabitlogic-blog"]

关键设计: 没有 Rust 构建阶段。二进制在宿主机用 cargo build 编译,Docker 只负责打包。

docker-compose.yml

services:
  postgres:
    image: postgres:17-alpine
    container_name: rabitlogic-blog-postgres
    restart: unless-stopped
    environment:
      POSTGRES_USER: ${POSTGRES_USER:-postgres}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
      POSTGRES_DB: ${POSTGRES_DB:-blog}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-blog}"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 10s
    networks:
      - blog-network

  redis:
    image: redis:7-alpine
    container_name: rabitlogic-blog-redis
    restart: unless-stopped
    command: redis-server --requirepass ${REDIS_PASSWORD:-}
    volumes:
      - redis_data:/data
    ports:
      - "6379:6379"
    profiles:
      - with-redis
    networks:
      - blog-network

  app:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: rabitlogic-blog-app
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      DATABASE_URL: postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/${POSTGRES_DB:-blog}
      SERVER_HOST: 0.0.0.0
      SERVER_PORT: 5051
      JWT_SECURITY_KEY: ${JWT_SECURITY_KEY:-change-me-in-production-blog-jwt-key}
      JWT_EXPIRES_HOURS: ${JWT_EXPIRES_HOURS:-24}
      RUST_LOG: ${RUST_LOG:-info}
    ports:
      - "${SERVER_PORT:-5051}:5051"
    volumes:
      - ./articles:/app/articles
    networks:
      - blog-network

volumes:
  postgres_data:
  redis_data:

networks:
  blog-network:
    driver: bridge

注意: Redis 服务通过 profiles 标记为可选,默认不启动。如需启用:

docker compose --profile with-redis up -d

.dockerignore

# Rust 构建产物(保留 release 二进制用于 Docker 镜像)
target/
!target/release/rabitlogic-blog

node_modules/
.env
.git

Docker CLI 常用命令速查

镜像管理

# 构建镜像
docker build -t rabitlogic-blog:latest .

# 查看本地镜像
docker images

# 删除镜像
docker rmi rabitlogic-blog:latest

# 打标签
docker tag rabitlogic-blog:latest ghcr.io/rabitlogic/rabitlogic-blog:latest

# 推送到仓库
docker push ghcr.io/rabitlogic/rabitlogic-blog:latest

# 拉取镜像
docker pull ghcr.io/rabitlogic/rabitlogic-blog:latest

# 查看镜像分层
docker history rabitlogic-blog:latest

# 清理未使用的镜像(谨慎!)
docker image prune

容器生命周期

# 创建并启动容器
docker run -d --name rabitlogic-blog -p 5051:5051 rabitlogic-blog:latest

# 查看运行中的容器
docker ps

# 查看所有容器(包括已停止的)
docker ps -a

# 停止容器
docker stop rabitlogic-blog-app

# 启动已停止的容器
docker start rabitlogic-blog-app

# 重启容器
docker restart rabitlogic-blog-app

# 删除容器
docker rm rabitlogic-blog-app

# 强制删除运行中的容器
docker rm -f rabitlogic-blog-app

# 查看容器日志
docker logs rabitlogic-blog-app

# 实时跟踪日志
docker logs -f rabitlogic-blog-app

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

# 在容器内执行命令
docker exec rabitlogic-blog-app ls -la /app

# 查看容器资源占用
docker stats rabitlogic-blog-app

# 查看容器进程
docker top rabitlogic-blog-app

Docker Compose

# 启动所有服务(后台)
docker compose up -d

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

# 启动单个服务
docker compose up -d app

# 强制重建容器
docker compose up -d --force-recreate app

# 停止所有服务
docker compose down

# 停止并删除数据卷(数据丢失!)
docker compose down -v

# 查看服务状态
docker compose ps

# 查看所有服务日志
docker compose logs -f

# 查看单个服务日志
docker compose logs -f app

# 重启单个服务
docker compose restart app

# 重新构建单个服务镜像
docker compose build app

# 拉取最新的镜像
docker compose pull

# 列出所有容器
docker compose ps -a

网络相关

# 列出 Docker 网络
docker network ls

# 查看网络详细信息
docker network inspect rabitlogic-blog_blog-network

# 手动创建网络
docker network create blog-net

# 将容器连接到网络
docker network connect blog-net rabitlogic-blog-app

# 端口映射排查
docker port rabitlogic-blog-app

数据卷管理

# 列出数据卷
docker volume ls

# 查看数据卷详情
docker volume inspect rabitlogic-blog_postgres_data

# 删除未使用的数据卷
docker volume prune

# 备份数据卷
docker run --rm -v rabitlogic-blog_postgres_data:/data -v $(pwd):/backup \
    alpine tar czf /backup/pg-backup.tar.gz -C /data .

调试与排查

# 查看容器详细信息
docker inspect rabitlogic-blog-app

# 查看容器日志(最后 50 行)
docker logs --tail=50 rabitlogic-blog-app

# 在容器内调试网络
docker exec rabitlogic-blog-app wget -q -O- http://localhost:5051/

# 查看容器内环境变量
docker exec rabitlogic-blog-app env

# 查看容器内文件结构
docker exec rabitlogic-blog-app ls -la /app

# 查看容器资源使用
docker stats --no-stream

# 清理所有停止的容器
docker container prune

# 清理所有未使用的资源(镜像、容器、网络、数据卷)
docker system prune -a

工作流程

日常开发

# 1. 修改 Rust 代码
# 2. 本地编译(利用 cargo 缓存,只需几秒)
cargo build --release --features "server"

# 3. 构建 Docker 镜像(只需 5 秒,无 Rust 编译)
docker compose build

# 4. 重启应用
docker compose up -d --force-recreate app

# 5. 查看日志
docker compose logs -f app

修改 Tailwind 样式

# Tailwind CSS 在 Docker 的 Node 阶段自动编译
# 修改 src/ 下任何 .rs 文件后,Docker 会重新构建 tailwind.css
docker compose build
docker compose up -d --force-recreate app

首次部署

# 1. 配置环境变量
cp .env.example .env
# 编辑 .env 中的数据库密码、JWT 密钥等

# 2. 本地编译 Rust
cargo build --release --features "server"

# 3. 构建并启动所有服务
docker compose up -d --build

# 4. 验证
curl http://localhost:5051/

.dockerignore 陷阱

.dockerignore 控制哪些文件被发送给 Docker 构建上下文。一个常见的坑:

# 错误写法:
target/
# 这样 target/release/rabitlogic-blog 也会被排除
# 导致 COPY target/release/rabitlogic-blog /app/ 报错 "not found"

# 正确写法:
target/
!target/release/rabitlogic-blog
# 先排除整个 target/,再取消排除需要的二进制文件

Docker 的 .dockerignore 规则:

  1. 先写排除规则(target/
  2. 再写例外规则(!target/release/rabitlogic-blog
  3. 例外规则只对同一目录层的排除有效

部署到云服务器(腾讯云)

部署有两种方式:

方案 A:镜像推送部署(推荐)

在开发机上构建镜像,推送到镜像仓库,云服务器拉取运行。

A1:使用 Docker Hub / GitHub Container Registry

# ── 开发机(本地) ──

# 1. 编译 Rust 二进制
cargo build --release --features "server"

# 2. 构建 Docker 镜像
docker compose build

# 3. 打标签并推送到 Docker Hub
docker tag rabitlogic-blog-app:latest your-dockerhub-username/rabitlogic-blog:latest
docker push your-dockerhub-username/rabitlogic-blog:latest

# ── 腾讯云服务器 ──

# 4. SSH 登录云服务器
ssh root@your-tencent-ip

# 5. 安装 Docker Compose(如果没装)
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

# 6. 拉取代码(只需要 docker-compose.yml + .env,不要源码)
git clone https://github.com/RabitLogic/rabitlogic-blog.git
cd rabitlogic-blog

# 7. 配置环境变量
cp .env.example .env
# 编辑 .env,尤其是 DATABASE_URL 中的密码、JWT_SECURITY_KEY

# 8. 修改 docker-compose.yml:用镜像代替本地构建
# 将 app 服务的 build: . 改为 image: your-dockerhub-username/rabitlogic-blog:latest

# 9. 启动所有服务
docker compose up -d

# 10. 验证
curl http://localhost:5051/

A2:离线传输(没有镜像仓库代理时推荐)

如果云服务器拉取 Docker Hub 镜像慢,可以用 docker save + scp 直接传文件:

# ── 开发机(本地) ──

# 1. 编译并构建镜像
cargo build --release --features "server"
docker compose build

# 2. 导出镜像为 tar 文件(约 170MB)
docker save rabitlogic-blog-app:latest -o rabitlogic-blog.tar
# 也可以压缩以加速传输
gzip rabitlogic-blog.tar  # → rabitlogic-blog.tar.gz (约 60MB)

# 3. 传输到云服务器(用 scp)
scp rabitlogic-blog.tar.gz root@your-tencent-ip:/root/

# ── 腾讯云服务器 ──

# 4. 加载镜像
gunzip rabitlogic-blog.tar.gz
docker load -i rabitlogic-blog.tar

# 5. 拉取代码中的配置(只需要 docker-compose.yml + .env)
git clone https://github.com/RabitLogic/rabitlogic-blog.git
cd rabitlogic-blog
cp .env.example .env
# 编辑 .env

# 6. 启动所有服务(不需要 --build,镜像已存在)
docker compose up -d

# 7. 验证
curl http://localhost:5051/

A3:使用腾讯云容器镜像服务 TCR(国内最快)

腾讯云的 TCR 国内访问速度极快,推荐使用:

# ── 腾讯云控制台 ──
# 1. 开通 TCR 服务:https://console.cloud.tencent.com/tcr
# 2. 创建镜像仓库(例如:rabitlogic-blog)
# 3. 获取登录指令

# ── 开发机(本地) ──

# 4. 登录 TCR
docker login ccr.ccs.tencentyun.com
# 输入腾讯云账号的 用户名 + 密码 或 访问凭证

# 5. 编译并构建镜像
cargo build --release --features "server"
docker compose build

# 6. 打标签并推送到 TCR
docker tag rabitlogic-blog-app:latest ccr.ccs.tencentyun.com/your-namespace/rabitlogic-blog:latest
docker push ccr.ccs.tencentyun.com/your-namespace/rabitlogic-blog:latest

# ── 腾讯云服务器 ──

# 7. SSH 登录,拉取代码
ssh root@your-tencent-ip
git clone https://github.com/RabitLogic/rabitlogic-blog.git
cd rabitlogic-blog
cp .env.example .env
# 编辑 .env

# 8. 修改 docker-compose.yml:使用 TCR 镜像
# image: ccr.ccs.tencentyun.com/your-namespace/rabitlogic-blog:latest

# 9. 登录 TCR 并启动
docker login ccr.ccs.tencentyun.com
docker compose up -d

方案 B:直接在云服务器上构建

如果云服务器有足够的内存和 Rust 工具链,可以直接在云服务器上编译构建。

# 1. SSH 登录云服务器
ssh root@your-tencent-ip

# 2. 安装 Rust(如果没装)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env

# 3. 安装系统依赖(编译 Rust 需要)
sudo apt-get update
sudo apt-get install -y pkg-config libssl-dev

# 4. 克隆代码
git clone https://github.com/RabitLogic/rabitlogic-blog.git
cd rabitlogic-blog

# 5. 配置环境变量
cp .env.example .env
# 编辑 .env

# 6. 编译 Rust 二进制
cargo build --release --features "server"

# 7. 构建并启动 Docker 服务
docker compose up -d --build

# 8. 验证
curl http://localhost:5051/

开启云服务器防火墙端口

腾讯云服务器需要在 安全组 中开放以下端口:

| 端口 | 用途 | 建议 | |:----:|------|:----:| | 5051 | 博客应用 | 内网访问,或通过 Nginx 反代 | | 80 | HTTP | 公网访问(需配合 Nginx) | | 443 | HTTPS | 公网访问(需 SSL 证书) | | 5432 | PostgreSQL | ❌ 不要对外开放,只内网访问 |

安全组规则示例(腾讯云控制台 → 安全组 → 添加规则):

方向:入站
类型:HTTP(80)
来源:0.0.0.0/0
策略:允许

方向:入站
类型:自定义TCP(5051)
来源:0.0.0.0/0
策略:允许

踩坑记录

PORTDIOXUS_PORT id="坑-1dioxus-的-port-环境变量是-不是">坑 1:Dioxus 的 PORT 环境变量是 不是
# ❌ 错误:Dioxus Server 不认这个变量
ENV DIOXUS_PORT=5051

# ✅ 正确:Dioxus 读的是 PORT
ENV PORT=5051

Dioxus Server 的 serve() 函数使用 dioxus_cli_config::fullstack_address_or_localhost() 来确定监听地址:

// 📁 dioxus/packages/cli-config/src/lib.rs
pub const SERVER_PORT_ENV: &str = "PORT";  // ← 读 PORT 环境变量

pub fn fullstack_address_or_localhost() -> SocketAddr {
    let ip = server_ip().unwrap_or_else(|| IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
    let port = server_port().unwrap_or(8080);  // ← 默认 8080
    SocketAddr::new(ip, port)
}
127.0.0.1 id="坑-2dioxus-默认绑定-docker-外部无法访问">坑 2:Dioxus 默认绑定 ,Docker 外部无法访问
# ❌ 错误:只绑定 localhost,外部连接被拒绝
ENV IP=127.0.0.1

# ✅ 正确:绑定 0.0.0.0 让 Docker 端口映射生效
ENV IP=0.0.0.0

表现: 容器启动后 docker compose ps 显示端口映射 5051->5051,但 curl 显示 Connection refused。因为 Docker 端口映射到容器内 127.0.0.1:5051,但宿主机无法直接访问容器的 localhost。

env!("CARGO_MANIFEST_DIR") id="坑-3-编译时路径在-docker-内失效">坑 3: 编译时路径在 Docker 内失效
// ❌ 编译时宏:在宿主机是 /home/user/project/assets
// 到了 Docker 容器里这个路径不存在,导致 panic
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("assets");

// ✅ 修复:优先使用环境变量,Docker 中设 DIOXUS_PUBLIC_PATH=/app/assets
let assets_dir = std::env::var("DIOXUS_PUBLIC_PATH")
    .map(std::path::PathBuf::from)
    .unwrap_or_else(|_| std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("assets"));
std::env::set_var("DIOXUS_PUBLIC_PATH", &assets_dir);
.dockerignore id="坑-4-否定模式">坑 4: 否定模式
# ❌ 错误:排除了 target/,二进制文件无法复制进镜像
target/

# ✅ 正确:先排除,再取消排除需要的文件
target/
!target/release/rabitlogic-blog

最终 docker-compose.yml 服务一览

| 服务 | 镜像 | 端口 | 作用 | |------|------|:----:|------| | postgres | postgres:17-alpine | 5432 | 数据库 | | redis | redis:7-alpine | 6379 | 缓存 / JWT 黑名单 | | app | rabitlogic-blog-app | 5051 | Dioxus SSR 应用 | | nginx | nginx:alpine | 80/443 | 反向代理 |


参考

dockerdioxusrustdevopsdeployment教程