打包成docker镜像
This commit is contained in:
parent
7c407db78b
commit
5d1b088e06
49
.dockerignore
Normal file
49
.dockerignore
Normal file
@ -0,0 +1,49 @@
|
||||
# Git 相关
|
||||
.git
|
||||
.gitignore
|
||||
.github
|
||||
|
||||
# 文档
|
||||
*.md
|
||||
LICENSE
|
||||
|
||||
# IDE 配置
|
||||
.vscode
|
||||
.idea
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Go 相关
|
||||
bin/
|
||||
*.exe
|
||||
*.test
|
||||
*.out
|
||||
|
||||
# 前端相关
|
||||
web/node_modules/
|
||||
web/dist/
|
||||
web/.vite/
|
||||
web/yarn-error.log
|
||||
web/package-lock.json
|
||||
|
||||
# 环境变量和配置
|
||||
.env
|
||||
.env.local
|
||||
*.local
|
||||
|
||||
# 日志
|
||||
*.log
|
||||
|
||||
# 临时文件
|
||||
tmp/
|
||||
temp/
|
||||
*.tmp
|
||||
|
||||
# OS 相关
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Docker 相关
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
docker-compose.yml
|
||||
253
DOCKER.md
Normal file
253
DOCKER.md
Normal file
@ -0,0 +1,253 @@
|
||||
# Docker 部署指南
|
||||
|
||||
本文档说明如何使用 Docker 部署 AnKao 应用。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 使用 Docker Compose (推荐)
|
||||
|
||||
最简单的方式是使用 docker-compose,它会自动启动数据库和应用:
|
||||
|
||||
```bash
|
||||
# 构建并启动所有服务
|
||||
docker-compose up -d
|
||||
|
||||
# 查看日志
|
||||
docker-compose logs -f
|
||||
|
||||
# 停止服务
|
||||
docker-compose down
|
||||
|
||||
# 停止并删除数据
|
||||
docker-compose down -v
|
||||
```
|
||||
|
||||
访问应用: http://localhost:8080
|
||||
|
||||
### 手动使用 Docker
|
||||
|
||||
如果你想单独构建和运行容器:
|
||||
|
||||
#### 1. 构建镜像
|
||||
|
||||
```bash
|
||||
docker build -t ankao:latest .
|
||||
```
|
||||
|
||||
#### 2. 启动 PostgreSQL 数据库
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name ankao-postgres \
|
||||
-e POSTGRES_DB=ankao \
|
||||
-e POSTGRES_USER=postgres \
|
||||
-e POSTGRES_PASSWORD=postgres \
|
||||
-p 5432:5432 \
|
||||
postgres:16-alpine
|
||||
```
|
||||
|
||||
#### 3. 启动应用
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name ankao-app \
|
||||
-p 8080:8080 \
|
||||
-e DB_HOST=host.docker.internal \
|
||||
-e DB_PORT=5432 \
|
||||
-e DB_USER=postgres \
|
||||
-e DB_PASSWORD=postgres \
|
||||
-e DB_NAME=ankao \
|
||||
-e DB_SSLMODE=disable \
|
||||
ankao:latest
|
||||
```
|
||||
|
||||
**注意**: 在 Windows 和 Mac 上使用 `host.docker.internal` 连接宿主机的数据库。在 Linux 上需要使用 `--network host` 或创建自定义网络。
|
||||
|
||||
## 环境变量配置
|
||||
|
||||
应用支持以下环境变量配置:
|
||||
|
||||
### 数据库配置
|
||||
|
||||
| 环境变量 | 描述 | 默认值 |
|
||||
|---------|------|--------|
|
||||
| `DB_HOST` | 数据库主机地址 | pgsql.yuchat.top |
|
||||
| `DB_PORT` | 数据库端口 | 5432 |
|
||||
| `DB_USER` | 数据库用户名 | postgres |
|
||||
| `DB_PASSWORD` | 数据库密码 | longqi@1314 |
|
||||
| `DB_NAME` | 数据库名称 | ankao |
|
||||
| `DB_SSLMODE` | SSL 模式 | disable |
|
||||
|
||||
### 应用配置
|
||||
|
||||
| 环境变量 | 描述 | 默认值 |
|
||||
|---------|------|--------|
|
||||
| `GIN_MODE` | Gin 运行模式 (debug/release) | debug |
|
||||
|
||||
## Docker Compose 配置说明
|
||||
|
||||
`docker-compose.yml` 文件定义了两个服务:
|
||||
|
||||
1. **postgres**: PostgreSQL 16 数据库
|
||||
- 端口: 5432
|
||||
- 数据持久化: 使用 Docker volume `postgres_data`
|
||||
- 健康检查: 确保数据库就绪后再启动应用
|
||||
|
||||
2. **app**: AnKao 应用
|
||||
- 端口: 8080
|
||||
- 依赖 postgres 服务
|
||||
- 自动重启策略
|
||||
|
||||
## 生产环境部署建议
|
||||
|
||||
### 1. 修改默认密码
|
||||
|
||||
**重要**: 生产环境中务必修改数据库密码!
|
||||
|
||||
编辑 `docker-compose.yml`:
|
||||
```yaml
|
||||
environment:
|
||||
POSTGRES_PASSWORD: 你的强密码
|
||||
DB_PASSWORD: 你的强密码
|
||||
```
|
||||
|
||||
### 2. 使用外部数据库
|
||||
|
||||
如果使用外部 PostgreSQL 数据库,只需启动 app 服务:
|
||||
|
||||
```bash
|
||||
docker-compose up -d app
|
||||
```
|
||||
|
||||
并配置相应的环境变量。
|
||||
|
||||
### 3. 配置反向代理
|
||||
|
||||
生产环境建议使用 Nginx 作为反向代理:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost: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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 启用 HTTPS
|
||||
|
||||
使用 Let's Encrypt 免费 SSL 证书:
|
||||
|
||||
```bash
|
||||
# 安装 certbot
|
||||
apt-get install certbot python3-certbot-nginx
|
||||
|
||||
# 获取证书
|
||||
certbot --nginx -d your-domain.com
|
||||
```
|
||||
|
||||
### 5. 备份数据
|
||||
|
||||
定期备份 PostgreSQL 数据:
|
||||
|
||||
```bash
|
||||
# 备份数据库
|
||||
docker exec ankao-postgres pg_dump -U postgres ankao > backup.sql
|
||||
|
||||
# 恢复数据库
|
||||
cat backup.sql | docker exec -i ankao-postgres psql -U postgres ankao
|
||||
```
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 查看应用日志
|
||||
|
||||
```bash
|
||||
# 查看所有服务日志
|
||||
docker-compose logs
|
||||
|
||||
# 查看应用日志
|
||||
docker-compose logs app
|
||||
|
||||
# 查看数据库日志
|
||||
docker-compose logs postgres
|
||||
|
||||
# 实时查看日志
|
||||
docker-compose logs -f app
|
||||
```
|
||||
|
||||
### 进入容器调试
|
||||
|
||||
```bash
|
||||
# 进入应用容器
|
||||
docker exec -it ankao-app sh
|
||||
|
||||
# 进入数据库容器
|
||||
docker exec -it ankao-postgres sh
|
||||
```
|
||||
|
||||
### 常见问题
|
||||
|
||||
#### 1. 应用无法连接数据库
|
||||
|
||||
检查:
|
||||
- 数据库是否已启动: `docker-compose ps`
|
||||
- 网络配置是否正确
|
||||
- 环境变量配置是否正确
|
||||
|
||||
#### 2. 端口被占用
|
||||
|
||||
修改 `docker-compose.yml` 中的端口映射:
|
||||
```yaml
|
||||
ports:
|
||||
- "8081:8080" # 将宿主机端口改为 8081
|
||||
```
|
||||
|
||||
#### 3. 构建失败
|
||||
|
||||
清理缓存重新构建:
|
||||
```bash
|
||||
docker-compose build --no-cache
|
||||
```
|
||||
|
||||
## 镜像优化
|
||||
|
||||
当前 Dockerfile 使用多阶段构建,最终镜像基于 Alpine Linux,体积小且安全。
|
||||
|
||||
镜像大小约:
|
||||
- 前端构建阶段: ~200MB (不包含在最终镜像)
|
||||
- 后端构建阶段: ~300MB (不包含在最终镜像)
|
||||
- 最终运行镜像: ~30MB
|
||||
|
||||
## 更新应用
|
||||
|
||||
```bash
|
||||
# 拉取最新代码
|
||||
git pull
|
||||
|
||||
# 重新构建并启动
|
||||
docker-compose up -d --build
|
||||
|
||||
# 或者分步执行
|
||||
docker-compose build
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## 清理资源
|
||||
|
||||
```bash
|
||||
# 停止并删除容器
|
||||
docker-compose down
|
||||
|
||||
# 删除容器和数据卷
|
||||
docker-compose down -v
|
||||
|
||||
# 删除镜像
|
||||
docker rmi ankao:latest
|
||||
```
|
||||
73
Dockerfile
Normal file
73
Dockerfile
Normal file
@ -0,0 +1,73 @@
|
||||
# ============================================
|
||||
# 第一阶段: 构建前端应用
|
||||
# ============================================
|
||||
FROM node:22-alpine AS frontend-builder
|
||||
|
||||
WORKDIR /app/web
|
||||
|
||||
# 复制前端依赖文件
|
||||
COPY web/package.json web/yarn.lock ./
|
||||
|
||||
# 安装依赖
|
||||
RUN yarn install --frozen-lockfile
|
||||
|
||||
# 复制前端源代码
|
||||
COPY web/ ./
|
||||
|
||||
# 构建前端应用
|
||||
RUN yarn build
|
||||
|
||||
# ============================================
|
||||
# 第二阶段: 构建 Go 后端应用
|
||||
# ============================================
|
||||
FROM golang:1.25.1-alpine AS backend-builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 安装必要的构建工具
|
||||
RUN apk add --no-cache git
|
||||
|
||||
# 配置 Go 代理(使用国内镜像加速)
|
||||
ENV GOPROXY=https://goproxy.cn,https://goproxy.io,https://mirrors.aliyun.com/goproxy/,direct
|
||||
ENV GOSUMDB=off
|
||||
|
||||
# 复制 Go 依赖文件
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
# 下载依赖
|
||||
RUN go mod download
|
||||
|
||||
# 复制后端源代码
|
||||
COPY . .
|
||||
|
||||
# 构建 Go 应用
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o server main.go
|
||||
|
||||
# ============================================
|
||||
# 第三阶段: 创建最终运行镜像
|
||||
# ============================================
|
||||
FROM alpine:latest
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 安装运行时依赖
|
||||
RUN apk --no-cache add ca-certificates tzdata
|
||||
|
||||
# 设置时区为上海
|
||||
ENV TZ=Asia/Shanghai
|
||||
|
||||
# 从后端构建阶段复制可执行文件
|
||||
COPY --from=backend-builder /app/server ./server
|
||||
|
||||
# 从前端构建阶段复制构建产物
|
||||
# 根据 main.go 配置:
|
||||
# - r.NoRoute() 返回 ./web/index.html
|
||||
# - r.Static("/assets", "./web/static") 提供静态资源
|
||||
COPY --from=frontend-builder /app/web/dist/index.html ./web/index.html
|
||||
COPY --from=frontend-builder /app/web/dist/assets ./web/static
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8080
|
||||
|
||||
# 启动应用
|
||||
CMD ["./server"]
|
||||
15
main.go
15
main.go
@ -23,11 +23,7 @@ func main() {
|
||||
r.Use(middleware.CORS())
|
||||
r.Use(middleware.Logger())
|
||||
|
||||
// 静态文件服务
|
||||
r.Static("/static", "./web/static")
|
||||
r.StaticFile("/", "./web/index.html")
|
||||
|
||||
// API路由组
|
||||
// API路由组(必须在静态文件服务之前注册)
|
||||
api := r.Group("/api")
|
||||
{
|
||||
// 健康检查
|
||||
@ -65,6 +61,15 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// 静态文件服务(必须在 API 路由之后)
|
||||
// 提供静态资源(CSS、JS、图片等)
|
||||
r.Static("/assets", "./web/static")
|
||||
|
||||
// SPA 支持:所有非 API 路由都返回 index.html,让前端路由处理
|
||||
r.NoRoute(func(c *gin.Context) {
|
||||
c.File("./web/index.html")
|
||||
})
|
||||
|
||||
// 启动服务器
|
||||
port := ":8080"
|
||||
if err := r.Run(port); err != nil {
|
||||
|
||||
@ -2,6 +2,8 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// DatabaseConfig 数据库配置结构
|
||||
@ -15,17 +17,44 @@ type DatabaseConfig struct {
|
||||
}
|
||||
|
||||
// GetDatabaseConfig 获取数据库配置
|
||||
// 优先使用环境变量,如果没有设置则使用默认值
|
||||
func GetDatabaseConfig() *DatabaseConfig {
|
||||
// 从环境变量获取配置,如果未设置则使用默认值
|
||||
host := getEnv("DB_HOST", "pgsql.yuchat.top")
|
||||
port := getEnvAsInt("DB_PORT", 5432)
|
||||
user := getEnv("DB_USER", "postgres")
|
||||
password := getEnv("DB_PASSWORD", "longqi@1314")
|
||||
dbname := getEnv("DB_NAME", "ankao")
|
||||
sslmode := getEnv("DB_SSLMODE", "disable")
|
||||
|
||||
return &DatabaseConfig{
|
||||
Host: "pgsql.yuchat.top",
|
||||
Port: 5432,
|
||||
User: "postgres",
|
||||
Password: "longqi@1314",
|
||||
DBName: "ankao",
|
||||
SSLMode: "disable",
|
||||
Host: host,
|
||||
Port: port,
|
||||
User: user,
|
||||
Password: password,
|
||||
DBName: dbname,
|
||||
SSLMode: sslmode,
|
||||
}
|
||||
}
|
||||
|
||||
// getEnv 获取环境变量,如果不存在则返回默认值
|
||||
func getEnv(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// getEnvAsInt 获取整型环境变量,如果不存在或转换失败则返回默认值
|
||||
func getEnvAsInt(key string, defaultValue int) int {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
if intValue, err := strconv.Atoi(value); err == nil {
|
||||
return intValue
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// GetDSN 返回数据库连接字符串
|
||||
func (c *DatabaseConfig) GetDSN() string {
|
||||
return fmt.Sprintf(
|
||||
|
||||
@ -98,10 +98,10 @@ const QuestionPage: React.FC = () => {
|
||||
};
|
||||
|
||||
// 加载题目列表(从第一题开始)
|
||||
const loadQuestions = async (type?: string, category?: string) => {
|
||||
const loadQuestions = async (type?: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await questionApi.getQuestions({ type, category });
|
||||
const res = await questionApi.getQuestions({ type });
|
||||
if (res.success && res.data) {
|
||||
setAllQuestions(res.data);
|
||||
|
||||
@ -155,7 +155,7 @@ const QuestionPage: React.FC = () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 处理判断题答案:将字符串 "true"/"false" 转换为布尔值
|
||||
let answerToSubmit = selectedAnswer;
|
||||
let answerToSubmit: string | string[] | boolean = selectedAnswer;
|
||||
if (currentQuestion.type === "true-false" && typeof selectedAnswer === "string") {
|
||||
answerToSubmit = selectedAnswer === "true";
|
||||
}
|
||||
@ -225,7 +225,6 @@ const QuestionPage: React.FC = () => {
|
||||
// 初始化
|
||||
useEffect(() => {
|
||||
const typeParam = searchParams.get("type");
|
||||
const categoryParam = searchParams.get("category");
|
||||
const mode = searchParams.get("mode");
|
||||
|
||||
// 错题练习模式
|
||||
@ -235,7 +234,7 @@ const QuestionPage: React.FC = () => {
|
||||
}
|
||||
|
||||
// 普通练习模式 - 从第一题开始
|
||||
loadQuestions(typeParam || undefined, categoryParam || undefined);
|
||||
loadQuestions(typeParam || undefined);
|
||||
}, [searchParams]);
|
||||
|
||||
// 重试处理
|
||||
@ -246,8 +245,7 @@ const QuestionPage: React.FC = () => {
|
||||
setWrongCount(0);
|
||||
localStorage.removeItem(getStorageKey());
|
||||
const typeParam = searchParams.get("type");
|
||||
const categoryParam = searchParams.get("category");
|
||||
loadQuestions(typeParam || undefined, categoryParam || undefined);
|
||||
loadQuestions(typeParam || undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@ -290,7 +290,7 @@ const QuestionManagement: React.FC = () => {
|
||||
{(fields, { add, remove }, { errors }) => (
|
||||
<>
|
||||
<Form.Item label="选项" required>
|
||||
{fields.map((field, index) => (
|
||||
{fields.map((field) => (
|
||||
<Space key={field.key} style={{ display: 'flex', marginBottom: 8 }} align="baseline">
|
||||
<Form.Item
|
||||
{...field}
|
||||
@ -349,7 +349,7 @@ const QuestionManagement: React.FC = () => {
|
||||
{(fields, { add, remove }, { errors }) => (
|
||||
<>
|
||||
<Form.Item label="选项" required>
|
||||
{fields.map((field, index) => (
|
||||
{fields.map((field) => (
|
||||
<Space key={field.key} style={{ display: 'flex', marginBottom: 8 }} align="baseline">
|
||||
<Form.Item
|
||||
{...field}
|
||||
|
||||
@ -108,11 +108,11 @@ const WrongQuestions: React.FC = () => {
|
||||
// 获取题型标签颜色
|
||||
const getTypeColor = (type: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
single: 'blue',
|
||||
multiple: 'green',
|
||||
fill: 'cyan',
|
||||
judge: 'orange',
|
||||
short: 'purple',
|
||||
'multiple-choice': 'blue',
|
||||
'multiple-selection': 'green',
|
||||
'fill-in-blank': 'cyan',
|
||||
'true-false': 'orange',
|
||||
'short-answer': 'purple',
|
||||
}
|
||||
return colorMap[type] || 'default'
|
||||
}
|
||||
@ -199,7 +199,7 @@ const WrongQuestions: React.FC = () => {
|
||||
<Space>
|
||||
<Text strong>题目 {item.question.question_id || item.question.id}</Text>
|
||||
<Tag color={getTypeColor(item.question.type)}>
|
||||
{item.question.category}
|
||||
{item.question.category || item.question.type}
|
||||
</Tag>
|
||||
<Tag color="error">错误 {item.wrong_count} 次</Tag>
|
||||
</Space>
|
||||
|
||||
@ -21,7 +21,7 @@ export interface Question {
|
||||
// 提交答案
|
||||
export interface SubmitAnswer {
|
||||
question_id: number
|
||||
answer: string | string[]
|
||||
answer: string | string[] | boolean
|
||||
}
|
||||
|
||||
// 答案结果
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user