1. 项目概述:为什么选择用 docker-compose 部署 playedu 这类开源培训系统?
最近有几位做企业内训平台搭建的同行在技术群问:“有没有开箱即用、部署快、后期好维护的开源培训系统?”我顺手翻了下 GitHub 上近期 star 增速较快的教育类项目,playedu就跳出来了——它不是那种功能堆砌型的“大而全”系统,而是聚焦在“轻量级课程交付+学员行为追踪+讲师协作闭环”三个真实场景上。核心模块就四块:课程管理后台、H5 学习端、讲师工作台、数据看板,所有接口都走 RESTful,前端用 Vue3 + Pinia,后端是 Spring Boot 3.x + PostgreSQL,整个架构干净得像刚洗过的玻璃杯。
但问题来了:它官方只提供源码和 Dockerfile,没给现成的 docker-compose.yml。很多团队第一次拉下来跑,卡在环境变量配错、PostgreSQL 初始化失败、Redis 连接超时这三关,有人甚至重装了三遍系统。我去年帮某高校继续教育学院落地过一版,从 clone 到可登录后台,实测最快 11 分钟,最慢那次花了 3 小时——不是代码问题,全是 compose 编排细节没对齐。比如它的.env文件里DB_URL写的是jdbc:postgresql://db:5432/playedu,但很多人直接照抄进本地配置,忘了db是服务名,不是 localhost;又比如它的application-prod.yml里默认启用了邮件通知,但没配 SMTP 就启动,Spring Boot 会卡在健康检查阶段死等 30 秒才报错,日志里只显示Waiting for application to start...,根本看不出是哪堵住了。
所以这篇不是教你怎么“运行一个容器”,而是带你把playedu 的 docker-compose 部署过程拆成可验证、可回滚、可交接的标准化动作。你会看到:
- 为什么必须用
depends_on+healthcheck而不是简单写restart: always; - 如何用
initdb脚本自动建库建用户,避开手动 exec 进容器执行 SQL 的低效操作; - 怎样让 Nginx 反向代理既支持 H5 端的
/api路由转发,又不破坏/static资源缓存策略; - 最关键的是——怎么设计
.env分层(dev/staging/prod),让同一套 compose 文件在测试机和生产服务器上零修改切换。
适合谁看?如果你是运维工程师,能直接抄走docker-compose.prod.yml和配套脚本;如果你是 Java 开发,能看清每个服务的 JVM 参数怎么调、GC 日志往哪写;如果你是技术负责人,会明白为什么我们宁可多写 20 行 YAML,也不用docker run -d手动起容器——因为上线那天,你不会想一边查日志一边祈祷“别是数据库连不上”。
2. 整体架构设计与方案选型逻辑
2.1 为什么不用 Kubernetes?为什么坚持用 docker-compose?
先说结论:对于单机或小集群(≤3 台物理机)、日活 < 5000 的培训系统,K8s 是杀鸡用牛刀,docker-compose 是刚刚好的扳手。我不是反对 K8s,而是算过一笔账:某公司曾用 K3s 搭了个 playedu 环境,结果光是 Cert-Manager + Ingress-Nginx 的证书自动续期就搞崩过两次,每次排查要 2 小时;而用 compose 部署,所有 TLS 终止放在 Nginx 层,用 Let’s Encrypt 的 certbot 定时任务搞定,脚本就 12 行,出问题直接docker logs nginx看错误行。
更实际的约束来自 playedu 自身:它没有原生支持水平扩缩容的组件。它的课程视频播放依赖本地文件存储(默认用LocalFileStorageService),如果把playedu-app扩成 3 个副本,三个容器各自读自己的/app/uploads目录,上传的课件就互相看不见。要改就得切到 MinIO 或阿里云 OSS,但这就超出“开箱即用”的范畴了。而 docker-compose 天然绑定单机存储卷,volumes:下直接挂载宿主机路径,/data/playedu/uploads一个目录管所有服务,省心。
再看资源开销:K8s 控制平面至少要 2G 内存起步,etcd、kube-apiserver、scheduler 全跑起来,空闲时也吃 1.2G;而 docker-compose 启动的 5 个服务(nginx、app、db、redis、adminer),实测内存占用峰值 1.8G,其中 PostgreSQL 占 900M,Java 应用占 600M,其余加起来不到 300M。一台 4C8G 的云服务器,跑它绰绰有余,还能剩 2G 给备份脚本和监控 Agent。
所以我们的架构图其实就一张纸:
[客户端浏览器] ↓ HTTPS [Nginx 反向代理] ↓ HTTP(内部网络) [playedu-app] ←→ [PostgreSQL] ←→ [Redis] ↓ [Adminer(可选)]注意两个关键设计点:
- Nginx 不只是反向代理,更是静态资源网关:所有
/static/**、/uploads/**请求由 Nginx 直接返回,不经过 Java 应用,减少 GC 压力; - PostgreSQL 和 Redis 用 healthcheck 主动探活:避免 app 启动时数据库还没 ready 就去连,导致 Spring Boot 启动失败重启 5 次后放弃。
提示:不要在
docker-compose.yml里写restart: always给所有服务。它会让故障服务无限重启,掩盖真正的问题。正确做法是:数据库和 Redis 设restart: unless-stopped(手动停才停),app 服务设restart: on-failure:3(失败最多重试 3 次),Nginx 设restart: no(它挂了说明配置错了,该人工介入)。
2.2 服务拆分依据:为什么是这 5 个容器?
playedu 官方文档建议“all-in-one”,但实际部署中,硬塞进一个容器会带来三个硬伤:
- 升级撕裂:PostgreSQL 从 14 升 15,Java 从 17 升 21,Nginx 从 1.22 升 1.24,三者生命周期完全不同,合在一起升级等于重装系统;
- 日志混乱:
docker logs playedu-all输出里,Java 的WARN、PostgreSQL 的LOG、Nginx 的error.log全混在一起,grep 时得写正则过滤; - 权限失控:PostgreSQL 必须以
postgres用户运行,Java 应用推荐用非 root 用户(如appuser),合在一个容器里要么降权失败,要么提权风险。
所以我们严格按职责拆:
| 容器名 | 镜像来源 | 核心职责 | 关键配置要点 |
|---|---|---|---|
nginx | nginx:alpine | HTTPS 终止、静态资源服务、API 路由转发 | proxy_buffering off关闭缓冲,避免长连接卡顿;client_max_body_size 100m支持大课件上传 |
playedu-app | playedu/app:latest(需自行 build) | 业务逻辑、课程管理、学习流程控制 | JVM 参数-Xms512m -Xmx1024m -XX:+UseZGC,GC 日志输出到/app/logs/gc.log |
db | postgres:14-alpine | 数据持久化、用户/课程/订单数据存储 | 初始化脚本挂载到/docker-entrypoint-initdb.d/,自动建库建用户 |
redis | redis:7-alpine | 会话共享、缓存课程元数据、消息队列(用于邮件异步) | redis.conf自定义maxmemory 256mb,maxmemory-policy allkeys-lru |
adminer | adminer:4.8(可选) | 数据库可视化管理,仅限开发/测试环境 | 通过ADMINER_DEFAULT_SERVER=db环境变量直连 PostgreSQL |
注意:
adminer容器在生产环境必须删除或加 IP 白名单。我见过太多团队把它留在线上,结果被扫描器扫到,暴露了数据库结构。哪怕加了基础认证,也挡不住暴力破解。真要查数据,docker exec -it playedu-db psql -U playedu_user playedu_db一行命令比网页快十倍。
2.3 网络与存储设计:bridge 网络够不够用?
答案是:够,而且更安全。很多人一听“生产环境”就觉得必须上host网络或自定义overlay,但 playedu 没有跨主机通信需求——所有服务都在同一台物理机或云服务器上。bridge网络天然隔离,服务间用service-name:port通信(如db:5432),外部访问只暴露 Nginx 的 443/80 端口,攻击面最小。
存储方面,我们放弃 bind mount(宿主机路径映射),全部用 named volume:
volumes: playedu-db-data: driver: local driver_opts: type: none device: /data/playedu/db o: bind playedu-redis-data: driver: local driver_opts: type: none device: /data/playedu/redis o: bind playedu-uploads: driver: local driver_opts: type: none device: /data/playedu/uploads o: bind为什么?因为 bind mount 在 Docker Desktop(Mac/Windows)上性能差,且权限容易错乱(宿主机 UID=1000,容器内 UID=999,文件属主不匹配)。而 named volume 由 Docker 管理,自动处理权限映射,/data/playedu/目录在宿主机上chown -R 1001:1001 /data/playedu一次就够了。
实操心得:
/data/playedu/uploads目录必须提前创建,并设置chmod 755。我踩过一次坑:没建目录就跑docker-compose up,PostgreSQL 初始化成功了,但 playedu-app 启动时报java.io.IOException: Permission denied,查了半小时才发现是 uploads 目录不存在,Docker 自动创建的目录属主是 root,而 app 容器用的是appuser(UID=1001)。
3. 核心细节解析与实操要点
3.1 环境变量分层管理:.env文件的三种写法
playedu 的配置分散在三处:
.env(docker-compose 读取,控制服务启动参数)application.yml(Spring Boot 读取,控制业务逻辑)nginx.conf(Nginx 读取,控制路由和安全策略)
如果全写死在docker-compose.yml里,换环境就得改 YAML,极易出错。我们的方案是:用三层.env文件解耦。
第一层:./.env(全局默认,开发机用)
# 通用配置 COMPOSE_PROJECT_NAME=playedu TZ=Asia/Shanghai # 数据库 POSTGRES_DB=playedu_db POSTGRES_USER=playedu_user POSTGRES_PASSWORD=dev_password_123 # 应用 APP_PROFILE=dev APP_JVM_OPTS=-Xms512m -Xmx1024m第二层:./env.staging(测试环境,CI/CD 流水线注入)
POSTGRES_PASSWORD=staging_pass_456 APP_PROFILE=staging APP_JVM_OPTS=-Xms768m -Xmx1536m NGINX_SSL_ENABLED=false第三层:./env.prod(生产环境,由 Ansible 模板生成)
POSTGRES_PASSWORD=prod_pass_789_AeFg APP_PROFILE=prod APP_JVM_OPTS=-Xms1024m -Xmx2048m -XX:+UseZGC NGINX_SSL_ENABLED=true NGINX_DOMAIN=training.example.com启动命令就变成:
# 开发环境 docker-compose --env-file .env up -d # 生产环境(注意顺序:后加载的 env 文件覆盖前面的) docker-compose --env-file .env --env-file env.prod up -d提示:
docker-compose加载 env 文件是覆盖式,不是合并式。所以env.prod里必须写全所有变量,不能只写POSTGRES_PASSWORD。我们用 Python 脚本自动生成env.prod:读取 Vault 密钥,拼接域名,计算 JVM 堆大小(按宿主机内存 25% 分配),最后写入文件。这样人手误改的概率降到最低。
3.2 PostgreSQL 初始化:如何让数据库“自己准备好”
playedu 的初始化不是简单的CREATE DATABASE,它需要:
- 创建数据库
playedu_db; - 创建用户
playedu_user并赋予权限; - 执行
schema.sql建表; - 执行
data.sql插入初始管理员账号(用户名 admin,密码 admin123); - 设置
search_path为playedu_schema(它用 schema 隔离多租户)。
官方没提供一键脚本,我们自己写了init-db.sh:
#!/bin/bash set -e psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL CREATE SCHEMA IF NOT EXISTS playedu_schema; SET search_path TO playedu_schema; -- 建表语句(从 playedu 源码的 resources/sql/schema.sql 复制) CREATE TABLE IF NOT EXISTS course ( id SERIAL PRIMARY KEY, title VARCHAR(255) NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- 插入初始用户 INSERT INTO "user" (username, password, role, status) SELECT 'admin', '\$2a\$10\$...', 'ADMIN', 'ACTIVE' WHERE NOT EXISTS (SELECT 1 FROM "user" WHERE username = 'admin'); EOSQL然后在docker-compose.yml里挂载:
services: db: image: postgres:14-alpine volumes: - playedu-db-data:/var/lib/postgresql/data - ./init-db.sh:/docker-entrypoint-initdb.d/init-db.sh environment: POSTGRES_DB: ${POSTGRES_DB} POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}关键点:/docker-entrypoint-initdb.d/目录下的脚本,会在 PostgreSQL 第一次初始化数据目录时自动执行,且只执行一次。后续重启容器不会重复运行,完美解决“重复插入管理员账号”的问题。
注意:
init-db.sh文件权限必须是755,否则 PostgreSQL 容器启动时报Permission denied。Mac 上用chmod 755 init-db.sh,Linux 上同理。我第一次部署时忘加权限,日志里只显示database system is ready to accept connections,但应用连上去发现表全空,折腾了 40 分钟才想到是脚本没执行。
3.3 Nginx 配置的四个生死线
Nginx 不是配个proxy_pass就完事。playedu 的 H5 端是单页应用(SPA),API 接口带/api前缀,静态资源在/static,上传文件在/uploads,还要求支持 WebSocket(用于实时消息)。漏掉任何一条,前端就白屏或报 404。
我们的nginx.conf核心段落:
upstream playedu_backend { server playedu-app:8080; } server { listen 80; server_name _; # 强制跳转 HTTPS(生产环境) if ($scheme != "https") { return 301 https://$host$request_uri; } } server { listen 443 ssl http2; server_name ${NGINX_DOMAIN}; ssl_certificate /etc/nginx/ssl/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/privkey.pem; # 静态资源:直接返回,不走后端 location /static/ { alias /app/static/; expires 1y; add_header Cache-Control "public, immutable"; } # 上传资源:同样直出,支持断点续传 location /uploads/ { alias /app/uploads/; expires 7d; add_header Access-Control-Allow-Origin "*"; } # API 接口:转发到 Java 应用 location /api/ { proxy_pass http://playedu_backend/; 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_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } # 根路径:返回 index.html,由前端路由接管 location / { root /app/dist; try_files $uri $uri/ /index.html; } }四个必验点:
try_files $uri $uri/ /index.html;—— 没这行,前端路由刷新页面会 404;proxy_set_header Connection "upgrade";—— 没这行,WebSocket 连接建立失败;alias /app/static/;结尾的/不能少,否则路径拼接错(/static/css/app.css会变成/app/staticcss/app.css);add_header Access-Control-Allow-Origin "*";仅限开发测试,生产环境必须换成具体域名,如https://training.example.com。
实操心得:SSL 证书路径必须用绝对路径,且 Nginx 容器内要提前
mkdir -p /etc/nginx/ssl。我们用 certbot 的--deploy-hook自动复制证书到/data/playedu/nginx/ssl/,再通过 volume 挂载进去。这样证书自动续期后,Nginx reload 一下就生效,不用重启容器。
4. 实操过程与核心环节实现
4.1 从零开始:12 步完成生产环境部署
以下步骤在 Ubuntu 22.04 LTS 服务器上实测通过,全程无需 root 权限(除安装 Docker 外),每步附耗时和验证方式。
第 1 步:安装 Docker 和 docker-compose(约 3 分钟)
# 官方脚本一键安装 curl -fsSL https://get.docker.com | sh sudo usermod -aG docker $USER # 退出终端重登,使组生效 newgrp docker # 安装 docker-compose v2.20.0(v1 已废弃) sudo apt-get install docker-compose-plugin✅ 验证:docker --version && docker compose version输出版本号。
第 2 步:创建项目目录并下载配置(约 1 分钟)
mkdir -p /data/playedu/{db,redis,uploads,nginx/ssl} cd /data/playedu wget https://raw.githubusercontent.com/xxx/playedu/main/docker-compose.prod.yml wget https://raw.githubusercontent.com/xxx/playedu/main/nginx.conf wget https://raw.githubusercontent.com/xxx/playedu/main/init-db.sh✅ 验证:ls -l看到三个文件。
第 3 步:生成生产环境 env 文件(约 2 分钟)
cat > env.prod <<'EOF' POSTGRES_DB=playedu_db POSTGRES_USER=playedu_user POSTGRES_PASSWORD=your_strong_password_here APP_PROFILE=prod APP_JVM_OPTS=-Xms1024m -Xmx2048m -XX:+UseZGC NGINX_SSL_ENABLED=true NGINX_DOMAIN=training.yourcompany.com EOF✅ 验证:grep PASSWORD env.prod确认密码已填。
第 4 步:构建 Java 应用镜像(约 8 分钟)
# 克隆源码(假设已 fork 到私有仓库) git clone https://git.yourcompany.com/playedu.git cd playedu # 修改 application-prod.yml 中的数据库 URL 为 jdbc:postgresql://db:5432/playedu_db sed -i 's/jdbc:postgresql:\/\/localhost:5432\/playedu_db/jdbc:postgresql:\/\/db:5432\/playedu_db/g' src/main/resources/application-prod.yml # 构建镜像 ./mvnw clean package -DskipTests docker build -t playedu/app:latest .✅ 验证:docker images | grep playedu/app看到镜像。
第 5 步:准备 SSL 证书(约 5 分钟)
# 安装 certbot sudo apt-get install certbot # 获取证书(需域名已解析到本机 IP) sudo certbot certonly --standalone -d training.yourcompany.com # 复制到 Nginx 目录 sudo cp /etc/letsencrypt/live/training.yourcompany.com/{fullchain.pem,privkey.pem} /data/playedu/nginx/ssl/ sudo chown -R $USER:$USER /data/playedu/nginx/ssl/✅ 验证:ls -l /data/playedu/nginx/ssl/有两个 pem 文件。
第 6 步:启动服务(约 1 分钟)
cd /data/playedu docker compose --env-file .env --env-file env.prod up -d✅ 验证:docker compose ps显示所有服务状态为running。
第 7 步:等待数据库初始化(约 30 秒)
# 查看 db 日志,直到出现 "database system is ready" docker logs playedu-db | tail -n 20 # 等到 "PostgreSQL init process complete" 就可以了第 8 步:检查 playedu-app 启动日志(约 2 分钟)
# 看到 "Started PlayeduApplication in X.XXX seconds" 即成功 docker logs playedu-app | grep "Started " # 如果卡住,看是否有 "Connection refused" 错误 docker logs playedu-app | grep -i "connection refused"第 9 步:验证 Nginx 是否响应(约 10 秒)
curl -I http://localhost # 应返回 301,跳转到 HTTPS curl -I https://localhost --insecure # 应返回 200,且 Header 有 "Content-Type: text/html"第 10 步:登录后台(约 30 秒)
打开浏览器访问https://training.yourcompany.com,输入默认账号admin/admin123。
✅ 验证:能进入课程管理页面,右上角显示“管理员”。
第 11 步:上传一个测试课件(约 1 分钟)
在后台点击“新建课程” → “上传封面” → 选一个 2MB 的 JPG。
✅ 验证:上传成功,图片能正常显示,/data/playedu/uploads/目录下有对应文件。
第 12 步:检查健康检查端点(约 10 秒)
curl https://training.yourcompany.com/actuator/health --insecure # 应返回 {"status":"UP","components":{"db":{"status":"UP"},"redis":{"status":"UP"}}}注意:第 4 步构建镜像时,如果 Maven 报
Could not resolve dependencies,说明私服地址没配。我们统一在~/.m2/settings.xml里配置了公司 Nexus 地址,<mirrorOf>*</mirrorOf>全局代理。这是 Java 团队的基建共识,不是 playedu 特有的问题。
4.2 JVM 参数调优:ZGC 为什么比 G1 更适合培训系统?
playedu 的 Java 应用有两大内存特征:
- 对象生命周期短:学员点击“开始学习”,生成 session、token、临时缓存,5 分钟内基本失效;
- 大对象多:课件 PDF、视频封面图、富文本 HTML 片段,单个对象常超 1MB。
G1 垃圾收集器在处理大对象时,会触发Humongous Allocation,把大对象直接扔进老年代,导致老年代快速膨胀,频繁 Mixed GC。我们实测过:G1 下,上传一个 50MB 视频后,jstat -gc显示G1OldGen使用率从 20% 暴涨到 85%,接着连续 3 次 Full GC,STW 时间累计 12 秒,用户端明显卡顿。
ZGC(Z Garbage Collector)是 JDK 11+ 的低延迟收集器,最大优势是:
- 停顿时间稳定在 10ms 以内,无论堆多大;
- 大对象分配无额外开销,ZGC 把堆分成 page,大对象直接占多个 page,不区分新生代老年代;
- 并发标记、并发移动、并发重定位,99% 的 GC 工作和应用线程并行。
我们的 JVM 参数:
-Xms1024m -Xmx2048m -XX:+UseZGC -XX:ZCollectionInterval=5 -XX:ZUncommitDelay=300-Xms1024m -Xmx2048m:初始堆 1G,最大 2G,留出 1G 给系统和其他进程;-XX:+UseZGC:启用 ZGC;-XX:ZCollectionInterval=5:每 5 秒强制触发一次 GC(ZGC 默认只在内存压力大时才 GC,但培训系统内存增长平缓,需要主动清理);-XX:ZUncommitDelay=300:内存释放后,300 秒再真正归还给操作系统(避免频繁申请释放)。
✅ 验证方式:
# 进入容器查看 GC 日志 docker exec -it playedu-app tail -f /app/logs/gc.log # 正常输出类似: # [1.234s][info][gc] GC(0) Pause Mark Start 1.234ms # [1.245s][info][gc] GC(0) Pause Mark End 1.245ms # STW 时间精确到毫秒,且始终 < 10ms。实操心得:ZGC 要求 Linux 内核 ≥ 4.14(Ubuntu 22.04 默认 5.15,满足)。如果用 CentOS 7(内核 3.10),必须升级内核或换 G1。我们曾在线上 CentOS 7 机器上强行开 ZGC,结果
dmesg报mmap failed with ENOMEM,ZGC 启动失败,回退到 Serial GC,整个系统卡死。所以部署前务必uname -r确认内核版本。
4.3 安全加固:生产环境必须做的五件事
docker-compose 默认配置是开发友好型,生产环境必须收紧:
禁用默认的
docker.sock挂载
有些教程教人把/var/run/docker.sock挂进容器,方便容器内调 Docker API。这是严重安全隐患!一旦 playedu-app 被 XSS 攻击,攻击者就能curl --unix-socket /var/run/docker.sock http://localhost/containers/json列出所有容器,甚至POST /containers/{id}/start启动恶意容器。我们的docker-compose.prod.yml里绝无此挂载。限制容器资源上限
services: playedu-app: mem_limit: 2g mem_reservation: 1g cpus: '1.5'防止 Java 应用 OOM 崩溃整个宿主机。
mem_reservation是软限制,保证 1G 内存随时可用;mem_limit是硬顶,超了就 kill。以非 root 用户运行应用
在Dockerfile里加两行:RUN addgroup -g 1001 -f appgroup && adduser -S appuser -u 1001 USER appuser避免
java -jar app.jar以 root 身份读写文件。/app/uploads目录的属主必须是1001:1001。关闭不必要的端口暴露
docker-compose.yml里只保留:services: nginx: ports: - "80:80" - "443:443" # db、redis、adminer 全部不写 ports!只允许内部网络访问启用 fail2ban 防爆破
在宿主机上装 fail2ban,监控 Nginx 的error.log:# /etc/fail2ban/jail.local [nginx-badbots] enabled = true filter = nginx-badbots logpath = /var/log/nginx/error.log maxretry = 3 bantime = 3600三次 404 或 403 就封 IP 1 小时。我们上线一周,拦截了 17 个扫描器 IP,包括
sqlmap和dirsearch。
提示:
adminer容器在生产环境必须注释掉。如果真需要数据库管理,用ssh -L 8081:localhost:8080 user@server建立隧道,本地浏览器访问http://localhost:8081,这样流量全程加密,且不暴露在公网。
5. 常见问题与排查技巧实录
5.1 问题速查表:高频故障与 5 分钟定位法
| 现象 | 可能原因 | 快速验证命令 | 解决方案 |
|---|---|---|---|
docker compose ps显示db状态为restarting | PostgreSQL 初始化脚本语法错误 | docker logs playedu-db | tail -n 20 | 检查init-db.sh里 SQL 语句末尾是否少了分号; |
docker logs playedu-app报Connection refused | db服务没起来,或depends_on没配healthcheck | docker compose ps db看状态;docker inspect playedu-db | grep Health | 在db服务下加healthcheck,playedu-app加depends_on: {db: {condition: service_healthy}} |
前端白屏,Network 面板显示GET /api/login 502 | Nginx 没把请求转发到playedu-app | docker exec -it playedu-nginx nginx -t检查配置;curl -v http://playedu-app:8080/actuator/health | 确认upstream playedu_backend指向playedu-app:8080,且playedu-app的server.port=8080 |
上传课件失败,报413 Request Entity Too Large | Nginx 默认client_max_body_size=1m | docker exec -it playedu-nginx nginx -T | grep client_max_body_size | 在nginx.conf的http块加client_max_body_size 100m; |
登录后立即 401,控制台报Invalid JWT token | playedu-app的jwt.secret和前端VUE_APP_JWT_SECRET不一致 | docker exec -it playedu-app cat /app/config/application.yml | grep jwt.secret;对比前端.env | 保持前后端 secret 完全一致,建议用openssl rand -base64 32生成 |
注意:
docker compose ps的Status列只显示容器状态,不反映应用健康。比如playedu-app容器 running,但 Spring Boot 启动卡在数据库连接,Status仍是running。必须结合docker logs和curl健康端点判断。
5.2 日志分析实战:从 1000 行日志里揪出真凶
有一次客户反馈:“课程列表打不开,一直 loading”。我们拿到playedu-app的日志,第一反应是查ERROR:
docker logs playedu-app \| grep -i "error\|exception" \| head -n 5结果空。再查WARN:
docker logs playedu-app \| grep -i "warn" \| head -n 5输出:
2024-05-20 14:22:33.123 WARN 1 --- [nio-8080-exec-3] c.p.c.CourseController : Failed to fetch courses: org.springframework.dao.DataAccessResourceFailureException: Could not obtain JDBC Connection