From d6a052e0a4715012aebc0ab3521472f47902f0fe Mon Sep 17 00:00:00 2001 From: Odboy Date: Thu, 27 Aug 2026 21:17:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20Docker=20Compose?= =?UTF-8?q?=20=E4=B8=80=E9=94=AE=E9=83=A8=E7=BD=B2=EF=BC=88MySQL=20+=20?= =?UTF-8?q?=E5=90=8E=E7=AB=AF=20+=20=E5=89=8D=E7=AB=AF=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 docker-compose.yml,按健康检查顺序启动 MySQL 8、Go 后端与 Nginx 前端 - 新增 server/web 多阶段 Dockerfile、nginx 反向代理配置与 .dockerignore - 后端支持 KENAITO_* 环境变量覆盖配置,新增 /health 健康检查接口 - 补充中英文 README 与 Docker 部署文档、.env.example 环境变量模板 --- .env.example | 19 ++++++++ README.md | 37 ++++++++++++++-- README_DOCKERFILE.md | 47 +++++++++++++++++++- README_EN.md | 37 ++++++++++++++-- docker-compose.yml | 66 ++++++++++++++++++++++++++++ server/.dockerignore | 6 +++ server/Dockerfile | 29 ++++++++++++ server/application-docker.properties | 12 +++++ server/cmd/main.go | 2 + server/internal/config/config.go | 45 ++++++++++++++----- server/internal/handler/health.go | 27 ++++++++++++ web/.dockerignore | 4 ++ web/Dockerfile | 18 ++++++++ web/nginx.conf | 61 +++++++++++++++++++++++++ 14 files changed, 393 insertions(+), 17 deletions(-) create mode 100644 .env.example create mode 100644 docker-compose.yml create mode 100644 server/.dockerignore create mode 100644 server/Dockerfile create mode 100644 server/application-docker.properties create mode 100644 server/internal/handler/health.go create mode 100644 web/.dockerignore create mode 100644 web/Dockerfile create mode 100644 web/nginx.conf diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d51d4d6 --- /dev/null +++ b/.env.example @@ -0,0 +1,19 @@ +# Docker Compose 环境变量示例 +# 复制为 .env 后按需修改:cp .env.example .env + +# 对外访问端口 +WEB_PORT=3000 +SERVER_PORT=8080 + +# MySQL +MYSQL_ROOT_PASSWORD=root123456 +MYSQL_DATABASE=kenaito_conf +MYSQL_USER=kenaito +MYSQL_PASSWORD=kenaito123 + +# 后端 JWT +JWT_SECRET=kenaito-conf-docker-secret-change-me +JWT_EXPIRE_HOURS=24 + +# 时区 +TZ=Asia/Shanghai diff --git a/README.md b/README.md index f889d4b..d54fb08 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,38 @@ kenaito-conf/ ## 快速开始 -### 1. 启动后端 +### 方式一:Docker Compose 一键启动(推荐) + +前置条件:Docker Desktop 或 Docker Engine + Compose v2。 + +```bash +# 可选:按需修改端口、数据库密码和 JWT 密钥 +cp .env.example .env + +docker compose up -d --build +``` + +启动完成后: + +- 管理界面:http://localhost:3000 +- 后端接口:http://localhost:8080 +- 健康检查:http://localhost:8080/health +- 默认管理员:`admin` / `admin123` + +常用命令: + +```bash +docker compose ps # 查看服务与健康状态 +docker compose logs -f server +docker compose down # 停止服务,保留 MySQL 数据 +docker compose down -v # 停止服务并删除 MySQL 数据卷 +``` + +首次启动时后端会自动建表并初始化管理员账号。MySQL 仅在容器网络内暴露,不会占用宿主机 3306 端口。 + +### 方式二:本地开发启动 + +#### 1. 启动后端 ```bash cd server @@ -81,7 +112,7 @@ go build -o bin/server cmd/main.go 后端默认监听 `:8080`。 -### 2. 启动前端 +#### 2. 启动前端 ```bash cd web @@ -91,7 +122,7 @@ npm run dev 前端默认监听 `:3000`,自动代理 API 到后端。 -### 3. 访问 +#### 3. 访问 打开 http://localhost:3000 diff --git a/README_DOCKERFILE.md b/README_DOCKERFILE.md index b33e3be..4467c07 100644 --- a/README_DOCKERFILE.md +++ b/README_DOCKERFILE.md @@ -1,9 +1,54 @@ # Kenaito Config Center 客户端 Docker 部署指南 -本文档说明如何使用 Dockerfile 构建和测试各语言客户端 demo。 +本文档说明如何使用 Dockerfile 构建和测试各语言客户端 demo,以及如何使用 Docker Compose 一键部署管理端。 所有命令均在项目根目录执行。 +## 服务端一键部署(Docker Compose) + +```bash +# 可选:按需修改端口、数据库密码和 JWT 密钥 +cp .env.example .env + +docker compose up -d --build +``` + +Compose 会启动三个服务: + +| 服务 | 说明 | 默认端口 | +| --- | --- | --- | +| mysql | MySQL 8.0,数据持久化到 `mysql-data` 卷 | 仅容器网络 | +| server | Go 后端,首次启动自动建表并初始化管理员 | 8080 | +| web | Nginx 托管的 Vue 前端,并反向代理 `/api`、`/admin`、`/user` | 3000 | + +启动后访问 http://localhost:3000 ,默认管理员 `admin` / `admin123`。 + +常用命令: + +```bash +docker compose ps # 查看服务与健康状态 +docker compose logs -f server +docker compose down # 停止服务,保留 MySQL 数据 +docker compose down -v # 停止服务并删除 MySQL 数据卷 +``` + +环境变量说明(写入 `.env` 即可生效): + +| 变量 | 默认值 | 说明 | +| --- | --- | --- | +| WEB_PORT | 3000 | 前端对外端口 | +| SERVER_PORT | 8080 | 后端对外端口 | +| MYSQL_DATABASE | kenaito_conf | 数据库名 | +| MYSQL_USER / MYSQL_PASSWORD | kenaito / kenaito123 | 后端使用的数据库账号 | +| MYSQL_ROOT_PASSWORD | root123456 | MySQL root 密码 | +| JWT_SECRET | kenaito-conf-docker-secret-change-me | JWT 密钥,生产环境必须修改 | +| JWT_EXPIRE_HOURS | 24 | JWT 有效期(小时) | +| TZ | Asia/Shanghai | 容器时区 | + +后端配置文件为 `server/application-docker.properties`,以下 `KENAITO_*` 环境变量优先级更高:`KENAITO_ACTIVE`、`KENAITO_SERVER_PORT`、`KENAITO_DB_HOST`、`KENAITO_DB_PORT`、`KENAITO_DB_NAME`、`KENAITO_DB_USER`、`KENAITO_DB_PASSWORD`、`KENAITO_JWT_SECRET`、`KENAITO_JWT_EXPIRE_HOURS`。 + +后端健康检查接口:`GET /health`,数据库不可用时返回 503。 + ## Go 客户端 ```bash diff --git a/README_EN.md b/README_EN.md index 4e8b327..4bb152d 100644 --- a/README_EN.md +++ b/README_EN.md @@ -46,7 +46,38 @@ kenaito-conf/ ## Quick Start -### 1. Start Backend +### Option 1: Docker Compose (recommended) + +Prerequisites: Docker Desktop or Docker Engine with Compose v2. + +```bash +# Optional: adjust ports, database passwords and JWT secret +cp .env.example .env + +docker compose up -d --build +``` + +After startup: + +- Web UI: http://localhost:3000 +- Backend API: http://localhost:8080 +- Health check: http://localhost:8080/health +- Default admin: `admin` / `admin123` + +Useful commands: + +```bash +docker compose ps # show services and health status +docker compose logs -f server +docker compose down # stop services, keep MySQL data +docker compose down -v # stop services and remove the MySQL volume +``` + +On first startup the backend creates the tables and seeds the admin account automatically. MySQL is only exposed on the Compose network and does not occupy port 3306 on the host. + +### Option 2: Local development + +#### 1. Start Backend ```bash cd server @@ -79,7 +110,7 @@ go build -o bin/server cmd/main.go The backend listens on `:8080` by default. -### 2. Start Frontend +#### 2. Start Frontend ```bash cd web @@ -89,7 +120,7 @@ npm run dev The frontend listens on `:3000` by default and automatically proxies API requests to the backend. -### 3. Access +#### 3. Access Open http://localhost:3000 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f99ce1d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,66 @@ +services: + mysql: + image: mysql:8.0 + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_unicode_ci + environment: + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root123456} + MYSQL_DATABASE: ${MYSQL_DATABASE:-kenaito_conf} + MYSQL_USER: ${MYSQL_USER:-kenaito} + MYSQL_PASSWORD: ${MYSQL_PASSWORD:-kenaito123} + TZ: ${TZ:-Asia/Shanghai} + volumes: + - mysql-data:/var/lib/mysql + healthcheck: + test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p\"$$MYSQL_ROOT_PASSWORD\" --silent"] + interval: 5s + timeout: 5s + retries: 30 + start_period: 30s + restart: unless-stopped + + server: + build: + context: ./server + environment: + KENAITO_ACTIVE: docker + KENAITO_DB_HOST: mysql + KENAITO_DB_PORT: "3306" + KENAITO_DB_NAME: ${MYSQL_DATABASE:-kenaito_conf} + KENAITO_DB_USER: ${MYSQL_USER:-kenaito} + KENAITO_DB_PASSWORD: ${MYSQL_PASSWORD:-kenaito123} + KENAITO_JWT_SECRET: ${JWT_SECRET:-kenaito-conf-docker-secret-change-me} + KENAITO_JWT_EXPIRE_HOURS: ${JWT_EXPIRE_HOURS:-24} + TZ: ${TZ:-Asia/Shanghai} + depends_on: + mysql: + condition: service_healthy + ports: + - "${SERVER_PORT:-8080}:8080" + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/health >/dev/null || exit 1"] + interval: 10s + timeout: 3s + retries: 10 + start_period: 10s + restart: unless-stopped + + web: + build: + context: ./web + depends_on: + server: + condition: service_healthy + ports: + - "${WEB_PORT:-3000}:80" + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1/ >/dev/null || exit 1"] + interval: 10s + timeout: 3s + retries: 5 + start_period: 5s + restart: unless-stopped + +volumes: + mysql-data: diff --git a/server/.dockerignore b/server/.dockerignore new file mode 100644 index 0000000..cd6eb31 --- /dev/null +++ b/server/.dockerignore @@ -0,0 +1,6 @@ +bin/ +*.log +application-dev.properties +application-*.properties +!application-docker.properties +.DS_Store diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..88da91e --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,29 @@ +# Build stage +FROM golang:1.26-alpine AS build + +ENV GOPROXY=https://goproxy.cn,direct \ + CGO_ENABLED=0 + +WORKDIR /src + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . +RUN go build -trimpath -ldflags="-s -w" -o /out/server ./cmd/main.go + +# Runtime stage +FROM alpine:3.20 + +RUN apk add --no-cache ca-certificates tzdata wget + +WORKDIR /app + +COPY --from=build /out/server ./server +COPY application.properties application-docker.properties ./ + +ENV KENAITO_ACTIVE=docker + +EXPOSE 8080 + +ENTRYPOINT ["./server", "-config", "application.properties"] diff --git a/server/application-docker.properties b/server/application-docker.properties new file mode 100644 index 0000000..f27e957 --- /dev/null +++ b/server/application-docker.properties @@ -0,0 +1,12 @@ +# Docker Compose 部署使用的默认配置。 +# 所有配置项均可通过 KENAITO_* 环境变量覆盖,参见 .env.example。 +server.port=8080 + +database.host=mysql +database.port=3306 +database.name=kenaito_conf +database.user=kenaito +database.password=kenaito123 + +jwt.secret=kenaito-conf-docker-secret-change-me +jwt.expireHours=24 diff --git a/server/cmd/main.go b/server/cmd/main.go index e5d7c7e..67cfed3 100644 --- a/server/cmd/main.go +++ b/server/cmd/main.go @@ -44,12 +44,14 @@ func main() { groupHandler := handler.NewGroupHandler() profileHandler := handler.NewProfileHandler() clientHandler := handler.NewClientHandler() + healthHandler := handler.NewHealthHandler() // init gin r := gin.Default() r.Use(middleware.CORS()) // public routes + r.GET("/health", healthHandler.Check) r.POST("/user/login", authHandler.Login) // admin routes (JWT auth required) diff --git a/server/internal/config/config.go b/server/internal/config/config.go index 03d37c0..c51b1e0 100644 --- a/server/internal/config/config.go +++ b/server/internal/config/config.go @@ -42,7 +42,10 @@ func Load(path string) error { return err } - active := baseProps["active"] + active := os.Getenv("KENAITO_ACTIVE") + if active == "" { + active = baseProps["active"] + } if active == "" { return fmt.Errorf("missing required config: active") } @@ -60,17 +63,18 @@ func Load(path string) error { Cfg = &Config{} - Cfg.Server.Port = intProp(baseProps, "server.port", 8080) + Cfg.Server.Port = envInt("KENAITO_SERVER_PORT", intProp(baseProps, "server.port", 8080)) setServerMode(active) - Cfg.Database.Host = baseProps["database.host"] - Cfg.Database.Port = intProp(baseProps, "database.port", 3306) - Cfg.Database.Name = baseProps["database.name"] - Cfg.Database.User = baseProps["database.user"] - Cfg.Database.Password = baseProps["database.password"] + // 环境变量优先,便于容器化部署时覆盖配置文件 + Cfg.Database.Host = envStr("KENAITO_DB_HOST", baseProps["database.host"]) + Cfg.Database.Port = envInt("KENAITO_DB_PORT", intProp(baseProps, "database.port", 3306)) + Cfg.Database.Name = envStr("KENAITO_DB_NAME", baseProps["database.name"]) + Cfg.Database.User = envStr("KENAITO_DB_USER", baseProps["database.user"]) + Cfg.Database.Password = envStr("KENAITO_DB_PASSWORD", baseProps["database.password"]) - Cfg.JWT.Secret = baseProps["jwt.secret"] - Cfg.JWT.ExpireHours = intProp(baseProps, "jwt.expireHours", 24) + Cfg.JWT.Secret = envStr("KENAITO_JWT_SECRET", baseProps["jwt.secret"]) + Cfg.JWT.ExpireHours = envInt("KENAITO_JWT_EXPIRE_HOURS", intProp(baseProps, "jwt.expireHours", 24)) if Cfg.Database.Host == "" { return fmt.Errorf("missing required config: database.host") @@ -82,7 +86,7 @@ func Load(path string) error { func setServerMode(active string) { if "daily" == active { Cfg.Server.Mode = gin.DebugMode - } else if "stage" == active || "online" == active { + } else if "stage" == active || "online" == active || "docker" == active { Cfg.Server.Mode = gin.ReleaseMode } else { Cfg.Server.Mode = gin.DebugMode @@ -129,6 +133,27 @@ func intProp(props map[string]string, key string, defaultVal int) int { return n } +// envStr 返回环境变量值,未设置时使用 fallback +func envStr(key, fallback string) string { + if v := os.Getenv(key); v != "" { + return v + } + return fallback +} + +// envInt 返回整数环境变量值,未设置或非法时使用 fallback +func envInt(key string, fallback int) int { + v := os.Getenv(key) + if v == "" { + return fallback + } + n, err := strconv.Atoi(v) + if err != nil { + return fallback + } + return n +} + func (d *DatabaseConfig) DSN() string { return d.User + ":" + d.Password + "@tcp(" + d.Host + ":" + strconv.Itoa(d.Port) + ")/" + d.Name + "?charset=utf8&parseTime=true&loc=Local" } diff --git a/server/internal/handler/health.go b/server/internal/handler/health.go new file mode 100644 index 0000000..e1640fa --- /dev/null +++ b/server/internal/handler/health.go @@ -0,0 +1,27 @@ +package handler + +import ( + "kenaito-conf/server/internal/dao" + "net/http" + + "github.com/gin-gonic/gin" +) + +type HealthHandler struct{} + +func NewHealthHandler() *HealthHandler { + return &HealthHandler{} +} + +// Check 用于容器健康检查,数据库不可用时返回 503 +func (h *HealthHandler) Check(c *gin.Context) { + if dao.Engine == nil { + c.JSON(http.StatusServiceUnavailable, gin.H{"status": "error", "message": "database not initialized"}) + return + } + if err := dao.Engine.Ping(); err != nil { + c.JSON(http.StatusServiceUnavailable, gin.H{"status": "error", "message": "database unavailable"}) + return + } + c.JSON(http.StatusOK, gin.H{"status": "ok"}) +} diff --git a/web/.dockerignore b/web/.dockerignore new file mode 100644 index 0000000..9451024 --- /dev/null +++ b/web/.dockerignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +.DS_Store +*.log diff --git a/web/Dockerfile b/web/Dockerfile new file mode 100644 index 0000000..00d1190 --- /dev/null +++ b/web/Dockerfile @@ -0,0 +1,18 @@ +# Build stage +FROM node:20-alpine AS build + +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci --no-audit --no-fund + +COPY . . +RUN npm run build + +# Runtime stage +FROM nginx:1.27-alpine + +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/dist /usr/share/nginx/html + +EXPOSE 80 diff --git a/web/nginx.conf b/web/nginx.conf new file mode 100644 index 0000000..b617ee1 --- /dev/null +++ b/web/nginx.conf @@ -0,0 +1,61 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + client_max_body_size 10m; + + gzip on; + gzip_types text/plain text/css application/javascript application/json image/svg+xml; + gzip_min_length 1024; + + # 客户端配置拉取接口 + location /api/ { + proxy_pass http://server:8080; + 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; + } + + # 管理端接口 + location /admin/ { + proxy_pass http://server:8080; + 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; + } + + # 登录相关接口(精确匹配,避免与前端 /user/list 路由冲突) + location = /user/login { + proxy_pass http://server:8080; + 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; + } + + location = /user/logout { + proxy_pass http://server:8080; + 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; + } + + location = /user/loginUserInfo { + proxy_pass http://server:8080; + 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; + } + + # 前端单页应用 + location / { + try_files $uri $uri/ /index.html; + } +}