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:
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
+46
-1
@@ -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
|
||||
|
||||
+34
-3
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
@@ -0,0 +1,6 @@
|
||||
bin/
|
||||
*.log
|
||||
application-dev.properties
|
||||
application-*.properties
|
||||
!application-docker.properties
|
||||
.DS_Store
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"})
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.DS_Store
|
||||
*.log
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user