diff --git a/README.md b/README.md index dac2f2b..d8df885 100644 --- a/README.md +++ b/README.md @@ -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、备份升级)。 + +--- + ## 本地开发 ### 环境要求 diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..cd23ab5 --- /dev/null +++ b/deploy/README.md @@ -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 # 升级前建议备份 +``` diff --git a/deploy/docker/Dockerfile.api b/deploy/docker/Dockerfile.api new file mode 100644 index 0000000..335e8a9 --- /dev/null +++ b/deploy/docker/Dockerfile.api @@ -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"] diff --git a/deploy/docker/Dockerfile.web b/deploy/docker/Dockerfile.web new file mode 100644 index 0000000..7919aca --- /dev/null +++ b/deploy/docker/Dockerfile.web @@ -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 diff --git a/deploy/docker/nginx.conf b/deploy/docker/nginx.conf new file mode 100644 index 0000000..fcdcd87 --- /dev/null +++ b/deploy/docker/nginx.conf @@ -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; + } +} diff --git a/deploy/scripts/backup-db.sh b/deploy/scripts/backup-db.sh new file mode 100755 index 0000000..a10d6a4 --- /dev/null +++ b/deploy/scripts/backup-db.sh @@ -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}'))" diff --git a/deploy/scripts/deploy.sh b/deploy/scripts/deploy.sh new file mode 100755 index 0000000..bd2c30c --- /dev/null +++ b/deploy/scripts/deploy.sh @@ -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 "$@" diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..780819f --- /dev/null +++ b/docker-compose.prod.yml @@ -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: