从拉取代码到一键上线:QiaoFu Blog 的完整部署流程
QiaoFu Blog 是一套前后端分离的个人内容平台:
- 前端:Nuxt 4、Vue 3、TypeScript
- 后端:Rust、Salvo、SQLx
- 数据库:MySQL 8
- 部署方式:Docker、Docker Compose、Nginx
- 自动化脚本:PowerShell
这篇文章记录如何从零拉取代码、配置本地环境、构建 Docker 镜像,并通过 deploy.ps1 将前后端一次性发布到 Linux 服务器。
一、项目仓库
项目分为两个独立仓库。
前端:
https://gitee.com/ainimr/blog-frontend
后端:
https://gitee.com/ainimr/blog-backend
在 Windows 开发机上创建项目目录:
New-Item -ItemType Directory -Path QiaoFu
Set-Location QiaoFu
New-Item -ItemType Directory -Path frontend
New-Item -ItemType Directory -Path backend
分别拉取前后端代码:
git clone https://gitee.com/ainimr/blog-frontend.git frontend/blog-frontend
git clone https://gitee.com/ainimr/blog-backend.git backend/blog-backend
最终目录结构如下:
QiaoFu/
├─ deploy.ps1
├─ frontend/
│ └─ blog-frontend/
│ ├─ Dockerfile
│ ├─ docker-compose.runtime.yml
│ ├─ scripts/
│ │ └─ package-image.ps1
│ └─ ...
└─ backend/
└─ blog-backend/
├─ Dockerfile
├─ docker-compose.runtime.yml
├─ scripts/
│ └─ package-image.ps1
└─ ...
根目录的 deploy.ps1 会按照这个结构查找前后端项目,因此不要随意修改目录层级。
二、本地开发环境
如果只需要通过 Docker 构建并部署,开发机安装以下工具即可:
- Git
- Docker Desktop
- PowerShell
- OpenSSH Client
如果还需要脱离 Docker 运行源码,则另外安装:
- Node.js 24
- pnpm
- Rust stable
- MySQL 8
安装 Docker Desktop
Windows 推荐使用 WSL 2 后端。下载安装后启动 Docker Desktop,并确认正在运行 Linux 容器。具体要求和安装方式以 Docker Desktop Windows 官方文档为准。
安装完成后打开 PowerShell:
docker version
docker compose version
docker run --rm hello-world
三个命令都能正常执行,说明 Docker 环境已经可以使用。
Docker Desktop 默认支持构建 Linux 镜像,也支持通过模拟构建 linux/amd64 和 linux/arm64 镜像。Docker 多平台构建文档
三、本地运行后端
进入后端目录:
Set-Location backend/blog-backend
Copy-Item .env.example .env
编辑 .env,至少填写数据库连接和 JWT 密钥:
BLOG__SERVER__HOST=127.0.0.1
BLOG__SERVER__PORT=5800
DATABASE_URL=mysql://blog:数据库密码@127.0.0.1:3306/blog
BLOG__JWT__SECRET=替换成足够长的随机字符串
BLOG__JWT__ISSUER=blog-backend
BLOG__JWT__AUDIENCE=blog-admin
BLOG__UPLOAD__DIRECTORY=storage/uploads
BLOG__GEOIP__DATABASE=storage/geoip/ip2region.xdb
BLOG__SMTP__ENABLED=false
数据库密码中如果包含 @、:、/ 等字符,需要先进行 URL 编码。
创建数据库:
CREATE DATABASE blog
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
CREATE USER 'blog'@'127.0.0.1'
IDENTIFIED BY '替换成数据库密码';
GRANT ALL PRIVILEGES ON blog.* TO 'blog'@'127.0.0.1';
FLUSH PRIVILEGES;
启动后端:
cargo run
服务启动后会自动执行尚未应用的数据库迁移。
验证接口:
Invoke-RestMethod http://127.0.0.1:5800/api/health
首次运行还需要创建管理员:
cargo run --bin create_admin
四、本地运行前端
进入前端目录:
Set-Location frontend/blog-frontend
Copy-Item .env.example .env
编辑 .env:
NUXT_PUBLIC_API_BASE=http://127.0.0.1:5800
NUXT_PUBLIC_SITE_URL=http://localhost:3000
安装依赖并启动:
corepack enable
pnpm install
pnpm dev
浏览器访问:
http://localhost:3000
生产环境必须把 NUXT_PUBLIC_SITE_URL 改成最终的 HTTPS 域名,否则 canonical、RSS 和 Sitemap 会生成错误地址。
五、为什么在本地构建镜像
服务器只负责运行镜像,不负责安装 Node.js、pnpm、Rust 或 Cargo,也不在服务器上编译项目。
整体流程是:
开发机拉取代码
↓
Docker 构建 Linux 镜像
↓
导出为 TAR 文件
↓
上传服务器
↓
服务器加载镜像
↓
Docker Compose 重建容器
这样可以降低服务器内存压力,也能保证服务器运行的内容与本地构建产物一致。
六、分别构建前后端镜像
后端打包:
Set-Location backend/blog-backend
.\scripts\package-image.ps1 `
-Version v1.0.0 `
-Platform linux/amd64
前端打包:
Set-Location frontend/blog-frontend
.\scripts\package-image.ps1 `
-Version v1.0.0 `
-Platform linux/amd64
普通云服务器一般使用:
linux/amd64
ARM 云服务器使用:
linux/arm64
可以在服务器执行下面的命令确认架构:
uname -m
对应关系:
x86_64 → linux/amd64
aarch64 → linux/arm64
打包脚本会完成以下工作:
- 根据目标平台构建 Docker 镜像。
- 检查镜像的系统和 CPU 架构。
- 使用
docker save导出镜像。 - 将镜像、Compose 文件和配置示例打包。
- 计算发布包的 SHA256。
构建产物保存在各自的 release 目录中。
七、首次配置服务器
服务器至少需要:
- Docker Engine
- Docker Compose 插件
- MySQL 8
- Nginx
- SSH 服务
Ubuntu 服务器建议按照 Docker Engine 官方安装文档配置 Docker 官方软件源,然后安装:
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
验证:
sudo systemctl status docker
sudo docker run hello-world
docker compose version
创建项目目录:
mkdir -p /www/wwwroot/zhu-blog/storage/uploads
cd /www/wwwroot/zhu-blog
服务器目录中需要预先准备:
/www/wwwroot/zhu-blog/
├─ docker-compose.bt.yml
├─ .env.backend
├─ .env.frontend
└─ storage/
└─ uploads/
后端生产配置
创建 .env.backend:
DATABASE_URL=mysql://blog:数据库密码@127.0.0.1:3306/blog
BLOG__DATABASE__MAX_CONNECTIONS=10
BLOG__DATABASE__MIN_CONNECTIONS=1
BLOG__DATABASE__ACQUIRE_TIMEOUT_SECONDS=5
BLOG__JWT__SECRET=替换成随机生成的高强度密钥
BLOG__JWT__ISSUER=blog-backend
BLOG__JWT__AUDIENCE=blog-admin
BLOG__JWT__ACCESS_TOKEN_EXPIRES_SECONDS=900
BLOG__AUTH__REFRESH_TOKEN_EXPIRES_SECONDS=7776000
BLOG__SMTP__ENABLED=false
RUST_LOG=info,sqlx=warn
可通过下面的命令生成 JWT 密钥:
openssl rand -base64 48
前端生产配置
创建 .env.frontend:
NUXT_PUBLIC_API_BASE=http://127.0.0.1:5800
NUXT_PUBLIC_SITE_URL=https://你的域名
Docker Compose 配置
创建 docker-compose.bt.yml:
name: qiaofu-blog
services:
backend:
image: qiaofu-blog-backend:local
restart: unless-stopped
network_mode: host
env_file:
- .env.backend
environment:
BLOG__SERVER__HOST: 127.0.0.1
BLOG__SERVER__PORT: 5800
BLOG__UPLOAD__DIRECTORY: /app/storage/uploads
BLOG__GEOIP__DATABASE: /app/storage/geoip/ip2region.xdb
volumes:
- ./storage/uploads:/app/storage/uploads
healthcheck:
test:
- CMD
- curl
- -fsS
- http://127.0.0.1:5800/api/health
interval: 10s
timeout: 5s
retries: 12
mem_limit: 384m
logging:
driver: json-file
options:
max-size: 10m
max-file: "3"
frontend:
image: qiaofu-blog-frontend:local
restart: unless-stopped
network_mode: host
env_file:
- .env.frontend
environment:
HOST: 127.0.0.1
PORT: 3000
depends_on:
backend:
condition: service_healthy
mem_limit: 512m
logging:
driver: json-file
options:
max-size: 10m
max-file: "3"
这里的 qiaofu-blog-* 是当前构建脚本使用的内部镜像名称。
检查配置:
docker compose -f docker-compose.bt.yml config --quiet
八、配置 SSH 密钥
自动部署不应该把服务器密码明文写进代码。
在 Windows 开发机生成密钥:
ssh-keygen -t ed25519 -C "qiaofu-deploy"
默认会生成:
C:\Users\你的用户名\.ssh\id_ed25519
C:\Users\你的用户名\.ssh\id_ed25519.pub
把公钥添加到服务器:
Get-Content "$env:USERPROFILE\.ssh\id_ed25519.pub" |
ssh root@你的服务器地址 "umask 077; mkdir -p ~/.ssh; cat >> ~/.ssh/authorized_keys"
首次执行会要求输入一次服务器密码。
之后验证免密登录:
ssh root@你的服务器地址
生产环境更推荐创建权限受限的部署用户,而不是长期使用 root。
九、一键部署脚本
在前后端目录的共同根目录创建 deploy.ps1:
param(
[string]$Version = (Get-Date -Format 'yyyyMMdd-HHmmss'),
[ValidateSet('linux/amd64', 'linux/arm64')]
[string]$Platform = 'linux/amd64'
)
$ErrorActionPreference = 'Stop'
# 根据自己的服务器修改
$Server = 'YOUR_SERVER_IP_OR_DOMAIN'
$User = 'root'
$RemoteDir = '/www/wwwroot/zhu-blog'
$KeyFile = Join-Path $env:USERPROFILE '.ssh\id_ed25519'
if ($Server -eq 'YOUR_SERVER_IP_OR_DOMAIN') {
throw '请先填写服务器地址。'
}
if (-not (Test-Path -LiteralPath $KeyFile)) {
throw "没有找到 SSH 私钥:$KeyFile"
}
$SshPath = (Get-Command ssh -ErrorAction Stop).Source
$ScpPath = (Get-Command scp -ErrorAction Stop).Source
$root = $PSScriptRoot
$backendDir = Join-Path $root 'backend\blog-backend'
$frontendDir = Join-Path $root 'frontend\blog-frontend'
$platformSlug = $Platform.Replace('/', '-')
$backendTar = "qiaofu-blog-backend-$Version-$platformSlug.tar"
$frontendTar = "qiaofu-blog-frontend-$Version-$platformSlug.tar"
Write-Host "==> [1/5] 构建后端镜像:$Platform"
& (Join-Path $backendDir 'scripts\package-image.ps1') `
-Version $Version `
-Platform $Platform
Write-Host "==> [2/5] 构建前端镜像:$Platform"
& (Join-Path $frontendDir 'scripts\package-image.ps1') `
-Version $Version `
-Platform $Platform
$backendTarPath = Join-Path $backendDir "release\$backendTar"
$frontendTarPath = Join-Path $frontendDir "release\$frontendTar"
if (-not (Test-Path -LiteralPath $backendTarPath)) {
throw "没有找到后端构建产物:$backendTarPath"
}
if (-not (Test-Path -LiteralPath $frontendTarPath)) {
throw "没有找到前端构建产物:$frontendTarPath"
}
Write-Host '==> [3/5] 上传后端镜像'
& $ScpPath `
-i $KeyFile `
$backendTarPath `
"${User}@${Server}:${RemoteDir}/"
if ($LASTEXITCODE -ne 0) {
throw '后端镜像上传失败。'
}
Write-Host '==> [4/5] 上传前端镜像'
& $ScpPath `
-i $KeyFile `
$frontendTarPath `
"${User}@${Server}:${RemoteDir}/"
if ($LASTEXITCODE -ne 0) {
throw '前端镜像上传失败。'
}
Write-Host '==> [5/5] 加载镜像并重建服务'
$remoteCommand = @(
'set -e'
"cd '$RemoteDir'"
"docker load -i '$backendTar'"
"docker load -i '$frontendTar'"
"rm -f '$backendTar' '$frontendTar'"
'docker compose -f docker-compose.bt.yml config --quiet'
'docker compose -f docker-compose.bt.yml up -d --force-recreate --remove-orphans'
'docker compose -f docker-compose.bt.yml ps'
) -join '; '
& $SshPath `
-i $KeyFile `
"${User}@${Server}" `
$remoteCommand
if ($LASTEXITCODE -ne 0) {
throw '服务器部署失败。'
}
Write-Host "==> 部署完成:$Version"
该脚本依次执行五个步骤:
- 构建后端 Linux 镜像。
- 构建前端 Linux 镜像。
- 上传后端镜像。
- 上传前端镜像。
- 在服务器加载镜像并重建容器。
十、执行一键部署
普通 AMD64 云服务器:
.\deploy.ps1 `
-Version v1.0.0 `
-Platform linux/amd64
ARM64 服务器:
.\deploy.ps1 `
-Version v1.0.0 `
-Platform linux/arm64
部署成功后检查:
cd /www/wwwroot/zhu-blog
docker compose -f docker-compose.bt.yml ps
docker compose -f docker-compose.bt.yml logs --tail=100 backend
docker compose -f docker-compose.bt.yml logs --tail=100 frontend
curl -fsS http://127.0.0.1:5800/api/health
curl -I http://127.0.0.1:3000
首次上线还需要创建管理员:
docker compose -f docker-compose.bt.yml run --rm backend create_admin
后端启动时会自动执行数据库迁移,不需要手动执行 SQL 迁移文件。
十一、配置 Nginx 和 HTTPS
Nginx 只需要反向代理前端的 3000 端口:
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
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;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
然后为域名申请 SSL 证书,并配置 HTTP 自动跳转 HTTPS。
前后端分别只监听:
127.0.0.1:3000
127.0.0.1:5800
因此服务器防火墙不需要向公网开放 3000 和 5800,公网只开放 80、443 和受控的 SSH 端口。
十二、更新与回滚
后续更新只需要拉取最新代码:
git -C frontend/blog-frontend pull
git -C backend/blog-backend pull
使用新的版本号部署:
.\deploy.ps1 -Version v1.0.1
每次部署都应使用不同版本号,便于追踪发布记录。
部署前需要单独备份:
- MySQL 数据库
.env.backend.env.frontendstorage/uploads
发布脚本不会覆盖服务器环境变量和上传目录,但数据库迁移可能修改表结构,因此重要更新前仍然需要备份。
结语
到这里,QiaoFu Blog 已经形成了一套完整的发布链路:
拉取代码
→ 配置开发环境
→ 本地运行验证
→ Docker 构建
→ 导出 Linux 镜像
→ SSH 上传
→ Compose 重建服务
→ 健康检查
服务器不需要源码,也不需要安装 Rust 和 Node.js。日常更新时,只需要在开发机完成修改和检查,然后执行一条 PowerShell 命令,就能同时完成前后端构建、上传和服务更新。
这套流程目前仍然比较朴素,但已经足以支撑个人项目稳定、可重复地迭代发布。

留下你的想法