07 · 部署:Docker Compose 与反向代理
07 · 部署:Docker Compose 与反向代理
本篇把「代码」变成「别人能访问的网站」,并解释请求从浏览器到数据库的完整链路。目标读者是完全没接触过容器的新手,所以会先讲清楚几个最基础的概念,再对照本项目真实文件逐行拆解。
7.1 先搞懂三个概念:镜像、容器、Docker Compose
很多新手第一次听 Docker 会被一堆名词绕晕,先把最关键的三个讲清楚。
镜像(Image)是什么
镜像可以理解成「一份打包好、随时能用的软件快照」。它里面包含:操作系统基础、运行环境(Java 17、Node 20 等)、你的代码和启动命令。镜像是静态的、只读的,它就像一张「装机光盘」或「虚拟机模板」。
类比:镜像 = 一颗「种子」或者「菜谱」。种子本身不是菜,但按菜谱一定能做出一样的菜。
本项目里你会看到两类镜像:
- 现成镜像:
postgres:17、redis:7-alpine、jc21/nginx-proxy-manager:latest,这些直接从 Docker Hub 拉取。 - 本地构建镜像:
backend和frontend没有指定现成镜像,而是用build: ./backend根据各自目录下的Dockerfile现做。
容器(Container)是什么
容器是镜像运行起来后的实例。镜像不动,你用 docker run 或 docker compose up 启动它,就产生一个容器。同一个镜像可以同时跑多个容器(就像一张光盘可以装很多台电脑)。
容器有生命周期:
- 运行(running):正在提供服务。
- 停止(stopped/exited):进程退出,但容器还在,数据卷里的数据还在。
- 删除(removed):容器被
docker rm,但默认情况下容器里写入的数据也会一起消失——这就是「数据卷」要解决的问题(见 7.5)。
类比:镜像 = 菜谱,容器 = 真正做出来、端上桌的那盘菜。菜吃完了(容器删除),菜谱还在(镜像还在)。
Docker Compose 是什么
一个完整应用需要多个组件:数据库、缓存、后端、前端、反向代理。手动一个个 docker run 很麻烦,而且谁先启动、谁来连谁、网络怎么互通,都要人肉记。
Docker Compose 就是一个「总指挥」:你写一个名为 docker-compose.yml 的文件,把所有服务和它们之间的关系描述出来,然后一条命令启动/停止/查看全部。它解决三件事:
- 编排:一次性拉起 5 个服务。
- 网络:自动建一个内部网络,让服务之间能用「名字」互相访问(DNS)。
- 依赖顺序:比如让后端等数据库「健康」后再启动。
docker compose up -d --build # 构建并后台启动所有服务
docker compose ps # 查看状态
docker compose logs -f backend # 跟踪后端日志
docker compose down # 停止并删除容器
docker compose down -v # 停止并删除数据卷(慎用!会清空数据库)提示:
docker compose和老命令docker-compose基本等价,新版 Docker 推荐用空格写法docker compose。本项目的命令都基于新版。
7.2 服务一览
docker-compose.yml 定义了 6 个服务(文件根节点 services:,见 docker-compose.yml:1)。
| 服务 | 容器名 | 镜像 | 作用 |
|---|---|---|---|
postgres | uas-postgres | postgres:17 | 业务数据库 |
redis | uas-redis | redis:7-alpine | 存登录会话(Token 与权限快照) |
rabbitmq | uas-rabbitmq | rabbitmq:3-management-alpine | 消息队列(银行式账务核心,承载充值 / 转账 / 购买命令) |
backend | uas-backend | 本地构建(见 backend/Dockerfile) | Spring Boot 后端 |
frontend | uas-frontend | 本地构建(见 frontend/Dockerfile) | Vue 静态资源 + Nginx |
npm | nginx-proxy-manager | jc21/nginx-proxy-manager:latest | 域名 / HTTPS / 反向代理 |
后端的环境变量示例(docker-compose.yml:38-50):
environment:
DB_URL: jdbc:postgresql://postgres:5432/uas
DB_USERNAME: uas
DB_PASSWORD: uas123456
REDIS_HOST: redis
REDIS_PORT: "6379"
RABBITMQ_HOST: rabbitmq
RABBITMQ_PORT: "5672"
RABBITMQ_USERNAME: uas
RABBITMQ_PASSWORD: uas123456
SERVER_PORT: "8080"注意 postgres、redis、rabbitmq 都是容器名 / 服务名,在 Docker 网络里可以直接当主机名用(DNS 自动解析),所以 DB_URL 里写的是 postgres:5432、RABBITMQ_HOST 里写的是 rabbitmq 而不是 localhost。后端通过 spring.rabbitmq.*(application.yml)读取这些变量建立到 RabbitMQ 的连接。
7.3 docker-compose.yml 逐行拆解
下面把 docker-compose.yml 的每一块讲透。
services 与服务声明
services:(docker-compose.yml:1)下面每个一级键(如 postgres:、redis:)就是一个服务。Compose 会为它们创建容器,容器名默认是 项目名_服务名_序号,但我们显式用 container_name 固定了名字,方便日志、调试和 NPM 里互相引用(见下文)。
image vs build
- 用现成镜像:
postgres用image: postgres:17(docker-compose.yml:3),直接拉官方镜像。 - 本地构建:
backend用build: ./backend(docker-compose.yml:35),意思是「去./backend目录找Dockerfile构建镜像」;frontend同理(docker-compose.yml:52)。
image:tag 里的 tag 很重要:postgres:17 锁了大版本,redis:7-alpine 用 alpine 精简版(体积小、攻击面小),而 npm 用 latest(最新版,可能随时变化,生产环境一般建议锁版本)。
container_name:固定容器名字
每个服务都写了 container_name,例如 container_name: uas-backend(docker-compose.yml:36)。好处:
- 日志命令可以直接写
docker logs uas-backend,不用去猜 Compose 自动生成的名字。 - 其他容器用它当主机名:前端 Nginx 里
proxy_pass http://backend:8080依赖服务名解析,而backend这个名字正是服务键名;container_name则用于你手动docker命令时更直观。
注意:服务键名(
backend)在内网 DNS 中作为主机名;container_name是给docker命令看的展示名。本项目两者含义一致,不要混淆即可。
restart: unless-stopped:自动重启策略
每个服务都配了 restart: unless-stopped(docker-compose.yml:5 等)。这是「容器退出了怎么办」的策略,有三个常见值:
| 策略 | 含义 |
|---|---|
no | 默认,退出就退出,不重启 |
on-failure | 只在非正常退出(非零码)时重启 |
unless-stopped | 只要不是你手动 docker stop,机器重启 / 进程崩了都会自动拉起 |
本项目用 unless-stopped,所以服务器重启后服务会自动恢复,适合常驻网站。
environment:把配置「注入」容器
environment: 把键值对写进容器的环境变量。后端读这些变量来定位数据库和缓存(docker-compose.yml:38-44):
DB_URL: jdbc:postgresql://postgres:5432/uas
REDIS_HOST: redis关键点:环境变量是后端连接信息的唯一来源。后端的 application.yml 用 ${DB_URL:...} 占位符读取(README.md:114)。也就是说,镜像本身不变,换一套环境变量就能指向不同数据库——这就是「配置与镜像分离」的思想。
ports:宿主机端口:容器端口
例如 postgres 的 ports: - "5432:5432"(docker-compose.yml:11-12)。冒号左边是宿主机端口,右边是容器内部端口。
为什么 5432 暴露出来?——纯粹为了方便你本地调试(用 Navicat / psql 连宿主机的 5432 直接看数据)。在生产环境里,如果只有容器内部需要访问数据库,完全可以删掉这行,数据库就「不露公网」,更安全(见 7.11 安全建议)。其他服务如 npm 暴露 80、443、81(docker-compose.yml:65-68)是因为那三个端口就是要对外提供 Web / HTTPS / 管理界面。
重点:容器之间通信不需要
ports映射!backend连postgres:5432走的是内部网络,跟左边宿主机端口无关。暴露端口只是「开一个从宿主机进来的窗户」。
volumes:让数据活过容器本身
postgres 配了 volumes: - postgres17_data:/var/lib/postgresql/data(docker-compose.yml:13-14)。这是具名数据卷(named volume)。
为什么要这样做?回顾 7.1 讲的:容器删除时,容器里写入的文件默认会消失。PostgreSQL 的数据全在 /var/lib/postgresql/data,如果直接写在容器里,一旦 docker compose down 再 up,数据库就空了。
数据卷把这部分目录挂到宿主机管理的一块持久存储上,容器换了、删了,数据还在。本项目定义了 4 个具名卷(docker-compose.yml:73-81):
volumes:
postgres17_data:
name: uas_postgres17_data
npm_data:
name: uas_npm_data
npm_letsencrypt:
name: uas_npm_letsencrypt
redis_data:
name: uas_redis_datapostgres17_data:业务数据库文件。redis_data:Redis 持久化数据(appendonly,见docker-compose.yml:25)。npm_data/npm_letsencrypt:NPM 的配置和 Let's Encrypt 证书,删了就要重新申请证书。
具名卷 vs 绑定挂载(bind mount):本项目用的是具名卷(Compose 自动在宿主机
/var/lib/docker/volumes/下管理)。另一种写法是./local/path:/container/path(绑定挂载,把宿主机某目录挂进去)。具名卷由 Docker 托管、更安全、便于备份;绑定挂载便于你直接在宿主机改文件调试。
healthcheck + depends_on:为什么要「等数据库」
postgres 配了健康检查(docker-compose.yml:15-19):
healthcheck:
test: ["CMD-SHELL", "pg_isready -U uas -d uas"]
interval: 10s
timeout: 5s
retries: 5pg_isready 是 PostgreSQL 自带的「我准备好接收连接了吗」工具。Compose 每 10 秒跑一次,连续 5 次超时才判定失败。
然后 backend 用 depends_on 要求数据库健康后再启动(docker-compose.yml:45-49):
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy为什么需要它? PostgreSQL 容器「启动」和「真正能接受连接」之间有时间差(要初始化数据目录、加载共享内存等)。如果后端一上来就连库,库还没好,后端会抛连接异常甚至反复崩溃。用了 service_healthy 条件,Compose 会阻塞后端启动,直到 pg_isready 通过。
redis 同样有健康检查 redis-cli ping(docker-compose.yml:28-32),后端也会等它。
networks:默认桥接网络 + 服务名 DNS
本项目没显式写 networks:,Compose 会建一个默认 bridge 网络,把所有服务放进同一个网段。在这个网络里,Docker 内置了一个 DNS,会把服务名解析成对应容器的 IP。
所以:
backend里写postgres:5432→ Docker DNS 解析到uas-postgres容器 IP。frontend里写backend:8080→ 解析到uas-backend容器 IP。npm里 forward host 写frontend→ 解析到uas-frontend。
你不需要知道容器 IP,也不需要配 /etc/hosts,Docker 自动搞定。这就是「容器间用服务名互相访问」的核心机制。
7.4 后端 Dockerfile:多阶段构建逐行讲
backend/Dockerfile 只有 12 行,但用了「多阶段构建」这个关键技巧。
FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B -q
COPY src ./src
RUN mvn clean package -DskipTests -B -q
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]逐行解析:
FROM maven:3.9-eclipse-temurin-17 AS build:第一阶段,用「带 Maven 的 JDK 17」镜像,起名build。它有mvn命令和 JDK,能编译 Java。WORKDIR /app:后续命令的工作目录设为/app。COPY pom.xml .:先把pom.xml(依赖清单)拷进去。RUN mvn dependency:go-offline -B -q:只下载依赖,不编译代码。-B批处理模式(不交互),-q安静输出。这步的意义:让依赖下载层和代码层分开,代码改动时不用重新下几百 MB 依赖,构建更快、缓存命中率更高。COPY src ./src:再把源码拷进去(此时依赖已缓存)。RUN mvn clean package -DskipTests -B -q:编译打包成target/*.jar。-DskipTests表示打包时不跑测试(测试在 CI 里单独跑,部署构建要快且测试失败不应阻塞出包;当然这也意味着部署镜像没验证测试,权衡点)。FROM eclipse-temurin:17-jre:第二阶段,用纯 JRE 运行镜像(不含 Maven、不含 JDK 编译器),体积比 JDK 小很多。WORKDIR /app:新阶段重新设工作目录。COPY --from=build /app/target/*.jar app.jar:只把第一阶段的 jar 拷过来。--from=build是关键:它从「build 阶段」拿产物,Maven、源码、依赖全部留在第一阶段,不进最终镜像。EXPOSE 8080:声明容器「监听 8080」,只是文档性声明,真正映射端口由docker-compose.yml的ports/ 内部网络决定。ENTRYPOINT ["java", "-jar", "app.jar"]:容器启动时执行的命令,运行 Spring Boot。
为什么多阶段能让镜像更小
最终镜像只包含:JRE + 一个 jar。Maven(几百 MB)、所有源码、~/.m2 依赖缓存全部被丢弃。最终镜像可能从 1GB+ 降到 300MB 左右。镜像小 → 传输快、拉取快、攻击面小。
镜像层(layers)的概念
Docker 镜像由一层层叠加而成,每条 RUN/COPY 产生一层。关键优化点:把变动少的放前面(如 pom.xml 和依赖),变动多的放后面(如 src)。这样改一行代码时,前面「下载依赖」那层可以直接用缓存,不用重下。本项目第 3-4 行先拷 pom.xml 再下依赖,正是这个套路。
7.5 前端 Dockerfile 与 nginx.conf
前端 Dockerfile(多阶段)
frontend/Dockerfile:
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci -q
COPY . .
RUN npm run build
FROM nginx:alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80逐行解析:
FROM node:20-alpine AS build:第一阶段用 Node 20 构建。COPY package*.json ./:先拷package.json和package-lock.json。RUN npm ci -q:npm ci严格按package-lock.json安装(和npm install不同,ci会删掉node_modules保证干净、可复现,且更快更适合 CI/构建)。COPY . .:拷全部源码。RUN npm run build:执行 Vite 构建,产物在dist/。FROM nginx:alpine:第二阶段用极小的 Nginx 镜像——它不需要 Node,因为构建产物已经是纯静态文件。COPY nginx.conf /etc/nginx/conf.d/default.conf:用我们的配置覆盖默认配置。COPY --from=build /app/dist /usr/share/nginx/html:把构建出的静态文件放进 Nginx 默认根目录。EXPOSE 80。
前端 nginx.conf 两个核心 location
frontend/nginx.conf:
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://backend: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 / → SPA 兜底(try_files)
Vue 是单页应用(SPA),用前端路由(history 模式)。用户访问 https://域名/users/123 时:
- 浏览器向服务器请求路径
/users/123。 - 但服务器(Nginx)上根本没有这个物理文件,只有
index.html。 - 如果没有兜底,Nginx 会返回 404。
try_files $uri $uri/ /index.html; 的意思是:先找对应文件 $uri,再找对应目录 $uri/,都找不到就回退到 /index.html。Vue 加载后读 URL 自己渲染对应页面。这就是 history 模式路由能刷新不 404 的原因。少了这行,子页面一刷新就 404(常见坑,见 7.10)。
第二个 location /api/ → 反代后端
proxy_pass http://backend:8080;:所有以 /api/ 开头的请求,Nginx 转发给后端容器 8080 端口。浏览器只和「同一个域名」打交道,真正的后端地址对浏览器透明——这就是「同源」的好处,顺便规避了大部分 CORS 问题(CORS 详见 Doc/05)。
下面是四个 proxy_set_header,作用是把「原始访客信息」透传给后端:
Host $host:保留原始 Host。X-Real-IP $remote_addr:真实客户端 IP。X-Forwarded-For:转发链 IP 列表。X-Forwarded-Proto $scheme:原始协议(http 还是 https),后端据此判断是否 HTTPS。
注意:
server_name _;是「匹配任意域名」的通配写法,因为真正按域名分发的是外面的 NPM,前端容器只管收请求。
7.6 完整请求链路(ASCII 图)
把前面所有概念串起来,一次「打开网站并登录」的请求是这样走的:
浏览器 (https://uas.aaai.pp.ua)
│ HTTPS :443,携带域名 uas.aaai.pp.ua
▼
┌─────────────────────────────────────────────┐
│ Nginx Proxy Manager (容器 nginx-proxy-manager) │
│ - 80/443 对外,81 管理界面 │
│ - 按域名匹配代理主机 │
│ - Let's Encrypt 证书提供 HTTPS(Force SSL) │
│ - block_exploits 拦截常见攻击 │
│ - 转发到 frontend:80 │
└─────────────────────────────────────────────┘
│ HTTP :80 (Docker 内网,frontend 主机名解析)
▼
┌─────────────────────────────────────────────┐
│ frontend (Nginx 静态服务) │
│ location / → 返回静态文件 + SPA 兜底 │
│ location /api/ → proxy_pass backend:8080 │
└─────────────────────────────────────────────┘
│ /api/* 请求转发(backend 主机名解析)
▼
┌─────────────────────────────────────────────┐
│ backend (Spring Boot :8080) │
│ - /api/auth/login 校验账号密码 │
│ - 写会话到 Redis、读数据到 PostgreSQL │
└─────────────────────────────────────────────┘
│ │
▼ ▼
PostgreSQL:5432 Redis:6379
(业务数据) (登录会话 Token/权限)每一跳说明:
- 浏览器 → NPM :443:用户访问域名,NPM 持有 HTTPS 证书,终结 TLS,并把 HTTP 明文转给内部
frontend:80。NPM 还负责 Force SSL(HTTP 自动跳 HTTPS)和 block_exploits。 - NPM → frontend:80:NPM 的代理主机配置
forward_host=frontend, forward_port=80。这里frontend是 Docker 内网 DNS 解析出的容器 IP——这是「主机名解析」在 Docker 网络里的体现。 - frontend 内部:静态资源直接返回;
/api/请求被proxy_pass到backend:8080(同样是内网 DNS 解析)。 - backend → postgres / redis:后端通过环境变量里的
postgres:5432、redis:6379访问,这些名字在默认 bridge 网络里被 Docker DNS 解析。 - 全程不经过宿主机端口:除了最外层 NPM 的 80/443/81,内部服务间的 5432、8080、6379 都在 Docker 内网,宿主机外部看不到。
7.7 Nginx Proxy Manager(npm)详解
它是什么
Nginx Proxy Manager(简称 npm,镜像 jc21/nginx-proxy-manager)是一个带 Web 界面的反向代理管理器。传统上你要手写 Nginx 配置、手动申请 Let's Encrypt 证书;npm 把这些变成「浏览器里点几下」:
- 添加代理主机(域名 → 后端地址)。
- 一键申请/自动续期 Let's Encrypt 免费证书。
- 开关 Force SSL、HTTP/2、block_exploits 等。
本项目里它的端口(docker-compose.yml:65-68):
80:HTTP 入口(Force SSL 时跳 HTTPS)。443:HTTPS 入口。81:管理后台(初次登录用,强烈建议限制 IP,见 7.11)。
代理主机配置(手动在 UI 里做)
在 http://<服务器IP>:81 登录后,新建 Proxy Host:
- Domain Names:
uas.aaai.pp.ua - Scheme:
http - Forward Hostname / IP:
frontend - Forward Port:
80 - SSL:Let's Encrypt,勾选「Force SSL」「HTTP/2」
- Block Common Exploits:开启
关键:
Forward Hostname填frontend(Compose 服务名),不是localhost、不是127.0.0.1——因为在 npm 容器眼里,localhost是它自己,只有frontend才能被 Docker DNS 解析到前端容器。
setup-npm.sh:用 API 自动创建代理主机
手动点太慢,本项目提供 setup-npm.sh 用 npm 的 HTTP API 自动建好这个代理主机。流程(setup-npm.sh):
- 轮询等待 NPM 的
/api/返回 200(setup-npm.sh:8-15),确保服务就绪。 - 用管理员邮箱密码换 token(
setup-npm.sh:17-25):返回的curl -X POST "${NPM_URL}/api/tokens" \ -d '{"identity":"admin@example.com","secret":"changeme"}'token用于后续鉴权。 - 先查是否已存在该域名代理主机,避免重复创建(
setup-npm.sh:27-31)。 - 用
POST /api/nginx/proxy-hosts创建,JSON 载荷关键字段(setup-npm.sh:37-59):
{
"domain_names": ["uas.aaai.pp.ua"],
"forward_scheme": "http",
"forward_host": "frontend",
"forward_port": 80,
"certificate_id": 0,
"ssl_forced": false,
"block_exploits": true,
"enabled": true
}字段含义:
domain_names:绑定的域名。forward_scheme/forward_host/forward_port:转发协议、目标主机(服务名)、目标端口。certificate_id: 0/ssl_forced: false:脚本创建时先不配 SSL(meta.letsencrypt_agree: false)。block_exploits: true:开启常见攻击拦截。enabled: true:立即启用。
⚠️ 重要提醒:
setup-npm.sh只创建「反向代理规则」,不会自动申请证书。跑完脚本后,你必须去http://localhost:81的代理主机详情里手动开启 SSL(选 Let's Encrypt、勾 Force SSL)。脚本末尾也打印了这句提示(setup-npm.sh:61)。证书申请需要域名已正确解析到服务器 IP 且 80 端口可达。
7.8 数据卷:具名卷 vs 绑定挂载,以及 -v 的破坏性
再强调一次数据持久化的两条路:
| 方式 | 写法示例 | 适用 |
|---|---|---|
| 具名卷 | postgres17_data:/var/lib/postgresql/data | 数据库、Redis、证书等需要 Docker 托管持久化的数据(本项目用法) |
| 绑定挂载 | ./nginx:/etc/nginx/conf.d | 你想直接在宿主机编辑文件、或挂载配置文件调试 |
本项目全用具名卷(docker-compose.yml:73-81),由 Docker 统一管理在 /var/lib/docker/volumes/。
docker compose down -v 会删数据!
这是新手最容易踩的「删库」坑:
docker compose down:只删容器和网络,数据卷保留 → 数据库数据还在。docker compose down -v:额外删掉所有具名数据卷 →postgres17_data、redis_data、npm_data、npm_letsencrypt全部清空,数据库归零、证书作废、NPM 配置丢失。
所以:
永远不要把
down -v当成日常重启命令。要重启请用docker compose restart或docker compose up -d。只有确认要「彻底重来」时才加-v,且事先备份。
备份数据卷示例:
# 备份 PostgreSQL 数据卷
docker run --rm -v uas_postgres17_data:/data -v $(pwd):/backup alpine \
tar czf /backup/postgres-backup.tar.gz -C /data .7.9 常用命令与排查(带解释)
| 命令 | 作用 |
|---|---|
docker compose up -d --build | 重新构建镜像并后台启动。--build 让代码改动生效 |
docker compose ps | 列出服务状态(Up / Exit / 健康状态) |
docker compose logs -f backend | 跟踪后端日志,-f = follow 持续输出 |
docker compose down | 停止并删容器(保留数据卷) |
docker compose down -v | 连同数据卷一起删(危险) |
docker compose config --quiet | 校验 docker-compose.yml 语法,无输出即正确 |
docker compose exec backend sh | 进到 backend 容器里执行命令(调试神器) |
docker compose restart backend | 只重启后端 |
docker compose up -d --scale backend=2 | 把后端扩到 2 个实例 |
进容器调试示例:
# 进 backend 容器看环境变量是否注入正确
docker compose exec backend sh
> echo $DB_URL
> echo $REDIS_HOST
# 在容器内直连数据库测试
> apt-get update && apt-get install -y postgresql-client
> psql "$DB_URL" -c "SELECT 1;"7.10 健康检查与可观测性
三层健康信号,帮你判断「到底哪挂了」:
容器内 healthcheck(Compose 层):
- PostgreSQL:
pg_isready -U uas -d uas(docker-compose.yml:16)。 - Redis:
redis-cli ping(docker-compose.yml:29)。 docker compose ps会显示health: starting / healthy / unhealthy。
- PostgreSQL:
后端业务健康检查
/api/health:- 返回
{"status":"ok"}(README.md:376)。 - 用于代理层和运维判断「服务活没活」。
- 本地:
curl http://localhost:8080/api/health;公网:curl https://uas.aaai.pp.ua/api/health。
- 返回
手动连通性测试:
# 数据库 docker compose exec postgres pg_isready -U uas -d uas # Redis docker compose exec redis redis-cli ping # 后端 curl http://localhost:8080/api/health
7.11 安全与生产建议(来自 README)
综合 README.md:625-632 的「安全与生产建议」:
- 改默认密码:
docker-compose.yml里POSTGRES_PASSWORD: uas123456、INITIAL_ADMIN_PASSWORD: changeme(docker-compose.yml:9、:64)都是示例值,上线必须改。 - 别暴露 5432 公网:如果数据库只给容器内部用,删掉
ports: "5432:5432"(docker-compose.yml:11-12),数据库就不对外开了。 - 限制 NPM 管理端口 81:
81是后台,建议用防火墙限制来源 IP,或只在内网访问,否则任何人都能改你的代理规则。 - 密码哈希升级:当前
PasswordHasher用 SHA-256(见README.md:316),教学/演示够用,真实生产应换 BCrypt / Argon2 / PBKDF2(带盐、可调成本)。 - 限制 Swagger 公网暴露:Swagger UI 在
/swagger-ui.html(README.md:449)。当前前端 Nginx 只代理/api/,Swagger 默认不对外;如需公网访问要单独配代理,建议生产关闭或加鉴权。 - 敏感配置外置:把密码迁到
.env文件或 Docker secrets,不要硬编码进公开仓库。
.env 用法示例(不提交到 git):
# .env(本地,加入 .gitignore)
POSTGRES_PASSWORD=你的强密码
NPM_ADMIN_PASSWORD=另一强密码# docker-compose.yml 中引用
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}7.12 扩展能力:为什么能轻松扩成多个后端
注意后端是无状态的:登录会话存在 Redis(README.md:287),业务数据在共享的 PostgreSQL。所以你可以:
docker compose up -d --scale backend=2这会起 2 个 uas-backend 实例(Compose v2 用 --scale,注意需配合 container_name 去掉或用 docker compose up 的扩展)。它们:
- 共享同一 PostgreSQL → 数据一致。
- 会话都在 Redis → 用户请求打到任意一个后端都能认出 Token。
- 前端 Nginx 和 NPM 的转发目标仍是
backend服务名(Docker 内置负载均衡到多个实例)。
这正是「无状态服务 + 外部化会话/数据」架构的价值:水平扩展几乎零成本。反例:如果会话存在后端内存里,扩 2 个实例就会「登录在 A、请求到 B 就掉线」。
7.13 故障排查手册
按「现象 → 原因 → 处理」整理。
容器起不来 / 一直重启
- 先看日志:
docker compose logs <服务名>,或docker compose ps看状态Exit (1)。 - 常见:端口被宿主机占用(如 5432 已被本机 PostgreSQL 占了)→ 改宿主机端口或停掉本机服务。
- 镜像构建失败:
docker compose build <服务>单独构建看详细报错。
后端连不上 PostgreSQL(Connection refused / database is starting up)
- 检查
DB_URL主机名是不是postgres(服务名),不是localhost(localhost在容器里指自己)。 - 检查
depends_on: condition: service_healthy是否生效:docker compose ps看 postgres 是否为healthy。若 DB 还没健康,backend 会被阻塞等待,日志会显示连接重试。 - 确认
POSTGRES_DB/USER/PASSWORD与后端DB_*一致(docker-compose.yml:6-9vs:38-41)。
NPM 返回 502 Bad Gateway
- 含义:NPM 能把请求转给
frontend:80,但 frontend 没响应 → 前端容器挂了或没起来。 - 处理:
docker compose ps看 frontend 状态,docker compose logs frontend排查 Nginx 配置错误(如nginx.conf语法错)。
前端子页面刷新 404
- 多半是
try_files ... /index.html没生效(见 7.5)。检查frontend/nginx.conf:8是否还在;确认 Nginx 用的是这份配置(Dockerfile 第 9 行覆盖的是default.conf)。
CORS 报错(403 Invalid CORS request)
- 后端
WebConfig允许的来源列表里必须包含生产域名(README.md:271-278)。若uas.aaai.pp.ua没加入allowedOrigins,浏览器带Origin发非简单请求时会被后端拒。详见 Doc/05。
HTTPS 证书问题
setup-npm.sh跑完不会自动有证书,必须去 NPM UI 手动开启 Let's Encrypt(见 7.7)。- 证书申请失败常见原因:域名没解析到服务器 IP、80 端口被防火墙挡、NPM 容器没起来。
- 证书存在
npm_letsencrypt卷,删卷(down -v)会丢失,需重新申请。
7.14 常见坑汇总
localhost在容器里指自己:backend 里写localhost:5432连的是 backend 自己,必须写服务名postgres。down -v清库:日常重启别加-v(见 7.8)。- SPA 刷新 404:缺
try_files ... /index.html兜底(见 7.5)。 - NPM 填
localhost作 forward host:应填frontend服务名。 - 忘了手动开 SSL:脚本只建代理规则,证书要手动开(见 7.7)。
- 改了代码没
--build:up -d不会重构建,必须加 --build或先build。 - 依赖下载慢/缓存失效:Dockerfile 先拷
pom.xml/package*.json再下依赖,是为了缓存;若把COPY . .放最前,每次改代码都会重下依赖。 - 217 端口/81 端口暴露公网:NPM 的
81是后台,别裸奔公网。 - CORS 域名没加白:生产域名需在后端
allowedOrigins(见 7.13)。
7.15 小结与练习
回顾要点
- 镜像(静态快照)→ 容器(运行实例)→ Compose(编排多个容器 + 网络 + 依赖)。
docker-compose.yml用environment注入配置、volumes持久化数据、healthcheck + depends_on控制启动顺序、restart: unless-stopped保活。- 后端多阶段构建:Maven 编译 → JRE 运行,镜像更小。
- 前端多阶段构建:Node 构建 → Nginx 托管;
try_files兜底 SPA,/api/反代后端。 - 请求链:浏览器 → NPM(HTTPS) → frontend(Nginx) → backend(Spring) → postgres/redis,全程靠 Docker DNS 用服务名解析。
- NPM 负责域名、HTTPS、反代;
setup-npm.sh用 API 自动建代理主机,但 SSL 需手动开。
练习
- 校验语法:运行
docker compose config --quiet,确认无报错(无输出即正确)。 - 加一个环境变量并验证:在
docker-compose.yml的backend下加MY_DEBUG: "true",执行docker compose up -d backend,然后用docker compose exec backend sh -c 'echo $MY_DEBUG'确认注入成功。完成后记得删掉这行或改回。 - 用
docker exec看运行容器环境:docker compose exec backend env,对照docker-compose.yml的environment块,理解「文件里的配置 = 容器里的环境变量」。 - 观察启动顺序:
docker compose logs -f同时看 postgres 和 backend,确认 backend 是在 postgres 变healthy之后才连接数据库。 - 跑两个后端副本:
docker compose up -d --scale backend=2(注意:Compose v2 对带container_name的服务用--scale可能报错,可临时删掉container_name再试),然后用curl https://uas.aaai.pp.ua/api/health多次,理解「无状态 + Redis 会话」让多实例透明协作(见 7.12)。 - 模拟删库警告:先
docker compose exec postgres psql -U uas -d uas -c "SELECT count(*) FROM sys_user;"看有数据,再思考down -v后这行查询会失败——不要真的执行down -v,除非你已备份且确定要重置。 - 手动走通 SSL:跑
setup-npm.sh后,登录http://<服务器IP>:81,找到uas.aaai.pp.ua代理主机,开启 Let's Encrypt 证书并勾 Force SSL,然后用curl -I https://uas.aaai.pp.ua/验证返回HTTP/2 200或HTTP/1.1 301跳转 HTTPS。 - 安全自检:检查
docker-compose.yml里是否还有uas123456/changeme这类默认密码,尝试把它们迁移到.env并用${...}引用(见 7.11)。