增加腾讯云生产 Docker 部署编排与一键脚本
- 提供 API/Web 多阶段镜像、Nginx 反代与 WS/文件路径 - docker-compose.prod 含 Postgres、MinIO、API、前端 - deploy.sh 生成密钥、构建启动、可选种子与备份脚本
This commit is contained in:
@@ -107,6 +107,21 @@ kefu_sys/
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 生产部署(腾讯云 Docker)
|
||||||
|
|
||||||
|
单机一键部署(Nginx + API + PostgreSQL + MinIO):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 服务器安装 Docker 后
|
||||||
|
chmod +x deploy/scripts/*.sh
|
||||||
|
./deploy/scripts/deploy.sh # 首次会生成 deploy/.env.prod,请改公网地址
|
||||||
|
./deploy/scripts/deploy.sh --seed # 可选:导入演示账号
|
||||||
|
```
|
||||||
|
|
||||||
|
说明见 **[deploy/README.md](deploy/README.md)**(安全组、HTTPS、COS 替换 MinIO、备份升级)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 本地开发
|
## 本地开发
|
||||||
|
|
||||||
### 环境要求
|
### 环境要求
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
# 生产部署(Docker / 腾讯云 CVM)
|
||||||
|
|
||||||
|
单机 Docker Compose 方案:`Nginx + API + PostgreSQL + MinIO`,适合腾讯云轻量/ CVM。
|
||||||
|
|
||||||
|
## 架构
|
||||||
|
|
||||||
|
```
|
||||||
|
Internet ──:80──▶ web(nginx)
|
||||||
|
├── / 前端 SPA
|
||||||
|
├── /api/* → api:8080(含 WebSocket)
|
||||||
|
├── /health → api
|
||||||
|
└── /files/* → minio:9000/kefu(图片公共读)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 服务器准备(腾讯云)
|
||||||
|
|
||||||
|
1. 购买 **CVM / 轻量应用服务器**(建议 2 核 4G 起,系统 Ubuntu 22.04)
|
||||||
|
2. **安全组**放行:`22`、`80`(有证书再放 `443`)
|
||||||
|
3. 安装 Docker:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ubuntu 示例
|
||||||
|
curl -fsSL https://get.docker.com | sh
|
||||||
|
sudo usermod -aG docker "$USER"
|
||||||
|
# 重新登录后 docker 无需 sudo
|
||||||
|
docker compose version
|
||||||
|
```
|
||||||
|
|
||||||
|
4. 上传代码到服务器,例如:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <你的仓库地址> kefu_sys
|
||||||
|
cd kefu_sys
|
||||||
|
```
|
||||||
|
|
||||||
|
## 一键部署
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x deploy/scripts/*.sh
|
||||||
|
|
||||||
|
# 首次会生成 deploy/.env.prod 并提示你改域名/公网地址
|
||||||
|
./deploy/scripts/deploy.sh
|
||||||
|
|
||||||
|
# 需要演示账号时(kefu_admin / kefu_admin123 等)
|
||||||
|
./deploy/scripts/deploy.sh --seed
|
||||||
|
```
|
||||||
|
|
||||||
|
### 必改配置(`deploy/.env.prod`)
|
||||||
|
|
||||||
|
| 变量 | 示例 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `APP_BASE_URL` | `http://1.2.3.4` 或 `https://kefu.xx.com` | 浏览器访问根地址 |
|
||||||
|
| `STORAGE_PUBLIC_BASE_URL` | `http://1.2.3.4/files` | **必须**与 Nginx `/files` 一致 |
|
||||||
|
| `HTTP_PORT` | `80` | 宿主机端口 |
|
||||||
|
| `DB_PASSWORD` / `JWT_SECRET` / `STORAGE_*` | 强随机 | 脚本可自动生成密钥 |
|
||||||
|
|
||||||
|
脚本会校验占位符是否改掉。
|
||||||
|
|
||||||
|
### 常用命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy/scripts/deploy.sh --status # 状态
|
||||||
|
./deploy/scripts/deploy.sh --down # 停容器(保留卷)
|
||||||
|
./deploy/scripts/deploy.sh --seed-only # 仅跑种子
|
||||||
|
./deploy/scripts/backup-db.sh # 备份数据库到 ./backups
|
||||||
|
|
||||||
|
docker compose -f docker-compose.prod.yml --env-file deploy/.env.prod logs -f api
|
||||||
|
docker compose -f docker-compose.prod.yml --env-file deploy/.env.prod logs -f web
|
||||||
|
```
|
||||||
|
|
||||||
|
## HTTPS 建议(腾讯云)
|
||||||
|
|
||||||
|
任选其一:
|
||||||
|
|
||||||
|
1. **CLB / CDN** 终结 HTTPS,回源 CVM `80`(最简单)
|
||||||
|
2. 本机 **Caddy / Nginx + Let’s Encrypt** 反代到本 compose 的 `80`
|
||||||
|
3. 把 `web` 容器映射改到 `8080`,宿主机 Nginx 做 443 → 8080
|
||||||
|
|
||||||
|
配置 HTTPS 后,请同步把 `APP_BASE_URL`、`STORAGE_PUBLIC_BASE_URL` 改成 `https://…`。
|
||||||
|
|
||||||
|
## 使用腾讯云 COS 替代 MinIO(可选)
|
||||||
|
|
||||||
|
1. 创建 COS 桶,开启公共读或使用 CDN 域名
|
||||||
|
2. 在 `.env.prod` 配置 S3 兼容参数(见 `.env.prod.example` 注释)
|
||||||
|
3. 从 `docker-compose.prod.yml` 中去掉 `minio` / `minio-init` 依赖,并修改 Nginx,去掉 `/files` 反代(图片直链 COS)
|
||||||
|
|
||||||
|
当前一键脚本默认 **内置 MinIO**,零额外云产品即可跑通。
|
||||||
|
|
||||||
|
## 默认种子账号(仅 --seed 后)
|
||||||
|
|
||||||
|
| 角色 | 用户名 | 密码 |
|
||||||
|
|------|--------|------|
|
||||||
|
| 平台管理员 | `platform_admin` | `kefu_admin123` |
|
||||||
|
| 租户管理员 | `kefu_admin` | `kefu_admin123` |
|
||||||
|
| 坐席 | `agent1` 等 | `kefu_admin123` |
|
||||||
|
|
||||||
|
**上线后立即修改密码或关闭 seed 账号。**
|
||||||
|
|
||||||
|
## 目录说明
|
||||||
|
|
||||||
|
| 路径 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `docker-compose.prod.yml` | 生产编排 |
|
||||||
|
| `deploy/docker/Dockerfile.api` | Go API 镜像 |
|
||||||
|
| `deploy/docker/Dockerfile.web` | 前端构建 + Nginx |
|
||||||
|
| `deploy/docker/nginx.conf` | 反代与 SPA |
|
||||||
|
| `deploy/.env.prod.example` | 环境变量模板 |
|
||||||
|
| `deploy/scripts/deploy.sh` | 一键部署 |
|
||||||
|
| `deploy/scripts/backup-db.sh` | 数据库备份 |
|
||||||
|
|
||||||
|
## 故障排查
|
||||||
|
|
||||||
|
| 现象 | 处理 |
|
||||||
|
|------|------|
|
||||||
|
| 页面 502 | `docker compose … logs api`,检查 DB 是否 healthy |
|
||||||
|
| 图片打不开 | 检查 `STORAGE_PUBLIC_BASE_URL` 是否为 `…/files`,MinIO 桶是否 public download |
|
||||||
|
| WebSocket 断 | 确认走同源 `/api/ws`,CLB 需开启 WebSocket/长连接 |
|
||||||
|
| 构建 Go 失败 | 服务器内存不足时加 swap,或本机 build 后 `docker save` 上传 |
|
||||||
|
|
||||||
|
## 升级
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git pull
|
||||||
|
./deploy/scripts/deploy.sh # 重新 build 并滚动启动
|
||||||
|
./deploy/scripts/backup-db.sh # 升级前建议备份
|
||||||
|
```
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# 客服云 API — 生产镜像(多阶段构建)
|
||||||
|
# build context: 仓库根目录
|
||||||
|
# docker build -f deploy/docker/Dockerfile.api -t kefu-api:latest .
|
||||||
|
|
||||||
|
FROM golang:1.24-alpine AS builder
|
||||||
|
|
||||||
|
# 若 go.mod 要求更高版本,自动下载 toolchain
|
||||||
|
ENV GOTOOLCHAIN=auto
|
||||||
|
ENV CGO_ENABLED=0
|
||||||
|
|
||||||
|
RUN apk add --no-cache git ca-certificates tzdata
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
# 先下依赖,利用缓存
|
||||||
|
COPY server/go.mod server/go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY server/ ./
|
||||||
|
RUN GOOS=linux GOARCH=amd64 \
|
||||||
|
go build -trimpath -ldflags="-s -w" -o /out/server ./cmd/main.go
|
||||||
|
|
||||||
|
# 种子程序(compose profile seed 使用)
|
||||||
|
RUN GOOS=linux GOARCH=amd64 \
|
||||||
|
go build -trimpath -ldflags="-s -w" -o /out/seed ./cmd/seed
|
||||||
|
|
||||||
|
FROM alpine:3.21
|
||||||
|
|
||||||
|
RUN apk add --no-cache ca-certificates tzdata wget \
|
||||||
|
&& adduser -D -H -u 10001 kefu
|
||||||
|
|
||||||
|
ENV TZ=Asia/Shanghai
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=builder /out/server /app/server
|
||||||
|
COPY --from=builder /out/seed /app/seed
|
||||||
|
|
||||||
|
USER kefu
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=15s --timeout=5s --start-period=20s --retries=5 \
|
||||||
|
CMD wget -qO- http://127.0.0.1:8080/health || exit 1
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/server"]
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# 客服云前端 — Nginx 托管静态资源并反代 API / 文件
|
||||||
|
# build context: 仓库根目录
|
||||||
|
|
||||||
|
FROM node:22-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /web
|
||||||
|
COPY web/package.json web/package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY web/ ./
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM nginx:1.27-alpine
|
||||||
|
|
||||||
|
RUN apk add --no-cache tzdata \
|
||||||
|
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||||
|
&& echo Asia/Shanghai > /etc/timezone
|
||||||
|
|
||||||
|
COPY deploy/docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY --from=builder /web/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=15s --timeout=5s --start-period=10s --retries=5 \
|
||||||
|
CMD wget -qO- http://127.0.0.1/healthz || exit 1
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# 生产 Nginx:SPA + API/WebSocket 反代 + 对象存储公共读路径
|
||||||
|
# 上游服务名与 docker-compose.prod.yml 中 service 名一致
|
||||||
|
|
||||||
|
map $http_upgrade $connection_upgrade {
|
||||||
|
default upgrade;
|
||||||
|
'' close;
|
||||||
|
}
|
||||||
|
|
||||||
|
upstream kefu_api {
|
||||||
|
server api:8080;
|
||||||
|
keepalive 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
upstream kefu_minio {
|
||||||
|
server minio:9000;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
client_max_body_size 20m;
|
||||||
|
# 上传/长连接
|
||||||
|
proxy_read_timeout 3600s;
|
||||||
|
proxy_send_timeout 3600s;
|
||||||
|
|
||||||
|
# 健康探针(不经 API)
|
||||||
|
location = /healthz {
|
||||||
|
access_log off;
|
||||||
|
add_header Content-Type text/plain;
|
||||||
|
return 200 'ok';
|
||||||
|
}
|
||||||
|
|
||||||
|
# 后端健康检查
|
||||||
|
location = /health {
|
||||||
|
proxy_pass http://kefu_api/health;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
# API + WebSocket(/api/ws、/api/widget/ws)
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://kefu_api/api/;
|
||||||
|
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 $connection_upgrade;
|
||||||
|
proxy_buffering off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 图片公共访问:浏览器访问 /files/* → MinIO bucket kefu/*
|
||||||
|
# 须与 STORAGE_PUBLIC_BASE_URL=https://你的域名/files 一致
|
||||||
|
location /files/ {
|
||||||
|
proxy_pass http://kefu_minio/kefu/;
|
||||||
|
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;
|
||||||
|
# 允许跨域拉取图片(嵌入站点)
|
||||||
|
add_header Access-Control-Allow-Origin * always;
|
||||||
|
add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS" always;
|
||||||
|
if ($request_method = OPTIONS) {
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
expires 7d;
|
||||||
|
add_header Cache-Control "public" always;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 前端静态资源
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 构建产物长缓存
|
||||||
|
location /assets/ {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
expires 30d;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+32
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 备份生产 Postgres 数据
|
||||||
|
# 用法:./deploy/scripts/backup-db.sh [输出目录]
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
|
ENV_FILE="deploy/.env.prod"
|
||||||
|
OUT_DIR="${1:-./backups}"
|
||||||
|
COMPOSE_FILE="docker-compose.prod.yml"
|
||||||
|
|
||||||
|
if [[ ! -f "$ENV_FILE" ]]; then
|
||||||
|
echo "缺少 $ENV_FILE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
set -a
|
||||||
|
source <(sed 's/\r$//' "$ENV_FILE" | grep -v '^\s*#' | grep -v '^\s*$' || true)
|
||||||
|
set +a
|
||||||
|
|
||||||
|
mkdir -p "$OUT_DIR"
|
||||||
|
STAMP="$(date +%Y%m%d_%H%M%S)"
|
||||||
|
FILE="${OUT_DIR}/kefu_pg_${STAMP}.sql.gz"
|
||||||
|
|
||||||
|
echo "备份到 $FILE …"
|
||||||
|
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" exec -T postgres \
|
||||||
|
pg_dump -U "${DB_USER:-kefu}" "${DB_NAME:-kefu_sys}" | gzip > "$FILE"
|
||||||
|
|
||||||
|
echo "完成: $FILE ($(du -h "$FILE" | awk '{print $1}'))"
|
||||||
Executable
+252
@@ -0,0 +1,252 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 客服云 — 腾讯云 / 任意 Linux 一键生产部署
|
||||||
|
# 用法:
|
||||||
|
# ./deploy/scripts/deploy.sh # 构建并启动
|
||||||
|
# ./deploy/scripts/deploy.sh --seed # 启动后导入演示账号数据
|
||||||
|
# ./deploy/scripts/deploy.sh --pull-only # 仅 pull/up,不强制 rebuild(需已有镜像)
|
||||||
|
# ./deploy/scripts/deploy.sh --down # 停止并移除容器(保留数据卷)
|
||||||
|
# ./deploy/scripts/deploy.sh --status # 查看状态
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
|
COMPOSE_FILE="docker-compose.prod.yml"
|
||||||
|
ENV_FILE="deploy/.env.prod"
|
||||||
|
ENV_EXAMPLE="deploy/.env.prod.example"
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log() { echo -e "${GREEN}[deploy]${NC} $*"; }
|
||||||
|
warn() { echo -e "${YELLOW}[warn]${NC} $*"; }
|
||||||
|
err() { echo -e "${RED}[error]${NC} $*" >&2; }
|
||||||
|
|
||||||
|
need_cmd() {
|
||||||
|
if ! command -v "$1" >/dev/null 2>&1; then
|
||||||
|
err "缺少命令: $1"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
compose() {
|
||||||
|
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
rand_hex() {
|
||||||
|
if command -v openssl >/dev/null 2>&1; then
|
||||||
|
openssl rand -hex 24
|
||||||
|
else
|
||||||
|
head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n' | head -c 48
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_env() {
|
||||||
|
if [[ ! -f "$ENV_FILE" ]]; then
|
||||||
|
if [[ ! -f "$ENV_EXAMPLE" ]]; then
|
||||||
|
err "找不到 $ENV_EXAMPLE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log "未找到 $ENV_FILE,从示例生成…"
|
||||||
|
cp "$ENV_EXAMPLE" "$ENV_FILE"
|
||||||
|
|
||||||
|
local jwt dbpass minio_pass
|
||||||
|
jwt="$(rand_hex)"
|
||||||
|
dbpass="$(rand_hex)"
|
||||||
|
minio_pass="K$(rand_hex | head -c 16)"
|
||||||
|
|
||||||
|
# macOS / GNU sed 兼容
|
||||||
|
if sed --version >/dev/null 2>&1; then
|
||||||
|
sed -i "s|^JWT_SECRET=.*|JWT_SECRET=${jwt}|" "$ENV_FILE"
|
||||||
|
sed -i "s|^DB_PASSWORD=.*|DB_PASSWORD=${dbpass}|" "$ENV_FILE"
|
||||||
|
sed -i "s|^STORAGE_SECRET_KEY=.*|STORAGE_SECRET_KEY=${minio_pass}|" "$ENV_FILE"
|
||||||
|
else
|
||||||
|
sed -i '' "s|^JWT_SECRET=.*|JWT_SECRET=${jwt}|" "$ENV_FILE"
|
||||||
|
sed -i '' "s|^DB_PASSWORD=.*|DB_PASSWORD=${dbpass}|" "$ENV_FILE"
|
||||||
|
sed -i '' "s|^STORAGE_SECRET_KEY=.*|STORAGE_SECRET_KEY=${minio_pass}|" "$ENV_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
warn "已生成随机 JWT_SECRET / DB_PASSWORD / STORAGE_SECRET_KEY"
|
||||||
|
warn "请编辑 $ENV_FILE,至少设置:"
|
||||||
|
warn " APP_BASE_URL=http://你的公网IP或域名"
|
||||||
|
warn " STORAGE_PUBLIC_BASE_URL=http://你的公网IP或域名/files"
|
||||||
|
echo
|
||||||
|
read -r -p "设置完成后按回车继续(或 Ctrl+C 退出先改配置): " _
|
||||||
|
fi
|
||||||
|
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
set -a
|
||||||
|
# 去除 Windows CRLF
|
||||||
|
source <(sed 's/\r$//' "$ENV_FILE" | grep -v '^\s*#' | grep -v '^\s*$' || true)
|
||||||
|
set +a
|
||||||
|
|
||||||
|
if [[ -z "${JWT_SECRET:-}" || "${JWT_SECRET}" == *"请改"* ]]; then
|
||||||
|
err "请在 $ENV_FILE 中设置有效的 JWT_SECRET"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ -z "${DB_PASSWORD:-}" || "${DB_PASSWORD}" == *"请改"* ]]; then
|
||||||
|
err "请在 $ENV_FILE 中设置有效的 DB_PASSWORD"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ -z "${STORAGE_PUBLIC_BASE_URL:-}" || "${STORAGE_PUBLIC_BASE_URL}" == *"你的"* ]]; then
|
||||||
|
err "请在 $ENV_FILE 中设置 STORAGE_PUBLIC_BASE_URL(如 http://1.2.3.4/files)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ -z "${STORAGE_ACCESS_KEY:-}" || -z "${STORAGE_SECRET_KEY:-}" ]]; then
|
||||||
|
err "请设置 STORAGE_ACCESS_KEY / STORAGE_SECRET_KEY"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ ${#STORAGE_SECRET_KEY} -lt 8 ]]; then
|
||||||
|
err "STORAGE_SECRET_KEY 长度至少 8 位(MinIO 要求)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_healthy() {
|
||||||
|
local url="${1:-http://127.0.0.1:${HTTP_PORT:-80}/health}"
|
||||||
|
local i
|
||||||
|
log "等待服务就绪: $url"
|
||||||
|
for i in $(seq 1 60); do
|
||||||
|
if curl -fsS "$url" >/dev/null 2>&1; then
|
||||||
|
log "健康检查通过"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
err "健康检查超时,请执行: docker compose -f $COMPOSE_FILE --env-file $ENV_FILE logs"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
print_summary() {
|
||||||
|
local base="${APP_BASE_URL:-http://127.0.0.1:${HTTP_PORT:-80}}"
|
||||||
|
echo
|
||||||
|
log "========== 部署完成 =========="
|
||||||
|
echo " 访问地址: ${base}"
|
||||||
|
echo " 登录页: ${base}/login"
|
||||||
|
echo " 健康检查: ${base}/health"
|
||||||
|
echo " Widget 预览: ${base}/widget/preview"
|
||||||
|
echo " 图片前缀: ${STORAGE_PUBLIC_BASE_URL}"
|
||||||
|
echo
|
||||||
|
echo " 常用命令:"
|
||||||
|
echo " 查看状态 ./deploy/scripts/deploy.sh --status"
|
||||||
|
echo " 查看日志 docker compose -f $COMPOSE_FILE --env-file $ENV_FILE logs -f"
|
||||||
|
echo " 导入种子 ./deploy/scripts/deploy.sh --seed-only"
|
||||||
|
echo " 停止服务 ./deploy/scripts/deploy.sh --down"
|
||||||
|
echo
|
||||||
|
echo " 安全提示: 安全组仅放行 80/443;改默认密码;生产务必使用 HTTPS。"
|
||||||
|
echo "=============================="
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_up() {
|
||||||
|
local seed=0 rebuild=1
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--seed) seed=1 ;;
|
||||||
|
--pull-only) rebuild=0 ;;
|
||||||
|
*) ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
need_cmd docker
|
||||||
|
need_cmd curl
|
||||||
|
if ! docker compose version >/dev/null 2>&1; then
|
||||||
|
err "需要 Docker Compose V2(docker compose)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ensure_env
|
||||||
|
|
||||||
|
log "拉取基础镜像…"
|
||||||
|
compose pull postgres minio minio-init || true
|
||||||
|
|
||||||
|
if [[ "$rebuild" -eq 1 ]]; then
|
||||||
|
log "构建并启动全部服务(api + web + db + minio)…"
|
||||||
|
compose up -d --build --remove-orphans
|
||||||
|
else
|
||||||
|
log "启动服务(不强制 rebuild)…"
|
||||||
|
compose up -d --remove-orphans
|
||||||
|
fi
|
||||||
|
|
||||||
|
wait_healthy "http://127.0.0.1:${HTTP_PORT:-80}/health" || true
|
||||||
|
wait_healthy "http://127.0.0.1:${HTTP_PORT:-80}/healthz" || true
|
||||||
|
|
||||||
|
if [[ "$seed" -eq 1 ]]; then
|
||||||
|
log "导入演示种子数据…"
|
||||||
|
compose --profile seed run --rm seed || warn "种子导入失败(可能已存在数据)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
compose ps
|
||||||
|
print_summary
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_seed_only() {
|
||||||
|
need_cmd docker
|
||||||
|
ensure_env
|
||||||
|
log "运行 seed…"
|
||||||
|
compose --profile seed run --rm seed
|
||||||
|
log "种子完成。默认租户管理员 kefu_admin / kefu_admin123 (若 seed 成功)"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_down() {
|
||||||
|
need_cmd docker
|
||||||
|
ensure_env
|
||||||
|
log "停止容器(保留数据卷)…"
|
||||||
|
compose down
|
||||||
|
log "已停止。删除数据卷请手动: docker volume rm kefu-prod_kefu_pgdata kefu-prod_kefu_miniodata"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_status() {
|
||||||
|
need_cmd docker
|
||||||
|
if [[ -f "$ENV_FILE" ]]; then
|
||||||
|
compose ps
|
||||||
|
echo
|
||||||
|
curl -fsS "http://127.0.0.1:${HTTP_PORT:-80}/health" && echo || warn "health 不可达"
|
||||||
|
else
|
||||||
|
err "缺少 $ENV_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
local action="${1:-up}"
|
||||||
|
case "$action" in
|
||||||
|
up|--up|"")
|
||||||
|
shift || true
|
||||||
|
cmd_up "$@"
|
||||||
|
;;
|
||||||
|
--seed)
|
||||||
|
# 兼容:./deploy.sh --seed
|
||||||
|
cmd_up --seed
|
||||||
|
;;
|
||||||
|
--seed-only)
|
||||||
|
cmd_seed_only
|
||||||
|
;;
|
||||||
|
--down|down)
|
||||||
|
cmd_down
|
||||||
|
;;
|
||||||
|
--status|status)
|
||||||
|
# 尝试读 HTTP_PORT
|
||||||
|
if [[ -f "$ENV_FILE" ]]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
set -a; source <(sed 's/\r$//' "$ENV_FILE" | grep -v '^\s*#' | grep -v '^\s*$' || true); set +a
|
||||||
|
fi
|
||||||
|
cmd_status
|
||||||
|
;;
|
||||||
|
--pull-only)
|
||||||
|
cmd_up --pull-only
|
||||||
|
;;
|
||||||
|
-h|--help|help)
|
||||||
|
sed -n '2,12p' "$0"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
err "未知参数: $action (见 --help)"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
# 客服云 — 生产 Docker Compose(适合腾讯云 CVM 单机)
|
||||||
|
# 使用:./deploy/scripts/deploy.sh
|
||||||
|
# 或:docker compose -f docker-compose.prod.yml --env-file deploy/.env.prod up -d --build
|
||||||
|
|
||||||
|
name: kefu-prod
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: kefu-prod-db
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: ${DB_USER:-kefu}
|
||||||
|
POSTGRES_PASSWORD: ${DB_PASSWORD:?请在 deploy/.env.prod 设置 DB_PASSWORD}
|
||||||
|
POSTGRES_DB: ${DB_NAME:-kefu_sys}
|
||||||
|
volumes:
|
||||||
|
- kefu_pgdata:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- kefu_net
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-kefu} -d ${DB_NAME:-kefu_sys}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
start_period: 20s
|
||||||
|
|
||||||
|
minio:
|
||||||
|
image: minio/minio:latest
|
||||||
|
container_name: kefu-prod-minio
|
||||||
|
restart: unless-stopped
|
||||||
|
command: server /data --console-address ":9001"
|
||||||
|
environment:
|
||||||
|
MINIO_ROOT_USER: ${STORAGE_ACCESS_KEY:?请设置 STORAGE_ACCESS_KEY}
|
||||||
|
MINIO_ROOT_PASSWORD: ${STORAGE_SECRET_KEY:?请设置 STORAGE_SECRET_KEY}
|
||||||
|
volumes:
|
||||||
|
- kefu_miniodata:/data
|
||||||
|
networks:
|
||||||
|
- kefu_net
|
||||||
|
# 官方镜像极简,无 curl/bash;由 minio-init 重试建桶保证就绪
|
||||||
|
|
||||||
|
minio-init:
|
||||||
|
image: minio/mc:latest
|
||||||
|
container_name: kefu-prod-minio-init
|
||||||
|
depends_on:
|
||||||
|
minio:
|
||||||
|
condition: service_started
|
||||||
|
restart: "no"
|
||||||
|
networks:
|
||||||
|
- kefu_net
|
||||||
|
entrypoint: >
|
||||||
|
/bin/sh -c "
|
||||||
|
set -e;
|
||||||
|
i=0;
|
||||||
|
until mc alias set local http://minio:9000 \"$$MINIO_ROOT_USER\" \"$$MINIO_ROOT_PASSWORD\" 2>/dev/null; do
|
||||||
|
i=$$((i+1));
|
||||||
|
if [ $$i -gt 30 ]; then echo 'minio not ready'; exit 1; fi;
|
||||||
|
echo waiting minio...;
|
||||||
|
sleep 2;
|
||||||
|
done;
|
||||||
|
mc mb -p local/$${STORAGE_BUCKET:-kefu} || true;
|
||||||
|
mc anonymous set download local/$${STORAGE_BUCKET:-kefu};
|
||||||
|
echo 'MinIO bucket ready';
|
||||||
|
"
|
||||||
|
environment:
|
||||||
|
MINIO_ROOT_USER: ${STORAGE_ACCESS_KEY}
|
||||||
|
MINIO_ROOT_PASSWORD: ${STORAGE_SECRET_KEY}
|
||||||
|
STORAGE_BUCKET: ${STORAGE_BUCKET:-kefu}
|
||||||
|
|
||||||
|
api:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: deploy/docker/Dockerfile.api
|
||||||
|
image: kefu-api:${IMAGE_TAG:-latest}
|
||||||
|
container_name: kefu-prod-api
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
minio-init:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
environment:
|
||||||
|
SERVER_PORT: "8080"
|
||||||
|
GIN_MODE: ${GIN_MODE:-release}
|
||||||
|
DB_HOST: postgres
|
||||||
|
DB_PORT: "5432"
|
||||||
|
DB_USER: ${DB_USER:-kefu}
|
||||||
|
DB_PASSWORD: ${DB_PASSWORD}
|
||||||
|
DB_NAME: ${DB_NAME:-kefu_sys}
|
||||||
|
DB_SSLMODE: ${DB_SSLMODE:-disable}
|
||||||
|
JWT_SECRET: ${JWT_SECRET:?请设置 JWT_SECRET}
|
||||||
|
STORAGE_ENDPOINT: minio:9000
|
||||||
|
STORAGE_ACCESS_KEY: ${STORAGE_ACCESS_KEY}
|
||||||
|
STORAGE_SECRET_KEY: ${STORAGE_SECRET_KEY}
|
||||||
|
STORAGE_BUCKET: ${STORAGE_BUCKET:-kefu}
|
||||||
|
STORAGE_USE_SSL: "false"
|
||||||
|
STORAGE_REGION: ${STORAGE_REGION:-us-east-1}
|
||||||
|
# 浏览器访问图片的公网前缀(经 Nginx /files 反代)
|
||||||
|
STORAGE_PUBLIC_BASE_URL: ${STORAGE_PUBLIC_BASE_URL:?请设置 STORAGE_PUBLIC_BASE_URL,如 https://你的域名/files}
|
||||||
|
STORAGE_MAX_UPLOAD_MB: ${STORAGE_MAX_UPLOAD_MB:-10}
|
||||||
|
STORAGE_MAX_IMAGE_EDGE: ${STORAGE_MAX_IMAGE_EDGE:-1920}
|
||||||
|
STORAGE_WEBP_QUALITY: ${STORAGE_WEBP_QUALITY:-80}
|
||||||
|
networks:
|
||||||
|
- kefu_net
|
||||||
|
# 不对外暴露 8080,统一走 nginx
|
||||||
|
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: deploy/docker/Dockerfile.web
|
||||||
|
image: kefu-web:${IMAGE_TAG:-latest}
|
||||||
|
container_name: kefu-prod-web
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
api:
|
||||||
|
condition: service_healthy
|
||||||
|
minio:
|
||||||
|
condition: service_started
|
||||||
|
ports:
|
||||||
|
- "${HTTP_PORT:-80}:80"
|
||||||
|
networks:
|
||||||
|
- kefu_net
|
||||||
|
|
||||||
|
# 可选:导入演示数据(profile seed)
|
||||||
|
# docker compose -f docker-compose.prod.yml --env-file deploy/.env.prod --profile seed run --rm seed
|
||||||
|
seed:
|
||||||
|
image: kefu-api:${IMAGE_TAG:-latest}
|
||||||
|
container_name: kefu-prod-seed
|
||||||
|
profiles: ["seed"]
|
||||||
|
depends_on:
|
||||||
|
api:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
DB_HOST: postgres
|
||||||
|
DB_PORT: "5432"
|
||||||
|
DB_USER: ${DB_USER:-kefu}
|
||||||
|
DB_PASSWORD: ${DB_PASSWORD}
|
||||||
|
DB_NAME: ${DB_NAME:-kefu_sys}
|
||||||
|
DB_SSLMODE: ${DB_SSLMODE:-disable}
|
||||||
|
JWT_SECRET: ${JWT_SECRET}
|
||||||
|
STORAGE_ENDPOINT: minio:9000
|
||||||
|
STORAGE_ACCESS_KEY: ${STORAGE_ACCESS_KEY}
|
||||||
|
STORAGE_SECRET_KEY: ${STORAGE_SECRET_KEY}
|
||||||
|
STORAGE_BUCKET: ${STORAGE_BUCKET:-kefu}
|
||||||
|
STORAGE_USE_SSL: "false"
|
||||||
|
STORAGE_PUBLIC_BASE_URL: ${STORAGE_PUBLIC_BASE_URL}
|
||||||
|
entrypoint: ["/app/seed"]
|
||||||
|
networks:
|
||||||
|
- kefu_net
|
||||||
|
restart: "no"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
kefu_net:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
kefu_pgdata:
|
||||||
|
kefu_miniodata:
|
||||||
Reference in New Issue
Block a user