不要全是感谢分享,感觉好像在逛黄网 ![]()
- 客户提供的服务器没有外网,无法直接拉去镜像,但是你们公司只提供docker版本怎么办?
- 你想傻瓜操作,本地只需运行一个脚本,服务器只需运行一个价脚本怎么办?
这篇文章让你一学就会!!!
部署流程概览
flowchart TD
Start([开始部署]) --> Config[1.修改配置文件.env]
Config --> Package[2.执行打包脚本<br/>package.sh/ps1]
Package --> Build1[构建后端镜像]
Build1 --> Build2[构建前端镜像]
Build2 --> Build3[构建人脸识别服务镜像]
Build3 --> Pull[拉取基础镜像<br/>MySQL/Redis/RustFS]
Pull --> Export[导出所有镜像为 .tar 文件]
Export --> Zip[打包成 deploy-bundle.zip]
Zip --> Upload{上传到离线服务器}
Upload --> Unzip[3.解压 deploy-bundle.zip]
Unzip --> Deploy[4.执行部署脚本 deploy.sh]
Deploy --> Load[加载所有 .tar 镜像]
Load --> StartMySQL[启动 MySQL 数据库]
StartMySQL --> StartRedis[启动 Redis 缓存]
StartRedis --> StartRustFS[启动 RustFS 对象存储]
StartRustFS --> StartFace[启动人脸识别服务]
StartFace --> StartBackend[启动后端服务]
StartBackend --> StartFrontend[启动前端服务]
StartFrontend --> Complete([5.部署完成])
style Start fill:#e1f5e1
style Complete fill:#e1f5e1
style Upload fill:#fff4e6
style Package fill:#e3f2fd
style Deploy fill:#f3e5f5
环境准备
本地开发机(用于打包)
必须环境就不说了,你肯定有
目标服务器(用于部署)
服务器装一个docker就行,你肯定会
我的工作目录
我将部署的工作目录放到了后端中,这是下边的介绍
device-server/deploy/
├── .env # 环境变量配置(给Docker Compose编排文件用的)
├── docker-compose.yml # Docker Compose 编排文件
├── package.sh # Linux/Mac 打包脚本
├── package.ps1 # Windows PowerShell 打包脚本
├── deploy.sh # 服务器部署脚本
├── README.md
├── backend/
│ ├── Dockerfile # 后端镜像构建文件
│ └── config.docker.yaml # 后端容器配置
├── frontend/
│ ├── Dockerfile # 前端镜像构建文件
│ └── nginx.conf # Nginx 配置
├── images/ # 导出的镜像文件(打包后生成)
│ ├── device-server-backend__1.0.0.tar
│ ├── device-server-frontend__1.0.0.tar
│ ├── face-recognition-service__1.0.0.tar
│ ├── mysql__8.4.6.tar
│ ├── redis__7.2.5.tar
│ └── rustfs__rustfs__1.0.0-alpha.59.tar
└── deploy-bundle.zip # 最终部署包(打包后生成)
我的镜像产物
| 服务 | 镜像 | 端口 | 说明 |
|---|---|---|---|
| 前端 | device-server-frontend:1.0.0 | 15501 | React 19 + Nginx 自建镜像 |
| 后端 | device-server-backend:1.0.0 | 15510 | GoFrame v2.9.0 自建镜像 |
| 人脸识别 | face-recognition-service:1.0.0 | 15514 | Flask Python 人脸识别服务 自建镜像 |
| 数据库 | mysql:8.4.6 | 15511 | MySQL 8.4 |
| 缓存 | redis:7.2.5 | 15512 | Redis 7.2 |
| 对象存储 | rustfs/rustfs:1.0.0-alpha.59 | 15513 | 对象存储服务 |
第一步:本地打包
1.1 配置环境变量
进入部署目录:
cd device-server/deploy
编辑 .env 文件,根据目标服务器环境调整配置:
# ========================================
# 镜像版本(通常不需要修改)
# ========================================
BACKEND_IMAGE=device-server-backend:1.0.0
FRONTEND_IMAGE=device-server-frontend:1.0.0
FACE_RECOGNITION_IMAGE=face-recognition-service:1.0.0
MYSQL_IMAGE=mysql:8.4.6
REDIS_IMAGE=redis:7.2.5
RUSTFS_IMAGE=rustfs/rustfs:1.0.0-alpha.59
# ========================================
# 服务端口(根据客户需求调整)
# ========================================
BACKEND_HOST_PORT=15510 # 后端 API 端口
FRONTEND_HOST_PORT=15501 # 前端访问端口
FACE_RECOGNITION_HOST_PORT=15514 # 人脸识别服务端口
MYSQL_HOST_PORT=15511 # MySQL 数据库端口
REDIS_HOST_PORT=15512 # Redis 端口
RUSTFS_HOST_PORT=15513 # 对象存储端口
# ========================================
# 数据库配置(⚠️ 生产环境请修改密码)
# ========================================
MYSQL_ROOT_PASSWORD=1 # MySQL root 密码
MYSQL_DATABASE=device # 数据库名
MYSQL_USER=admin # 数据库用户
MYSQL_PASSWORD=1 # 数据库密码
# ========================================
# 对象存储配置(⚠️ 生产环境请修改密钥)
# ========================================
RUSTFS_ACCESS_KEY=admin # 对象存储访问密钥
RUSTFS_SECRET_KEY=1 # 对象存储私钥
RUSTFS_CONSOLE_ENABLE=true # 是否启用控制台
RUSTFS_LOG_LEVEL=info # 日志级别
# ========================================
# 时区配置
# ========================================
TZ=Asia/Shanghai
1.2 执行打包脚本
Windows 用户(PowerShell)
# 进入部署目录
cd device-server\deploy
# 执行打包脚本
.\package.ps1
Linux/Mac 用户
# 进入部署目录
cd device-server/deploy
# 赋予执行权限(首次运行)
chmod +x package.sh
# 执行打包脚本
./package.sh
1.3 打包过程详解
脚本执行过程中会依次完成以下步骤:
[1/8] 检查工具依赖
[2/8] 加载 .env 配置
[3/8] 构建后端镜像
[4/8] 构建前端资产
[5/8] 构建前端镜像
[6/8] 构建人脸识别服务镜像
[7/8] 拉取基础镜像
[8/8] 导出所有镜像到 images/ 目录
├─ device-server-backend__1.0.0.tar
├─ device-server-frontend__1.0.0.tar
├─ face-recognition-service__1.0.0.tar
├─ mysql__8.4.6.tar
├─ redis__7.2.5.tar
└─ rustfs__rustfs__1.0.0-alpha.59.tar
[9/9] 打包成 deploy-bundle.zip
完成后会看到提示:
镜像构建、导出与资料打包完成,压缩包位于 deploy-bundle.zip
第二步:服务器部署
2.1 上传到服务器
你肯定会上传到服务器,选一个合适的位置
2.2 解压部署包
# 创建部署目录
sudo mkdir -p /opt/device
cd /opt/device
# 解压(假设文件在 /tmp 目录)
sudo unzip /tmp/deploy-bundle.zip -d .
# 验证解压结果
ls -la
# 应该看到:.env, docker-compose.yml, deploy.sh, images/, backend/, frontend/ 等
2.3 执行部署脚本
# 赋予执行权限
chmod +x deploy.sh
# 执行部署
sudo ./deploy.sh
2.4 部署过程详解
脚本会自动完成以下操作:
[1/5] 检查 Docker 和 Docker Compose
[2/5] 加载 .env 配置
[3/5] 导入镜像
├─ 加载 mysql__8.4.6.tar
├─ 加载 redis__7.2.5.tar
├─ 加载 rustfs__rustfs__1.0.0-alpha.59.tar
├─ 加载 face-recognition-service__1.0.0.tar
├─ 加载 device-server-backend__1.0.0.tar
└─ 加载 device-server-frontend__1.0.0.tar
[4/5] 启动服务
├─ 启动 MySQL (等待健康检查)
├─ 启动 Redis
├─ 启动 RustFS
├─ 启动人脸识别服务
├─ 启动后端服务 (等待 MySQL 就绪)
└─ 启动前端服务 (等待后端就绪)
[5/5] 署完成!
2.5 验证部署状态
查看所有容器状态:
docker compose ps
期望输出:
NAME IMAGE STATUS PORTS
device-server-backend device-server-backend:1.0.0 Up 2 minutes 0.0.0.0:15510->15510/tcp
device-server-frontend device-server-frontend:1.0.0 Up 2 minutes 0.0.0.0:15501->80/tcp
device-server-mysql mysql:8.4.6 Up 2 minutes 0.0.0.0:15511->3306/tcp
device-server-redis redis:7.2.5 Up 2 minutes 0.0.0.0:15512->6379/tcp
device-server-rustfs rustfs/rustfs:1.0.0-alpha.59 Up 2 minutes 0.0.0.0:15513->9000/tcp
device-server-face-recognition face-recognition-service:1.0.0 Up 2 minutes 0.0.0.0:15514->5000/tcp
2.6 访问系统
看看能正常访问就没问题了
前端访问地址:
http://服务器IP:15501
后端 API 地址:
http://服务器IP:15510
RustFS 控制台(如果启用):
http://服务器IP:15513
附录
- 打包脚本:
device-server/deploy/package.ps1(Windows PowerShell) - 部署脚本:
device-server/deploy/deploy.sh(服务器端) - Docker Compose 配置:
device-server/deploy/docker-compose.yml
package.ps1
param(
[switch]$Help
)
if ($Help) {
Write-Host "Usage: .\package.ps1"
Write-Host "Build Docker images and package deployment files"
exit 0
}
# Set error handling
$ErrorActionPreference = 'Stop'
# Get script directory and related paths
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$RootDir = Join-Path $ScriptDir ".."
$FrontendDir = Join-Path $RootDir "..\device-font"
$FaceRecognitionDir = Join-Path $RootDir "face-recognition-service"
$EnvFile = Join-Path $ScriptDir ".env"
$ImagesDir = Join-Path $ScriptDir "images"
$BundleFile = Join-Path $ScriptDir "deploy-bundle.zip"
function Test-Command {
param([string]$Command)
try {
Get-Command $Command -ErrorAction Stop | Out-Null
return $true
} catch {
return $false
}
}
function Ensure-Tooling {
if (-not (Test-Command "docker")) {
Write-Error "Missing command: docker"
exit 1
}
if (-not (Get-Command "Compress-Archive" -ErrorAction SilentlyContinue)) {
Write-Error "Missing compression tool, please ensure PowerShell 5.0+ is available"
exit 1
}
}
function Load-Env {
if (-not (Test-Path $EnvFile)) {
Write-Error ".env file does not exist: $EnvFile"
exit 1
}
Get-Content $EnvFile | ForEach-Object {
$line = $_.Trim()
if ($line -and -not $line.StartsWith("#") -and $line.Contains("=")) {
$parts = $line.Split("=", 2)
if ($parts.Length -eq 2) {
$name = $parts[0].Trim()
$value = $parts[1].Trim()
Set-Variable -Name $name -Value $value -Scope Global
[Environment]::SetEnvironmentVariable($name, $value, 'Process')
Write-Host "Loaded env: $name = $value"
}
}
}
}
function Sanitize-ImageName {
param([string]$ImageName)
return $ImageName -replace '[/:]', '__'
}
function Build-Backend {
Write-Host "Building backend image $BACKEND_IMAGE"
$dockerfilePath = Join-Path $ScriptDir "backend\Dockerfile"
# Convert Windows path to Unix-style path for Docker
$buildContext = (Resolve-Path $RootDir).Path -replace '\\', '/'
& docker build -f $dockerfilePath -t $BACKEND_IMAGE $buildContext
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
}
function Build-FrontendAssets {
Write-Host "Building frontend assets (local pnpm build)"
if (-not (Test-Command "pnpm")) {
Write-Error "Missing command: pnpm"
exit 1
}
Push-Location $FrontendDir
try {
& pnpm install --frozen-lockfile --force
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
& pnpm build
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
} finally {
Pop-Location
}
}
function Build-Frontend {
Write-Host "Building frontend image $FRONTEND_IMAGE"
$dockerfilePath = Join-Path $ScriptDir "frontend\Dockerfile"
# Convert Windows path to Unix-style path for Docker
$buildContext = (Resolve-Path $FrontendDir).Path -replace '\\', '/'
& docker build -f $dockerfilePath -t $FRONTEND_IMAGE $buildContext
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
}
function Build-FaceRecognition {
Write-Host "Building face-recognition image $FACE_RECOGNITION_IMAGE"
$dockerfilePath = Join-Path $FaceRecognitionDir "Dockerfile"
# Convert Windows path to Unix-style path for Docker
$buildContext = (Resolve-Path $FaceRecognitionDir).Path -replace '\\', '/'
& docker build -f $dockerfilePath -t $FACE_RECOGNITION_IMAGE $buildContext
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
}
function Pull-BaseImages {
$images = @($MYSQL_IMAGE, $REDIS_IMAGE, $RUSTFS_IMAGE)
foreach ($image in $images) {
Write-Host "Pulling base image $image"
& docker pull $image
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
}
}
function Save-Images {
if (-not (Test-Path $ImagesDir)) {
New-Item -ItemType Directory -Path $ImagesDir | Out-Null
}
$images = @($BACKEND_IMAGE, $FRONTEND_IMAGE, $FACE_RECOGNITION_IMAGE, $MYSQL_IMAGE, $REDIS_IMAGE, $RUSTFS_IMAGE)
foreach ($image in $images) {
$sanitizedName = Sanitize-ImageName $image
$filename = Join-Path $ImagesDir "$sanitizedName.tar"
Write-Host "Exporting image $image -> $filename"
& docker save $image -o $filename
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
}
}
function Create-Bundle {
Write-Host "Creating deployment bundle $BundleFile"
Push-Location $ScriptDir
try {
$bundleName = "deploy-bundle.zip"
if (Test-Path $bundleName) {
Remove-Item -LiteralPath $bundleName -Force
}
$items = Get-ChildItem -LiteralPath . -Force | Where-Object { $_.Name -ne $bundleName }
Compress-Archive -LiteralPath $items -DestinationPath $bundleName -Force
} finally {
Pop-Location
}
}
function Main {
try {
Ensure-Tooling
Load-Env
Build-Backend
Build-FrontendAssets
Build-Frontend
Build-FaceRecognition
Pull-BaseImages
Save-Images
Create-Bundle
Write-Host "Image build, export and packaging completed. Bundle located at: $BundleFile"
} catch {
Write-Error $_.Exception.Message
exit 1
}
}
# Execute main function
Main
deploy.sh
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
ENV_FILE="${SCRIPT_DIR}/.env"
IMAGES_DIR="${SCRIPT_DIR}/images"
COMPOSE_FILE="${SCRIPT_DIR}/docker-compose.yml"
require_command() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "缺少命令: $1" >&2
exit 1
fi
}
require_docker_compose() {
if ! docker compose version >/dev/null 2>&1; then
echo "当前 Docker 不支持 compose 插件,请安装 docker compose" >&2
exit 1
fi
}
load_env() {
if [[ ! -f "${ENV_FILE}" ]]; then
echo ".env 文件不存在: ${ENV_FILE}" >&2
exit 1
fi
set -a
# shellcheck disable=SC1090
source "${ENV_FILE}"
set +a
}
load_images() {
if [[ ! -d "${IMAGES_DIR}" ]]; then
echo "镜像目录不存在: ${IMAGES_DIR}" >&2
exit 1
fi
shopt -s nullglob
local files=(${IMAGES_DIR}/*.tar)
shopt -u nullglob
if [[ ${#files[@]} -eq 0 ]]; then
echo "镜像目录为空,请先运行 package.sh" >&2
exit 1
fi
for file in "${files[@]}"; do
echo "导入镜像 ${file}" >&2
docker load -i "${file}"
done
}
run_compose() {
docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" up -d
}
main() {
require_command docker
require_docker_compose
load_env
load_images
run_compose
echo "部署完成" >&2
}
main "$@"
docker-compose.yml
version: "3.9"
services:
backend:
image: ${BACKEND_IMAGE}
container_name: device-server-backend
restart: unless-stopped
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_started
rustfs:
condition: service_started
face-recognition:
condition: service_started
environment:
TZ: ${TZ}
ports:
- "${BACKEND_HOST_PORT}:${BACKEND_CONTAINER_PORT}"
frontend:
image: ${FRONTEND_IMAGE}
container_name: device-server-frontend
restart: unless-stopped
depends_on:
backend:
condition: service_started
ports:
- "${FRONTEND_HOST_PORT}:80"
mysql:
image: ${MYSQL_IMAGE}
container_name: device-server-mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
TZ: ${TZ}
ports:
- "${MYSQL_HOST_PORT}:3306"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 10s
retries: 10
volumes:
- mysql_data:/var/lib/mysql
redis:
image: ${REDIS_IMAGE}
container_name: device-server-redis
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes"]
ports:
- "${REDIS_HOST_PORT}:6379"
volumes:
- redis_data:/data
rustfs:
image: ${RUSTFS_IMAGE}
container_name: device-server-rustfs
privileged: true
restart: unless-stopped
environment:
RUSTFS_ACCESS_KEY: ${RUSTFS_ACCESS_KEY}
RUSTFS_SECRET_KEY: ${RUSTFS_SECRET_KEY}
RUSTFS_ADDRESS: "0.0.0.0:9000"
RUSTFS_CONSOLE_ENABLE: ${RUSTFS_CONSOLE_ENABLE}
RUSTFS_LOG_LEVEL: ${RUSTFS_LOG_LEVEL}
TZ: ${TZ}
ports:
- "${RUSTFS_HOST_PORT}:9000"
volumes:
- rustfs_data:/data
- rustfs_logs:/logs
face-recognition:
image: ${FACE_RECOGNITION_IMAGE}
container_name: device-server-face-recognition
restart: unless-stopped
environment:
PORT: 5000
PYTHONUNBUFFERED: 1
TZ: ${TZ}
ports:
- "${FACE_RECOGNITION_HOST_PORT}:5000"
healthcheck:
test: ["CMD", "python", "-c", "import requests; requests.get('http://localhost:5000/health')"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
volumes:
mysql_data:
redis_data:
rustfs_data:
rustfs_logs: