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)本身性能优秀,但:
- Rust 工具链镜像约 1.2GB,首次拉取极慢
- Docker 层缓存基于文件变动,改一行代码也要重新编译整个 crate
- 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 规则:
- 先写排除规则(
target/) - 再写例外规则(
!target/release/rabitlogic-blog) - 例外规则只对同一目录层的排除有效
部署到云服务器(腾讯云)
部署有两种方式:
方案 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 | 反向代理 |