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:
18
.dockerignore
Normal file
18
.dockerignore
Normal file
@@ -0,0 +1,18 @@
|
||||
node_modules
|
||||
**/node_modules
|
||||
**/dist
|
||||
.git
|
||||
.gitignore
|
||||
.env
|
||||
.env.local
|
||||
.env.*
|
||||
!.env.production.example
|
||||
prisma/dev.db
|
||||
prisma/dev.db-journal
|
||||
*.log
|
||||
.DS_Store
|
||||
# 原始素材(镜像只需要 packages/server/uploads/stamps 里的成品)
|
||||
assets/
|
||||
# Docker 构建上下文不需要旧的数据目录
|
||||
data/
|
||||
uploads/
|
||||
12
.env.production.example
Normal file
12
.env.production.example
Normal file
@@ -0,0 +1,12 @@
|
||||
# 部署前复制为 .env 并修改以下值
|
||||
# cp .env.production.example .env
|
||||
|
||||
# JWT 签名密钥(32+ 字符随机串)
|
||||
JWT_SECRET=please-replace-with-a-strong-random-secret
|
||||
|
||||
# 管理后台访问密钥(通过 X-Admin-Key 请求头传入)
|
||||
ADMIN_API_KEY=please-replace-with-a-strong-random-admin-key
|
||||
|
||||
# 对外访问的站点根 URL(用于生成 QR 码中的收集链接)
|
||||
# 必须与你在 Nginx 上配置的域名一致
|
||||
SITE_URL=https://stamp.example.com
|
||||
181
DEPLOY.md
Normal file
181
DEPLOY.md
Normal 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`
|
||||
44
Dockerfile
Normal file
44
Dockerfile
Normal file
@@ -0,0 +1,44 @@
|
||||
FROM node:22-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 国内构建环境加速(仅容器构建期生效,不影响本地/正式 registry)
|
||||
# 1) Alpine apk 源换清华
|
||||
RUN sed -i 's|https\?://dl-cdn.alpinelinux.org|https://mirrors.tuna.tsinghua.edu.cn|g' /etc/apk/repositories
|
||||
|
||||
RUN corepack enable && corepack prepare pnpm@9 --activate \
|
||||
&& apk add --no-cache openssl
|
||||
|
||||
# 2) npm / pnpm registry 换淘宝镜像
|
||||
# 3) Prisma 下载引擎走淘宝镜像
|
||||
ENV PRISMA_ENGINES_MIRROR=https://registry.npmmirror.com/-/binary/prisma
|
||||
RUN npm config set registry https://registry.npmmirror.com/ \
|
||||
&& pnpm config set registry https://registry.npmmirror.com/
|
||||
|
||||
# Copy everything (.dockerignore filters out node_modules, dist, .git, etc.)
|
||||
COPY . .
|
||||
|
||||
# 强制项目级 registry(兜底:防止项目根 .npmrc 未覆盖)
|
||||
RUN echo "registry=https://registry.npmmirror.com/" > /app/.npmrc
|
||||
|
||||
# Install all deps (tsx is used at runtime via devDependencies)
|
||||
RUN pnpm install --frozen-lockfile --registry=https://registry.npmmirror.com/
|
||||
|
||||
# Generate Prisma client (produces linux-musl binary per schema.prisma)
|
||||
RUN pnpm exec prisma generate
|
||||
|
||||
# Build web SPA; server runs directly from TS via tsx
|
||||
RUN pnpm --filter @stamp/web build
|
||||
|
||||
# Stash initial stamp assets so the uploads volume can be seeded on first run
|
||||
RUN mkdir -p /app/stamps-seed \
|
||||
&& cp packages/server/uploads/stamps/*.jpg /app/stamps-seed/ \
|
||||
&& rm -rf packages/server/uploads/stamps
|
||||
|
||||
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
|
||||
ENV NODE_ENV=production
|
||||
EXPOSE 3000
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
||||
19
docker-compose.yml
Normal file
19
docker-compose.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
image: citywalk-stamp:latest
|
||||
container_name: citywalk-stamp
|
||||
restart: unless-stopped
|
||||
# Bind to loopback only: the outside world must go through your host Nginx
|
||||
# 宿主机 3001 -> 容器 3000(3000 已被宿主其它服务占用,例如 1Panel Gitea)
|
||||
ports:
|
||||
- "127.0.0.1:3001:3000"
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
- ./uploads:/app/packages/server/uploads
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
SERVER_PORT: 3000
|
||||
DATABASE_URL: file:/app/data/prod.db
|
||||
20
docker/entrypoint.sh
Normal file
20
docker/entrypoint.sh
Normal file
@@ -0,0 +1,20 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
cd /app
|
||||
|
||||
mkdir -p /app/data
|
||||
mkdir -p /app/packages/server/uploads/stamps
|
||||
|
||||
# Seed stamp assets on first run (idempotent)
|
||||
if [ -z "$(ls -A /app/packages/server/uploads/stamps 2>/dev/null)" ]; then
|
||||
echo "→ Seeding stamp assets into uploads volume..."
|
||||
cp /app/stamps-seed/*.jpg /app/packages/server/uploads/stamps/ 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo "→ Applying database migrations..."
|
||||
pnpm exec prisma migrate deploy
|
||||
|
||||
echo "→ Starting server on :${SERVER_PORT:-3000}..."
|
||||
cd /app/packages/server
|
||||
exec pnpm exec tsx src/index.ts
|
||||
@@ -31,6 +31,17 @@ app.use("/api/redemption", redemptionRoutes);
|
||||
// Admin routes
|
||||
app.use("/api/admin", adminRoutes);
|
||||
|
||||
// Serve built web frontend in production (single-container deployment)
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
const webDist = path.join(__dirname, "../../web/dist");
|
||||
app.use(express.static(webDist));
|
||||
app.use((req, res, next) => {
|
||||
if (req.method !== "GET" && req.method !== "HEAD") return next();
|
||||
if (req.path.startsWith("/api") || req.path.startsWith("/uploads")) return next();
|
||||
res.sendFile(path.join(webDist, "index.html"));
|
||||
});
|
||||
}
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running on http://localhost:${PORT}`);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
provider = "prisma-client-js"
|
||||
binaryTargets = ["native", "linux-musl-openssl-3.0.x"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
|
||||
Reference in New Issue
Block a user