feat: 新增 Docker Compose 部署方案

- Dockerfile 单阶段 Alpine 镜像,使用国内镜像(npmmirror / Prisma 引擎 / apk)加速
- entrypoint 首次复制内置图章素材到 uploads volume,自动执行 prisma migrate deploy
- docker-compose.yml 绑定 127.0.0.1:3001,强制走外部 Nginx 反代
- Express 在 production 下同时托管 packages/web/dist 及 SPA fallback
- Prisma schema 增加 linux-musl 二进制目标,支持 Alpine 运行
- 新增 DEPLOY.md 部署指南,含 .env 模板与 Nginx 反代示例

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-19 17:04:25 +08:00
parent 0319557723
commit 711f422558
8 changed files with 307 additions and 1 deletions

181
DEPLOY.md Normal file
View File

@@ -0,0 +1,181 @@
# 部署指南
基于 Docker Compose 的单容器部署方案。容器内 Express 同时托管:
- `/api/*` — API
- `/uploads/*` — 图章静态资源
- 其余路径 — React SPA`packages/web/dist`
外部通过宿主机 Nginx 反向代理到容器的 3000 端口(仅绑定 127.0.0.1,不直接暴露)。
---
## 1. 前置条件
- Linux 服务器Docker ≥ 24、Docker Compose v2
- 已解析到服务器的域名(例 `stamp.example.com`
- 已签发的 SSL 证书Let's Encrypt / acme.sh 等任选)
---
## 2. 拉取代码并配置环境变量
```bash
git clone <repo-url> citywalk-stamp
cd citywalk-stamp
cp .env.production.example .env
vim .env # 填入 JWT_SECRET / ADMIN_API_KEY / SITE_URL
```
必填项:
| 变量 | 说明 |
|---|---|
| `JWT_SECRET` | 用户 JWT 签名密钥,建议 `openssl rand -hex 32` 生成 |
| `ADMIN_API_KEY` | 管理后台访问密钥,同上 |
| `SITE_URL` | 对外域名,例 `https://stamp.example.com`(影响 QR 码链接) |
---
## 3. 启动容器
```bash
docker compose up -d --build
```
首次启动会自动完成:
- Prisma migrate deploy建表
- 把内置的 16 枚图章素材复制到 `./uploads/stamps/`
查看日志确认启动成功:
```bash
docker compose logs -f app
# 看到 "Server running on http://localhost:3000" 即 OK
```
---
## 4. 首次导入图章数据
容器默认不自动 seed 数据库(避免覆盖后续运营数据)。首次部署需手动执行一次:
```bash
docker compose exec app pnpm db:seed
```
> 再次执行会**清空并重建**所有图章,同时级联删除用户的收集记录。生产环境请勿随意重跑。
---
## 5. 配置 Nginx 反向代理
在宿主机上配置 Nginx
```nginx
server {
listen 80;
server_name stamp.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name stamp.example.com;
ssl_certificate /etc/letsencrypt/live/stamp.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/stamp.example.com/privkey.pem;
# 单入口反代:容器里 Express 自己处理 /api、/uploads、SPA 路由
location / {
proxy_pass http://127.0.0.1:3001;
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 ^~ /uploads/ {
proxy_pass http://127.0.0.1:3001;
proxy_set_header Host $host;
expires 7d;
add_header Cache-Control "public, max-age=604800";
}
# 上传体积(管理后台上传图章)
client_max_body_size 10m;
}
```
测试并 reload
```bash
sudo nginx -t && sudo systemctl reload nginx
```
访问 `https://stamp.example.com` 即可看到首页。
---
## 6. 数据持久化与备份
容器两个 bind mount
- `./data/` — SQLite 数据库(`prod.db`
- `./uploads/` — 图章图片(含首次填充的 16 枚 + 后续管理后台上传的)
备份:
```bash
# 停机备份最稳妥SQLite 文件锁)
docker compose stop app
tar czf backup-$(date +%F).tgz data/ uploads/ .env
docker compose start app
```
---
## 7. 常用运维命令
```bash
# 查看日志
docker compose logs -f app
# 进入容器
docker compose exec app sh
# 重启
docker compose restart app
# 升级(拉新代码后重建)
git pull
docker compose up -d --build
# 查看当前图章数
docker compose exec app sh -c 'echo "SELECT COUNT(*) FROM Stamp;" | sqlite3 /app/data/prod.db' \
|| docker compose exec app node -e "
import('@prisma/client').then(async ({PrismaClient}) => {
const p = new PrismaClient();
console.log(await p.stamp.count());
await p.\$disconnect();
});
"
# 完全重置(会删除所有用户和收集数据,谨慎)
docker compose down
rm -rf data/ uploads/
docker compose up -d --build
docker compose exec app pnpm db:seed
```
---
## 8. 故障排查
- **容器启动失败 `PrismaClientInitializationError`**:检查 `./data/` 目录权限、`DATABASE_URL`
- **图章图片 404**:进入容器 `ls /app/packages/server/uploads/stamps` 看是否被 seed 进来;若空,手动执行 `cp /app/stamps-seed/*.jpg /app/packages/server/uploads/stamps/`
- **QR 码扫出来是 `localhost`**`.env``SITE_URL` 没改对,修正后 `docker compose restart app`
- **Nginx 502**:确认容器跑着 `docker compose ps`,端口 `ss -tlnp | grep 3000`