新增(server/): - Dockerfile:node:22-alpine、非 root(node uid 1000)、npm ci --omit=dev、 HEALTHCHECK 用 /api/v1/health(alpine 无 curl,改用 node fetch)、exec 形式 CMD。 node:sqlite 是 Node 内置模块,alpine/musl 上实测可用,无需换 slim。 - .dockerignore:排除 .env / data / test / smoke.mjs / coverage / docs, 保证密钥与用户密码哈希不进镜像层。 - docker-compose.yml:3100:3000(避开本机已被占用的 3000)、env_file 读 .env、 bind mount ./data:/app/data(用户要能直接看到/拷走 onlyinone.db)、 restart: unless-stopped、init: true、healthcheck。 docs/deploy.md:TL;DR、首次部署、客户端填什么、局域网、升级/备份、故障表、设计取舍。 修正 DEFAULT_BASE_URL: `https://api.onlyinone.app` 是从不存在的域名,用户不填地址就永远连接失败。 改为 `http://127.0.0.1:3100`(与 compose 宿主机端口一致),本机 Docker 开箱即用。 选「默认可用」而非「留空+强制 UI 提示」:改动面只有一行常量、不碰 UI 逻辑, 而本机试跑正是首要场景;局域网需手填 IP,已在文档写明。 测试:新增默认地址回归防护(不可为空、必须能过 normalizeBaseUrl 且不被改写、 必须是纯 origin、端口须为 3100)。launcher 525/525、server 82/82。
82 lines
3.6 KiB
YAML
82 lines
3.6 KiB
YAML
# Only in One - 账号服务端(本机 Docker 部署)
|
||
#
|
||
# 三条命令跑起来(详见 docs/deploy.md):
|
||
# cp .env.example .env # 然后把 JWT_SECRET 填进去(必填,否则容器拒绝启动)
|
||
# docker compose up -d --build
|
||
# curl http://127.0.0.1:3100/api/v1/health
|
||
#
|
||
# 刻意**不写** `version:` 字段 —— compose v2 已废弃它,写了只会刷一条 WARNING。
|
||
|
||
services:
|
||
# 服务名刻意与 container_name 保持一致。
|
||
# 原因:`docker compose exec / logs / restart` 后面跟的是**服务名**而不是容器名,
|
||
# 两者不同名时 `docker compose exec onlyinone-server ...` 会报
|
||
# 「service "onlyinone-server" is not running」,非常容易踩。
|
||
onlyinone-server:
|
||
build:
|
||
context: .
|
||
dockerfile: Dockerfile
|
||
image: onlyinone-server:local
|
||
container_name: onlyinone-server
|
||
restart: unless-stopped
|
||
|
||
# 宿主机 3100 → 容器 3000。
|
||
# 为什么不写 3000:3000:用户机器上 3000 已被另一个容器占用
|
||
# (`lsof -nP -iTCP:3000 -sTCP:LISTEN` 显示 com.docke 在监听),映射过去会直接
|
||
# EADDRINUSE 起不来。容器内端口恒为 3000,只改左边这一位。
|
||
ports:
|
||
- "3100:3000"
|
||
|
||
# 密钥从宿主机 .env 读,不进镜像层。
|
||
env_file:
|
||
- .env
|
||
|
||
# 下面几项显式声明 → 优先级高于 env_file(compose 的 environment 覆盖 env_file)。
|
||
# 目的:防止用户在 .env 里把 PORT 改掉,导致「端口映射 3100:3000」与
|
||
# 「真实监听 3101」错位,表现为 healthcheck 永远失败。
|
||
# 要换对外端口请改上面 ports 的左边,不要动这里。
|
||
environment:
|
||
NODE_ENV: production
|
||
HOST: 0.0.0.0
|
||
PORT: "3000"
|
||
DB_PATH: /app/data/onlyinone.db
|
||
|
||
# 数据持久化选 **bind mount** 而不是 named volume。
|
||
# 理由:用户的核心诉求是「能直接看到 / 备份数据库」。bind mount 之后
|
||
# onlyinone.db 就是宿主机上的 server/data/onlyinone.db,Finder 里双击就能看到、
|
||
# 拷走就是备份;named volume 得先 `docker volume inspect` 找到
|
||
# /var/lib/docker/volumes/... 的真实路径,在 macOS 上还得进虚拟机,用户碰不到。
|
||
# 代价:Linux 上宿主机目录的 uid 需要与容器内 node(1000) 匹配,见 deploy.md 排障表。
|
||
volumes:
|
||
- ./data:/app/data
|
||
|
||
# 用 tini 当 PID 1。
|
||
# 不装的话 node 本身就是 PID 1,而 **Linux 内核不会把「默认动作是终止」的
|
||
# SIGTERM 投递给 PID 1**(除非进程自己装了 handler)。server.js 没装 SIGTERM
|
||
# handler,于是 `docker stop` 会静默等满 10s 超时再 SIGKILL。
|
||
# 加上 init 后 node 变成子进程,SIGTERM 正常转发,停容器是秒级。
|
||
init: true
|
||
|
||
# healthcheck 在 compose 层再声明一次(与 Dockerfile 里的一致):
|
||
# 好处是 `docker ps` / `docker compose ps` 能直接看到 healthy 状态列。
|
||
healthcheck:
|
||
test:
|
||
- CMD
|
||
- node
|
||
- -e
|
||
- "fetch('http://127.0.0.1:3000/api/v1/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
|
||
interval: 30s
|
||
timeout: 5s
|
||
start_period: 20s
|
||
retries: 3
|
||
|
||
# 日志:server/logger.js 是**双写** —— 同时写 stdout 和 ${logDir}/server.log。
|
||
# stdout 那份由 docker 收集(就是 json-file),下面做大小轮转防止无限增长;
|
||
# 文件那份落在 bind mount 里 → server/data/logs/server.log(自带 5MB×3 轮转)。
|
||
# 两种看法见 deploy.md。
|
||
logging:
|
||
driver: json-file
|
||
options:
|
||
max-size: "10m"
|
||
max-file: "3"
|