feat: 新增 Docker Compose 一键部署(MySQL + 后端 + 前端)

- 新增 docker-compose.yml,按健康检查顺序启动 MySQL 8、Go 后端与 Nginx 前端
- 新增 server/web 多阶段 Dockerfile、nginx 反向代理配置与 .dockerignore
- 后端支持 KENAITO_* 环境变量覆盖配置,新增 /health 健康检查接口
- 补充中英文 README 与 Docker 部署文档、.env.example 环境变量模板
This commit is contained in:
2026-08-27 21:17:10 +08:00
parent 005bd2490c
commit d6a052e0a4
14 changed files with 393 additions and 17 deletions
+19
View File
@@ -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
+34 -3
View File
@@ -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 ```bash
cd server cd server
@@ -81,7 +112,7 @@ go build -o bin/server cmd/main.go
后端默认监听 `:8080`。 后端默认监听 `:8080`。
### 2. 启动前端 #### 2. 启动前端
```bash ```bash
cd web cd web
@@ -91,7 +122,7 @@ npm run dev
前端默认监听 `:3000`,自动代理 API 到后端。 前端默认监听 `:3000`,自动代理 API 到后端。
### 3. 访问 #### 3. 访问
打开 http://localhost:3000 打开 http://localhost:3000
+46 -1
View File
@@ -1,9 +1,54 @@
# Kenaito Config Center 客户端 Docker 部署指南 # 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 客户端 ## Go 客户端
```bash ```bash
+34 -3
View File
@@ -46,7 +46,38 @@ kenaito-conf/
## Quick Start ## 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 ```bash
cd server cd server
@@ -79,7 +110,7 @@ go build -o bin/server cmd/main.go
The backend listens on `:8080` by default. The backend listens on `:8080` by default.
### 2. Start Frontend #### 2. Start Frontend
```bash ```bash
cd web cd web
@@ -89,7 +120,7 @@ npm run dev
The frontend listens on `:3000` by default and automatically proxies API requests to the backend. The frontend listens on `:3000` by default and automatically proxies API requests to the backend.
### 3. Access #### 3. Access
Open http://localhost:3000 Open http://localhost:3000
+66
View File
@@ -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:
+6
View File
@@ -0,0 +1,6 @@
bin/
*.log
application-dev.properties
application-*.properties
!application-docker.properties
.DS_Store
+29
View File
@@ -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"]
+12
View File
@@ -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
+2
View File
@@ -44,12 +44,14 @@ func main() {
groupHandler := handler.NewGroupHandler() groupHandler := handler.NewGroupHandler()
profileHandler := handler.NewProfileHandler() profileHandler := handler.NewProfileHandler()
clientHandler := handler.NewClientHandler() clientHandler := handler.NewClientHandler()
healthHandler := handler.NewHealthHandler()
// init gin // init gin
r := gin.Default() r := gin.Default()
r.Use(middleware.CORS()) r.Use(middleware.CORS())
// public routes // public routes
r.GET("/health", healthHandler.Check)
r.POST("/user/login", authHandler.Login) r.POST("/user/login", authHandler.Login)
// admin routes (JWT auth required) // admin routes (JWT auth required)
+35 -10
View File
@@ -42,7 +42,10 @@ func Load(path string) error {
return err return err
} }
active := baseProps["active"] active := os.Getenv("KENAITO_ACTIVE")
if active == "" {
active = baseProps["active"]
}
if active == "" { if active == "" {
return fmt.Errorf("missing required config: active") return fmt.Errorf("missing required config: active")
} }
@@ -60,17 +63,18 @@ func Load(path string) error {
Cfg = &Config{} Cfg = &Config{}
Cfg.Server.Port = intProp(baseProps, "server.port", 8080) Cfg.Server.Port = envInt("KENAITO_SERVER_PORT", intProp(baseProps, "server.port", 8080))
setServerMode(active) setServerMode(active)
Cfg.Database.Host = baseProps["database.host"] // 环境变量优先,便于容器化部署时覆盖配置文件
Cfg.Database.Port = intProp(baseProps, "database.port", 3306) Cfg.Database.Host = envStr("KENAITO_DB_HOST", baseProps["database.host"])
Cfg.Database.Name = baseProps["database.name"] Cfg.Database.Port = envInt("KENAITO_DB_PORT", intProp(baseProps, "database.port", 3306))
Cfg.Database.User = baseProps["database.user"] Cfg.Database.Name = envStr("KENAITO_DB_NAME", baseProps["database.name"])
Cfg.Database.Password = baseProps["database.password"] 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.Secret = envStr("KENAITO_JWT_SECRET", baseProps["jwt.secret"])
Cfg.JWT.ExpireHours = intProp(baseProps, "jwt.expireHours", 24) Cfg.JWT.ExpireHours = envInt("KENAITO_JWT_EXPIRE_HOURS", intProp(baseProps, "jwt.expireHours", 24))
if Cfg.Database.Host == "" { if Cfg.Database.Host == "" {
return fmt.Errorf("missing required config: database.host") return fmt.Errorf("missing required config: database.host")
@@ -82,7 +86,7 @@ func Load(path string) error {
func setServerMode(active string) { func setServerMode(active string) {
if "daily" == active { if "daily" == active {
Cfg.Server.Mode = gin.DebugMode Cfg.Server.Mode = gin.DebugMode
} else if "stage" == active || "online" == active { } else if "stage" == active || "online" == active || "docker" == active {
Cfg.Server.Mode = gin.ReleaseMode Cfg.Server.Mode = gin.ReleaseMode
} else { } else {
Cfg.Server.Mode = gin.DebugMode Cfg.Server.Mode = gin.DebugMode
@@ -129,6 +133,27 @@ func intProp(props map[string]string, key string, defaultVal int) int {
return n 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 { 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" return d.User + ":" + d.Password + "@tcp(" + d.Host + ":" + strconv.Itoa(d.Port) + ")/" + d.Name + "?charset=utf8&parseTime=true&loc=Local"
} }
+27
View File
@@ -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"})
}
+4
View File
@@ -0,0 +1,4 @@
node_modules/
dist/
.DS_Store
*.log
+18
View File
@@ -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
+61
View File
@@ -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;
}
}