这份文档用于部署叮卡的完整前后端。只需要前端时,使用 README 顶部的 Vercel 按钮即可,不必准备服务器。
Docker Compose 会启动一个 app 容器。容器内的 Fastify 直接提供前端页面、/uploads 图片和 /api 接口。db 卷保存 SQLite 数据库,uploads 卷保存上传图片。
建议使用一台 Linux 服务器,并提前准备:
- 已安装 Git、Docker Engine、Docker Compose 和 OpenSSL。
- 一个指向服务器的域名。只在本机或内网试用时可以暂时不用域名。
- 对外开放 80 和 443 端口。8080 只用于首次检查,正式环境不应直接暴露。
- 足够存放图片和备份的磁盘空间。
Windows 和 macOS 可以通过 Docker Desktop 试跑,但下面的公网部署步骤以 Linux 为准。
克隆仓库并创建配置:
git clone https://github.com/lottshin/DingCard.git
cd DingCard
cp .env.example .env
JWT_SECRET="$(openssl rand -hex 32)"
sed -i "s/^JWT_SECRET=.*/JWT_SECRET=${JWT_SECRET}/" .env
unset JWT_SECRET
chmod 600 .env模板已经固定 DINGCARD_VERSION=0.42.0。先检查配置,再拉取预构建镜像并启动 app:
docker compose config --quiet
docker compose pull
docker compose up -d --no-build
docker compose ps app
curl -f http://127.0.0.1:8080/api/healthdocker compose pull 返回非零时先停止部署,检查版本号、网络和 GHCR 包权限,解决后再重试。
健康检查应返回:
{"ok":true}首次检查时可以通过 http://服务器地址:8080 打开叮卡。此时还是明文 HTTP,不要在公网注册正式账号或录入重要内容。
如果启动失败,先查看 app 日志:
docker compose logs --tail=100 app正式环境应由宿主机上的 Caddy、Nginx 或云负载均衡接收 80/443,再转发到 Compose。先把 .env 中的端口改为:
WEB_PORT=127.0.0.1:8080确认 Compose 只监听本机:
docker compose config | grep -F 'host_ip: 127.0.0.1'
docker compose config | grep -F 'published: "8080"'两条检查都输出匹配结果后再执行重建:
docker compose up -d --force-recreate app此后不要再把防火墙的 8080 端口开放到公网。
Caddy 会自动申请和续期证书。按官方说明安装 Caddy 后,在 /etc/caddy/Caddyfile 中加入:
dingcard.example.com {
reverse_proxy 127.0.0.1:8080
}把域名替换成自己的地址,然后检查并重载:
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy证书可以交给 Certbot 管理。站点的 HTTPS server 块至少需要下面的代理配置:
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $http_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;
}证书路径、80 到 443 的跳转和 TLS 参数应由 Certbot 或现有 Nginx 配置生成。配置完成后检查并重载:
sudo nginx -t
sudo systemctl reload nginx最后从外网访问域名,并再次检查:
curl -f https://dingcard.example.com/api/health完整说明和默认值见根目录的 .env.example。通常只需要关注下面几项:
| 变量 | 用途 |
|---|---|
DINGCARD_VERSION |
GHCR 镜像版本,当前固定为 0.42.0。生产环境不要默认使用 latest。 |
JWT_SECRET |
JWT 签名密钥。必须随机生成,不要提交到 Git。 |
WEB_PORT |
Compose 对外端口。接入 HTTPS 后使用 127.0.0.1:8080。 |
JWT_EXPIRY |
登录有效期,默认 7d。 |
USER_QUOTA_BYTES |
每个用户可使用的图片空间。 |
MAX_UPLOAD_BYTES |
Fastify 接收的单张图片上限;修改后需要重建 app。 |
DINGCARD_ADMIN_TOKEN |
可选的管理员看板令牌。设置后打开 /admin 输入令牌即可查看实例总览(用户、作品、图片占用、分享链接、API 令牌的数量);不设置则看板完全不注册。 |
镜像自带无头 Chromium 与 CJK 字体:POST /api/decks 可以在服务端把一份自由画布文档 JSON 一步渲染成分享链接(接口契约见 docs/backend-plan.md),宿主机不需要装浏览器。渲染在容器内逐个排队,一台小机器同时只渲一份;内存余量建议 1 GB 以上。
修改 .env 后先运行 docker compose config --quiet,再让容器读取新值:
docker compose up -d --force-recreate app编辑器图片抽屉里的「图库」面板可以搜索并导入在线图片。搜索与下载全部由后端代理:密钥只写在服务端 .env 里,不会进前端构建,也不会打进镜像。
| 变量 | 用途 |
|---|---|
PIXABAY_KEY |
Pixabay API 密钥,见 https://pixabay.com/api/docs/。留空 = 不启用。 |
UNSPLASH_KEY |
Unsplash Access Key,见 https://unsplash.com/developers/。留空 = 不启用。 |
PEXELS_KEY |
Pexels API 密钥,见 https://www.pexels.com/api/。留空 = 不启用。 |
三个源全部留空时,面板自动降级到 Openverse——它不需要密钥,只返回 CC0 / 公有领域授权的图片,可商用且无需署名。填了任意一个密钥后,该源会出现在面板的来源选择里,Openverse 始终可用作兜底。
导入的图片与直接上传走同一条管线:服务端下载、校验 MIME 与大小、计入该用户配额、落盘 /data/uploads。修改 .env 中的密钥后同样需要 docker compose up -d --force-recreate app 生效。
数据库使用 SQLite WAL。为了得到一致的数据库和图片快照,备份时先停止 app。下面的命令从 app 容器动态取得实际卷名,不依赖仓库所在目录的名称。
APP_ID="$(docker compose ps --all -q app)"
test -n "$APP_ID"
DB_VOLUME="$(docker inspect "$APP_ID" --format '{{range .Mounts}}{{if eq .Destination "/data"}}{{.Name}}{{end}}{{end}}')"
UPLOADS_VOLUME="$(docker inspect "$APP_ID" --format '{{range .Mounts}}{{if eq .Destination "/data/uploads"}}{{.Name}}{{end}}{{end}}')"
test -n "$DB_VOLUME" && test -n "$UPLOADS_VOLUME"
BACKUP_DIR="$(pwd)/backups/$(date +%Y%m%d-%H%M%S)"
mkdir -p "$BACKUP_DIR"
docker compose stop app
docker run --rm -v "$DB_VOLUME:/source:ro" alpine:3.22 tar -czf - -C /source . > "$BACKUP_DIR/db.tar.gz"
docker run --rm -v "$UPLOADS_VOLUME:/source:ro" alpine:3.22 tar -czf - -C /source . > "$BACKUP_DIR/uploads.tar.gz"
docker compose start app
tar -tzf "$BACKUP_DIR/db.tar.gz" >/dev/null
tar -tzf "$BACKUP_DIR/uploads.tar.gz" >/dev/null
ls -lh "$BACKUP_DIR"备份必须同时包含 db.tar.gz 和 uploads.tar.gz。它们不包含 .env,请单独保管一份受限访问的配置副本。定期把备份复制到另一台机器或对象存储;只留在当前服务器上不能防止磁盘损坏。
恢复会覆盖当前数据库和全部图片。先为当前数据再做一次备份,并确认目标目录同时包含两个归档文件。
新服务器还没有 app 容器时,先执行:
docker compose pull
docker compose create --no-build app已有 app 容器时跳过上面两条命令。然后获取卷名并恢复归档:
BACKUP_DIR="$(pwd)/backups/替换为备份目录"
test -f "$BACKUP_DIR/db.tar.gz"
test -f "$BACKUP_DIR/uploads.tar.gz"
APP_ID="$(docker compose ps --all -q app)"
test -n "$APP_ID"
DB_VOLUME="$(docker inspect "$APP_ID" --format '{{range .Mounts}}{{if eq .Destination "/data"}}{{.Name}}{{end}}{{end}}')"
UPLOADS_VOLUME="$(docker inspect "$APP_ID" --format '{{range .Mounts}}{{if eq .Destination "/data/uploads"}}{{.Name}}{{end}}{{end}}')"
test -n "$DB_VOLUME" && test -n "$UPLOADS_VOLUME"
read -r -p '输入 RESTORE 覆盖当前数据:' CONFIRM
test "$CONFIRM" = 'RESTORE'
docker compose stop app
docker run --rm -i -v "$DB_VOLUME:/target" alpine:3.22 sh -c 'rm -rf /target/* /target/.[!.]* /target/..?* && tar -xzf - -C /target' < "$BACKUP_DIR/db.tar.gz"
docker run --rm -i -v "$UPLOADS_VOLUME:/target" alpine:3.22 sh -c 'rm -rf /target/* /target/.[!.]* /target/..?* && tar -xzf - -C /target' < "$BACKUP_DIR/uploads.tar.gz"
docker compose start app
curl -f http://127.0.0.1:8080/api/health如果正式环境已经绑定域名,也应通过 HTTPS 地址再检查一次。恢复失败时不要反复启动服务,先保留现场并查看归档文件和 app 日志。
升级前先备份。先用当前版本的 Compose 配置停掉旧容器,再拉取新版代码和预构建镜像:
docker compose down --remove-orphans
git pull --ff-only
sed -i 's/^DINGCARD_VERSION=.*/DINGCARD_VERSION=0.42.0/' .env
docker compose config --quiet
docker compose pull
docker compose up -d --no-build
docker compose ps app
curl -f http://127.0.0.1:8080/api/healthdown --remove-orphans 会移除旧容器和网络,但会保留 db、uploads 命名卷。从 0.10.x 升级时,旧 server 和 web 容器也会在这一步移除,不会占用新 app 的端口。不要把 -v 或 --volumes 写进升级脚本。
需要验证本地修改或 GHCR 暂无目标版本时,可以从当前源码构建:
docker compose up -d --build app
curl -f http://127.0.0.1:8080/api/health源码构建是单独的部署方式,不是镜像拉取失败后的自动回退。
# 查看 app 状态
docker compose ps app
# 查看最近日志;需要持续跟随时加 -f
docker compose logs --tail=100 app
docker compose logs -f --tail=100 app
# 检查环境变量和端口展开结果
docker compose config
# 重新创建 app,不改镜像版本
docker compose up -d --force-recreate app常见问题:
- 提示
JWT_SECRET缺失:检查.env中该值是否为空,再运行docker compose config --quiet。 /api/health不是 200:运行docker compose ps app和docker compose logs --tail=100 app检查启动错误。- 页面能打开但上传失败:检查
MAX_UPLOAD_BYTES、磁盘空间和uploads卷。 - 域名出现重定向或同源问题:确认外层代理传递了原始
Host,并把X-Forwarded-Proto设置为实际协议。 - 修改
.env后没有生效:用docker compose up -d --force-recreate app重建容器。
临时停止并保留数据:
docker compose stop app移除容器和网络,但保留数据库与图片卷:
docker compose down下面的命令会永久删除数据库和全部上传图片,只能在已经确认备份可用、并且确实要清空实例时执行:
docker compose down -v后端实现、接口和安全契约见后端实现与接入方案。
本节只供仓库维护者使用,普通部署用户不需要执行。
首次推送版本标签后,GitHub Packages 中的新包可能默认是 private。维护者需要把 ghcr.io/lottshin/dingcard 设为 public,然后重新运行发布工作流。只有匿名 manifest 检查以及 amd64、arm64 两个镜像 smoke job 都通过,才创建 GitHub Release。