# 所有密钥都从 docker/.env 读(见 .env.example)。 # # ── 库和缓存:两种跑法,按需选一种 ── # # A. 用外部 MySQL / Redis(默认):在 .env 里把 SPRING_DATASOURCE_URL / # SPRING_REDIS_HOST 等填成外部实例,然后 `docker compose up -d --build`。 # 下面那两个自带容器挂着 profiles: ["local-db"],不带 --profile 不会启动。 # # B. 用自带的 MySQL / Redis 容器: # `docker compose --profile local-db up -d --build`。 # # **插值先于 profile 过滤。** compose 会把整份文件里的 ${...} 先解析一遍,再看哪个 # service 被 profile 排除掉。所以上面那两个容器的变量必须是 ${VAR:-}(可空)—— # 写成 ${VAR:?} 的话,只跑 A 模式的人会被一个他根本不启动的容器卡住,报错还指向 # 一个跟他无关的变量。反过来,连库用的那几项(口令、主密钥)两种模式都要,照旧 :?。 # # 需要 Docker Compose >= 2.20(depends_on 的 required 是 2.20 引入的;profiles 更早)。 # # 用 ${VAR:?} 而不是 ${VAR:-默认值} 是刻意的:没配就直接拒绝启动。 # 这个文件原先只注入了数据源和 Redis 的变量,**没有**注入 APP_SECRET_KEY, # 于是照仓库文档部署出来的系统用的就是写死在 application.yml 里的那个密钥 —— # 而它随仓库公开,等于把设备令牌的签发权一起公开了。文档里写「生产请覆盖」挡不住这件事, # 所以改成不说清楚就起不来。 services: mysql: # 只在 `--profile local-db` 下启动(用外部库时不启动它,见文件头)。 profiles: ["local-db"] image: mysql:8.0 container_name: sms-gateway-mysql environment: # 可空:用外部库的人不需要它。真留空又跑了 --profile local-db 时,MySQL 镜像 # 自己会拒绝初始化并报 "Database is uninitialized and password option is not # specified" —— 停在那一刻,而不是悄悄起一个没口令的库。 MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-} MYSQL_DATABASE: sms_gateway # 与后端连库用同一组账号:外部模式改 .env 里的值,这里跟着走,不必改两处。 # :? 意味着「连库必须有口令」—— 后端那边也是这个要求。真遇到没有口令的库 # (不建议,尤其它多半能被局域网里别的东西连上),把这两处一起改成 ${MYSQL_PASSWORD:-}。 MYSQL_USER: ${SPRING_DATASOURCE_USERNAME:-smsuser} MYSQL_PASSWORD: ${MYSQL_PASSWORD:?请在 docker/.env 中设置数据库口令} TZ: ${TZ:-Asia/Shanghai} ports: # 只绑回环:以前是 "3306:3306",等于把数据库端口发布到所有网卡上。 # 左侧可用 MYSQL_PORT 覆盖:本机已有 MySQL 时,把这一套挪到别的端口上并存, # 不必停掉任何一个。注意这只影响「从宿主机怎么连这个容器」, # 后端容器连的是服务名 mysql:3306,跟它无关。 - "127.0.0.1:${MYSQL_PORT:-3306}:3306" volumes: - mysql_data:/var/lib/mysql - ../backend/sql/schema.sql:/docker-entrypoint-initdb.d/01-schema.sql command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-p${MYSQL_ROOT_PASSWORD:-}"] interval: 10s timeout: 5s retries: 5 redis: # 同 mysql:只在 `--profile local-db` 下启动。 profiles: ["local-db"] image: redis:7-alpine container_name: sms-gateway-redis # 自带容器这条路上口令必须非空:Redis 里明文存着管理员令牌(sms:admin:token:*) # 和调用方 API Key(key 名本身就是密钥),没口令 + 端口外放等于把管理后台拱手让人。 # # 但这个「必须」只能在这里强求,不能写成 ${REDIS_PASSWORD:?} —— REDIS_PASSWORD # 本身允许留空(外部 Redis 常常没设 requirepass),而整份文件都会先插值, # 写成 :? 连用外部 Redis 的人一起卡住。所以空口令在这里直接拒绝启动。 command: - sh - -c - | [ -n "$$REDIS_PASSWORD" ] || { echo '自带 Redis 必须设 REDIS_PASSWORD(见 docker/.env.example)'; exit 1; } exec redis-server --requirepass "$$REDIS_PASSWORD" environment: REDIS_PASSWORD: ${REDIS_PASSWORD:-} TZ: ${TZ:-Asia/Shanghai} ports: # 同 MySQL:本机已跑着 Redis 时用 REDIS_PORT 挪开。同样只影响宿主机怎么连它。 - "127.0.0.1:${REDIS_PORT:-6379}:6379" healthcheck: test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-}", "ping"] interval: 10s timeout: 5s retries: 5 backend: # 从哪拉镜像。**服务器上就是靠这一行**:那边不放源码,只 pull 镜像(见 # .github/workflows/release.yml —— 打 tag 时 CI 构建并推到 ghcr.io)。 # # 本机改代码时不受影响:compose 只在带 --build 时才就地构建(那用的是下面 build:), # 不带 --build 且本地没有这个镜像时才会去拉。 # # 服务器上要钉死某一版,就在 docker/.env 里写 BACKEND_IMAGE=...:v1.0.0; # 不写就是 latest(每次 pull 拿最新的)。 image: ${BACKEND_IMAGE:-ghcr.io/hearthealt/sms-gateway-backend:latest} build: context: ../backend dockerfile: Dockerfile container_name: sms-gateway-backend # **host 网络**:容器与宿主机共用网络栈。 # # 这是为了能连服务器上那套 MySQL / Redis —— 它们装在宿主机上、只听 127.0.0.1, # 而 bridge 网络里容器自己的 127.0.0.1 是容器自己,根本到不了宿主机。 # 共用网络栈之后,.env 里直接写 127.0.0.1 就通了,服务器那边**一个字都不用改** # (不动 bind-address、不用 socat、防火墙也不必给 3306/6379 开口子)。 # # 代价(两条都容易踩): # 1. 下面没有 ports: —— host 网络下端口映射**无效**,容器监听什么端口, # 宿主机上就是什么端口。这里让它只听回环(SERVER_ADDRESS), # 于是外部(含设备)只能经前端那层 nginx 进来,8080 不对公网开放。 # 2. 自带容器(--profile local-db)也走这套网络:那时后端要连的是它们**发布的** # 127.0.0.1 端口(MYSQL_PORT / REDIS_PORT),不是服务名 mysql / redis。 network_mode: host environment: # 只监听回环:后端不该被外部直接访问,统一走前端那层 nginx(它会把 /api 转过来)。 # 端口与前端 nginx 配置里的 proxy_pass 用同一个值(都来自 .env 的 BACKEND_PORT)。 SERVER_ADDRESS: 127.0.0.1 SERVER_PORT: ${BACKEND_PORT:-8080} # characterEncoding 要的是 **Java 字符集名**(UTF-8),不是 MySQL 的服务端字符集名 # (utf8mb4)。写成 utf8mb4 时 Connector/J 直接抛 # `Unsupported character encoding 'utf8mb4'`,连接池建不起来、应用启动即退 —— # 表现是容器起来几秒就 Exited,日志里只有一句 Hibernate 的「Unable to determine # Dialect」,跟真正的病因隔了好几层。库表字符集由 MySQL 那侧的 # --character-set-server=utf8mb4 保证,这里只负责连接编码。 # # 默认值里的 mysql / redis 是**自带容器**的名字 —— host 网络下它们解析不到, # 所以真要用自带容器时得把下面几项改成 127.0.0.1 + 发布的端口(.env.example 有说明)。 # 用宿主机上的库和缓存时(服务器上就是这情况)写 127.0.0.1 即可。 SPRING_DATASOURCE_URL: ${SPRING_DATASOURCE_URL:-jdbc:mysql://127.0.0.1:3306/sms_gateway?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8} SPRING_DATASOURCE_USERNAME: ${SPRING_DATASOURCE_USERNAME:-smsuser} SPRING_DATASOURCE_PASSWORD: ${MYSQL_PASSWORD:?请在 docker/.env 中设置数据库口令} SPRING_REDIS_HOST: ${SPRING_REDIS_HOST:-127.0.0.1} SPRING_REDIS_PORT: ${SPRING_REDIS_PORT:-6379} # 可空:外部 Redis 常常没设 requirepass(自带容器那边则强制非空,见 redis 服务) SPRING_REDIS_PASSWORD: ${REDIS_PASSWORD:-} # 系统主密钥:设备令牌 = HMAC-SHA256(deviceId, 它)。 # 换了它,所有已签发的设备令牌立即失效,每台设备都要重新注册。 APP_SECRET_KEY: ${APP_SECRET_KEY:?请在 docker/.env 中设置} # 仅在 admin_user 表为空时用于创建账号。已建过账号的系统,改它不会有任何效果 —— # 那种情况用下面的 APP_ADMIN_RESET_PASSWORD。 APP_ADMIN_DEFAULT_PASSWORD: ${APP_ADMIN_DEFAULT_PASSWORD:?请在 docker/.env 中设置} # 需要轮换管理员密码时临时设上、重启、确认登录成功后删掉。 APP_ADMIN_RESET_PASSWORD: ${APP_ADMIN_RESET_PASSWORD:-} # 消息转发(v2)的加密密钥。生成:openssl rand -base64 32 # # **这里没有「转发总开关」**:它已经搬到数据库的 sys_config 表里, # 在管理后台「系统设置」页开关,改完立即生效、不用重启容器 —— # 原先是个环境变量,改一次就要重启一次,太麻烦。 # # 留空时应用照常启动,只是转发不可启用(在界面上打开开关时会提示你去配)。 # 配置方法见 docs/notify-channel-setup.md。 # # 注意:密钥丢了,已配的渠道凭据全都要重新填 —— 密文在库里,没有密钥读不出来。 NOTIFY_ENCRYPT_KEY: ${NOTIFY_ENCRYPT_KEY:-} TZ: ${TZ:-Asia/Shanghai} depends_on: # required: false —— 这两个服务挂在 local-db profile 上,A 模式下它们压根不在 # 本次启动范围内:不加这句,compose 会因为「依赖的服务没启用」直接失败。 # 加了之后两条路都对:B 模式(profile 开着)照旧按依赖顺序等它们健康再起后端, # A 模式只留一句警告,不阻塞启动。 mysql: condition: service_healthy required: false redis: condition: service_healthy required: false frontend: # 同 backend:服务器上用这一行拉镜像,本机 --build 时用的是下面 build:。 # 要钉版本就改 .env 里的 FRONTEND_IMAGE。 image: ${FRONTEND_IMAGE:-ghcr.io/hearthealt/sms-gateway-frontend:latest} build: context: ../management dockerfile: Dockerfile container_name: sms-gateway-frontend # 同 backend:host 网络(要连宿主机的库和缓存,且只监听回环,两个容器必须在同一 # 个网络栈里才说得上话)。 # # 没有 ports: —— 对外监听哪个端口由 nginx 决定,值来自下面的 FRONTEND_PORT, # 容器启动时由 render-templates.sh 渲染进 default.conf(见 management/nginx.conf)。 # 所以改端口 = 改 .env 里的 FRONTEND_PORT + 重建容器,不再有「改了 ports 没反应」。 network_mode: host environment: # 两个端口都渲染进 nginx 配置:对外监听端口,以及要转给后端的端口 # (后端容器里的 SERVER_PORT 是同一个值)。宿主机上只放行 FRONTEND_PORT。 FRONTEND_PORT: ${FRONTEND_PORT:-8081} BACKEND_PORT: ${BACKEND_PORT:-8080} # 设备要访问的地址**不在这里配**:二维码里写的是「管理员访问后台用的那个 origin」 # (前端唯一入口就是这层 nginx,设备的 /api/... 也由它转给后端), # 所以以后上域名是自动的 —— 用域名打开后台,二维码里就是域名。 # 代价:别用 SSH 隧道 / 内网 IP 打开后台去生成二维码,那样地址是错的。 # 见 management/src/api/device.ts。 TZ: ${TZ:-Asia/Shanghai} depends_on: # 只要后端容器起来就行,不等它健康 —— 后端自己还在等 MySQL/Redis 就绪, # 串起来启动会慢很多,而它晚几十秒可用对静态站点没有影响。 - backend volumes: mysql_data: