再也不怕客户提供离线服务器,小白一学就会,基于Docker Compose实现Go Web项目本地一键打包前后端和依赖镜像,上传服务器一键部署

不要全是感谢分享,感觉好像在逛黄网 :laughing:

  • 客户提供的服务器没有外网,无法直接拉去镜像,但是你们公司只提供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:
84 个赞

感谢分享

2 个赞

好文分享

1 个赞

不错不错,感谢教程! :smiling_face_with_three_hearts:

2 个赞

学习了,谢谢分享

1 个赞

学习了感谢分享

2 个赞

感谢分享

2 个赞

感谢分享~

2 个赞

感谢分享

1 个赞

学习了,这个真有用

1 个赞

感谢佬~

1 个赞

谢谢分享 :hand_with_index_finger_and_thumb_crossed:

3 个赞

很好的教程,感谢分享 :folded_hands:

1 个赞

感谢佬友的精彩分享。

1 个赞

学习了,感谢大佬的分享。

3 个赞

芜湖,我必须立刻收藏

1 个赞

强呀!支持大佬

1 个赞

学习,感谢佬友分享~

3 个赞

这个教程做成视频就完美

5 个赞

感谢佬友分享。(如果是h网,更多的是好人一生平安)

3 个赞