Turtl 0.1.3 Docker Compose 部署指南
从官方 turtl/server 源码构建镜像并用 Docker Compose 部署 Turtl server 0.1.3,覆盖 PostgreSQL 持久化、注册认证同步全链路验证、备份恢复与常见故障排查。
Turtl 0.1.3 Docker Compose 部署指南
目标:在 Ubuntu 26.04 服务器上以 Docker Compose 从源码构建并部署 Turtl server 0.1.3(
turtl/server官方源码构建镜像 + PostgreSQL 11),完成注册、认证、同步推送/拉取、落库持久化全链路验证。本文所有命令与回显均为 2026-11-02 实测采集。
一、环境信息
- 操作系统:Ubuntu 26.04.1 LTS(x86_64)
- Docker:29.9.2,build a82e528
- Docker Compose:v5.6.1
- 应用:Turtl server 0.1.3(镜像
turtl-server:0.1.3,由turtl/server源码构建,基座node:11-alpine) - 数据库:PostgreSQL 11(
postgres:11-alpine,与官方 compose 一致) - 对外端口:8144(映射到应用内部 8181)
- 网络:
turtl-0.1.3-net(bridge,固定子网 10.89.61.0/24) - 部署目录:
/opt/turtl/deploy/(compose 项目名turtl-0.1.3-stack)
二、镜像选型说明
Turtl 没有官方 Docker Hub 镜像(Docker Hub 检索 turtl 命名空间命中 0,查询日期 2026-11-02),server 仓库也从未发布 Release。可用部署形态对照:
| 形态 | 来源 | 说明 |
|---|---|---|
源码构建 turtl-server:0.1.3 | 官方仓库自带 Dockerfile(build 型 compose) | 版本以 commit 自锁,本文采用 |
| 社区镜像(webofmars/turtl-docker 等) | Docker Hub | 第三方打包,版本与更新节奏不受控 |
| 旧架构镜像(turtl + gatekeeper + couchdb) | 历史教程 | 旧同步架构,org 已无 gatekeeper 仓库,勿采用 |
官方 Dockerfile 全文(turtl/server 仓库根目录,本文构建即用它):
FROM node:11-alpine
ARG TURTL_SERVER_PLUGIN_REPO
ARG TURTL_SERVER_PLUGIN_LOCATION
EXPOSE 8181
WORKDIR /app
COPY . .
COPY config/config.yaml.docker config/config.yaml
RUN apk add -U bash git &&\
npm install --production &&\
./scripts/install-plugins.sh &&\
mkdir /plugins /uploads
ENTRYPOINT ["/app/entrypoint.sh"]
三个部署要点都藏在这份文件里:
- 第 9 行把
config/config.yaml.docker拷成运行配置config/config.yaml(镜像内置基线:db.host: 'db'、插件目录/plugins、附件目录/uploads、allow_unconfirmed_invites: true)。 - 入口脚本
entrypoint.sh强制要求TURTL_APP_SECURE_HASH_SALT环境变量,未设置直接退出(回显:TURTL_APP_SECURE_HASH_SALT is unset.);通过后先跑scripts/init-db.sh建表(幂等),再node server.js启动。 - 基座是
node:11-alpine(已 EOL),安全补丁需自行维护或换基座重建。
三、前置条件
- 已安装 Docker 与 compose 插件:
docker compose version
Docker Compose version v5.6.1
- 目标端口空闲(部署前检查,无输出即空闲;本文部署时 8144 空闲后才配置映射):
ss -ltn | grep ':8144' || echo "8144 空闲"
- 建立部署目录并获取源码(仓库无 tag,master 即最新,建议记录 commit 哈希自锁版本):
mkdir -p /opt/turtl/deploy && cd /opt/turtl/deploy
git clone https://github.com/turtl/server.git server
四、部署步骤
4.1 编写 compose 文件
/opt/turtl/deploy/docker-compose.yml(本文实测文件原样;基于官方仓库 docker-compose.yml 生产化改写——加项目名、宿主端口 8144、固定子网、pgdata bind mount、API_URL 变量):
name: turtl-0.1.3-stack
services:
turtl-server:
build:
context: ./server
image: turtl-server:0.1.3
environment:
TURTL_DB_HOST: postgres-db
TURTL_DB_PORT: 5432
TURTL_DB_DATABASE: turtl
TURTL_DB_USER: turtl
TURTL_DB_PASSWORD: turtl
TURTL_APP_SECURE_HASH_SALT: abuHassN892MMn
TURTL_APP_API_URL: http://127.0.0.1:8144
ports:
- 8144:8181
depends_on:
postgres-db:
condition: service_healthy
restart: unless-stopped
postgres-db:
image: postgres:11-alpine
environment:
POSTGRES_PASSWORD: turtl
POSTGRES_USER: turtl
POSTGRES_DB: turtl
volumes:
- /opt/turtl/deploy/pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U turtl"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
networks:
default:
name: turtl-0.1.3-net
driver: bridge
ipam:
config:
- subnet: 10.89.61.0/24
关键项解说:
build.context: ./server:从官方源码目录构建(即第二章 Dockerfile),产出镜像turtl-server:0.1.3。TURTL_DB_*五项:环境变量覆盖镜像内置配置的db.*(命名规则TURTL_+ YAML 路径大写,详见 《Turtl 0.1.3 服务器配置参数详解》)。TURTL_APP_SECURE_HASH_SALT:必填,entrypoint 未检测到直接 exit 1。该盐用于 auth 哈希与私有数据加密,一旦启用后更换,所有既有账号登录校验立即失效;生产请换长随机值(如openssl rand -hex 32)并妥善备份。TURTL_APP_API_URL:服务器对外访问地址,用于确认/删除邮件里的链接与 307 跳转目标(官方默认值是占位域名,必须改)。ports: 8144:8181:容器内固定监听 8181(DockerfileEXPOSE 8181),宿主端口按需改。depends_on+pg_isready健康检查:server 启动即建表连库,必须等 PG healthy 再起(官方 compose 已有此结构,保留)。volumes: pgdata bind mount:官方 compose 没有任何卷配置,README 明示取消后数据即丢;本文以 bind mount 持久化。networks:自定义 bridge + 固定子网10.89.61.0/24,便于防火墙按网段白名单。
4.2 构建镜像
cd /opt/turtl/deploy && docker compose build turtl-server
输出节选(实测首尾;首次构建需拉取 node:11-alpine 基座并执行 npm install --production 与插件安装,耗时较长,以下为缓存命中时的完整流程):
#1 [internal] load local bake definitions
#1 reading from stdin 546B done
#1 DONE 0.0s
#2 [internal] load build definition from Dockerfile
#2 transferring dockerfile: 363B done
#2 DONE 0.0s
#6 [1/5] FROM docker.io/library/node:11-alpine@sha256:8bb56bab197299c8ff820f1a55462890caf08f57ffe3b91f5fa6945a4d505932
#6 resolve docker.io/library/node:11-alpine@sha256:8bb56bab197299c8ff820f1a55462890caf08f57ffe3b91f5fa6945a4d505932 0.0s done
#6 DONE 0.0s
#10 [5/5] RUN apk add -U bash git && npm install --production && ./scripts/install-plugins.sh && mkdir /plugins /uploads
#10 CACHED
#11 exporting to image
#11 naming to docker.io/library/turtl-server:0.1.3 done
#11 unpacking to docker.io/library/turtl-server:0.1.3 done
#11 DONE 0.1s
Image turtl-server:0.1.3 Built
4.3 启动
cd /opt/turtl/deploy && docker compose up -d
实测回显(节选,实测首尾):
Network turtl-0.1.3-net Creating
Network turtl-0.1.3-net Created
Container turtl-013-stack-postgres-db-1 Creating
Container turtl-013-stack-postgres-db-1 Created
Container turtl-013-stack-turtl-server-1 Creating
Container turtl-013-stack-turtl-server-1 Created
Container turtl-013-stack-postgres-db-1 Starting
Container turtl-013-stack-postgres-db-1 Started
Container turtl-013-stack-postgres-db-1 Waiting
Container turtl-013-stack-postgres-db-1 Healthy
Container turtl-013-stack-turtl-server-1 Starting
Container turtl-013-stack-turtl-server-1 Started
4.4 验证服务
docker compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
turtl-013-stack-postgres-db-1 postgres:11-alpine "docker-entrypoint.s…" postgres-db 23 seconds ago Up 22 seconds (healthy) 5432/tcp
turtl-013-stack-turtl-server-1 turtl-server:0.1.3 "/app/entrypoint.sh" turtl-server 23 seconds ago Up 12 seconds 0.0.0.0:8144->8181/tcp, [::]:8144->8181/tcp
ss -ltn | grep ':8144'
LISTEN 0 4096 0.0.0.0:8144 0.0.0.0:*
LISTEN 0 4096 [::]:8144 [::]:*
公开路由探活(GET / 与 GET /health/db 在认证白名单内,无需凭据):
curl -s http://127.0.0.1:8144/ ; echo; curl -s http://127.0.0.1:8144/health/db
{"greeting":"Hi."}
{"healthy":true}
启动日志确认建表与控制器加载就绪(docker compose logs --no-color 节选,实测首尾):
turtl-server-1 | - running DB schema
turtl-server-1 | - creating indexes
turtl-server-1 | - done
turtl-server-1 | info: Loading controller: users
turtl-server-1 | info: Loading controller: sync
turtl-server-1 | info: Loading controller: spaces
turtl-server-1 | info: Loading controller: files
turtl-server-1 | info: Loading controller: feedback
turtl-server-1 | info: Loading controller: errlog
turtl-server-1 | info: Loading controller: cla
turtl-server-1 | info: Loading controller: health
turtl-server-1 | info: Listening for turtls on port 8181...
turtl-server-1 | info: ::ffff:10.89.61.1 "GET /" 200 18
turtl-server-1 | info: ::ffff:10.89.61.1 "GET /health/db" 200 16
💡 说明:日志里没有
Loading controller: bookmarking是正常的——该控制器默认不加载,需在配置中显式声明隐私风险才启用(见 《Turtl 0.1.3 服务器配置参数详解》 5.7)。
4.5 端到端功能验证(注册 → 认证 → 同步 → 拉取)
以下命令与回显均为实测(端点 http://127.0.0.1:8144;auth blob 为演示值,真实客户端由密钥体系生成)。
注册(POST /users,公开路由;X-Turtl-Client 标识客户端类型):
curl -s -X POST http://127.0.0.1:8144/users \
-H 'Content-Type: application/json' \
-H 'X-Turtl-Client: web:linux' \
-d '{
"username": "admin@turtl.local",
"auth": "dGVzdC1hdXRoLWJsb2ItMDEyMzQ1Njc4OWFiY2RlZg",
"data": {
"pubkey": "3c6b1a5d2e4f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f809",
"name": "admin",
"body": "e30:R0dldCBubyBmcmVlIGx1bmNo",
"avatar": null
}
}'
响应节选(实测,无包裹层直接返回 user 对象):
{"id":"1","username":"admin@turtl.local","active":true,"confirmed":false,"confirmation_token":"56db8d9ed67ef56a517c93fc0768aad662af5985dd95e8ac01b86f5369bf69d2967e886f5d58532ff088bc4ac8ed5a2d8bfeb3deb243b9b15775707d1245f179","data":{"body":"e30:R0dldCBubyBmcmVlIGx1bmNo","name":"admin","pubkey":"3c6b1a5d2e4f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f809"},"last_login":"2026-11-02T07:23:22.779Z","login_failed_last":null,"login_failed_count":0,"created":"2026-11-02T07:23:22.779Z","updated":"2026-11-02T07:23:22.779Z"}
⚠️ 注意:
confirmed:false是因为 email 插件默认关闭、确认邮件静默跳过(源码行为,见 《Turtl 0.1.3 服务器配置参数详解》 5.9)。个人自用不影响同步主链路;要开放多人共享需配好 SMTP 完成确认。
认证检查(POST /auth,Basic 认证,用户名 + auth blob):
curl -s -X POST http://127.0.0.1:8144/auth -u 'admin@turtl.local:dGVzdC1hdXRoLWJsb2ItMDEyMzQ1Njc4OWFiY2RlZg' -H 'X-Turtl-Client: web:linux'
"1"
同步推送(POST /sync,一次推 space/board/note 各一条,id 必须是纯十六进制客户端 id):
curl -s -X POST http://127.0.0.1:8144/sync \
-u 'admin@turtl.local:dGVzdC1hdXRoLWJsb2ItMDEyMzQ1Njc4OWFiY2RlZg' \
-H 'Content-Type: application/json' \
-H 'X-Turtl-Client: web:linux' \
-d '[
{"id":"sync-1","type":"space","action":"add","data":{
"id":"a1b2c3d4e5f60718","user_id":1,
"body":"eyJrIjoiZW5jIn0=:c3BhY2UgYnVja2V0IEVuY3J5cHRpb24="}},
{"id":"sync-2","type":"board","action":"add","data":{
"id":"b2c3d4e5f60718a1","space_id":"a1b2c3d4e5f60718","user_id":1,
"keys":[{"id":"key-1","type":"link","priv":"eyJhbGciOiJHYyJ9","to":"3c6b1a5d2e4f"}],
"body":"eyJrIjoiZW5jIn0=:Ym9hcmQgdGl0bGU6IFdvcms="}},
{"id":"sync-3","type":"note","action":"add","data":{
"id":"c3d4e5f60718a1b2","space_id":"a1b2c3d4e5f60718","board_id":"b2c3d4e5f60718a1","user_id":1,
"has_file":false,"mod":1793604202805,
"keys":[{"id":"key-1","type":"link","priv":"eyJhbGciOiJHYyJ9","to":"3c6b1a5d2e4f"}],
"body":"eyJrIjoiZW5jIn0=:SGVsbG8gVHVydGwsIHRoaXMgaXMgYSBub3Rl"}}
]'
响应(实测原样单行;三条全部进 success,failures/blocked 为空):
{"success":[{"id":"sync-1","user_id":"1","item_id":"a1b2c3d4e5f60718","type":"space","action":"add","sync_ids":["1"],"data":{"id":"a1b2c3d4e5f60718","body":"eyJrIjoiZW5jIn0=:c3BhY2UgYnVja2V0IEVuY3J5cHRpb24=","user_id":"1","members":[{"id":"1","space_id":"a1b2c3d4e5f60718","user_id":"1","role":"owner","created":"2026-11-02T07:23:22.856Z","updated":"2026-11-02T07:23:22.856Z","username":"admin@turtl.local"}]}},{"id":"sync-2","user_id":"1","item_id":"b2c3d4e5f60718a1","type":"board","action":"add","sync_ids":["2"],"data":{"id":"b2c3d4e5f60718a1","body":"eyJrIjoiZW5jIn0=:Ym9hcmQgdGl0bGU6IFdvcms=","keys":[{"id":"key-1","to":"3c6b1a5d2e4f","priv":"eyJhbGciOiJHYyJ9","type":"link"}],"user_id":"1","space_id":"a1b2c3d4e5f60718"}},{"id":"sync-3","user_id":"1","item_id":"c3d4e5f60718a1b2","type":"note","action":"add","sync_ids":["3"],"data":{"id":"c3d4e5f60718a1b2","mod":1793604202805,"body":"eyJrIjoiZW5jIn0=:SGVsbG8gVHVydGwsIHRoaXMgaXMgYSBub3Rl","keys":[{"id":"key-1","to":"3c6b1a5d2e4f","priv":"eyJhbGciOiJHYyJ9","type":"link"}],"user_id":"1","board_id":"b2c3d4e5f60718a1","space_id":"a1b2c3d4e5f60718"}}],"failures":[],"blocked":[]}
增量拉取(GET /sync?sync_id=0,从 0 拉即全量历史):
curl -s 'http://127.0.0.1:8144/sync?sync_id=0' \
-u 'admin@turtl.local:dGVzdC1hdXRoLWJsb2ItMDEyMzQ1Njc4OWFiY2RlZg' \
-H 'X-Turtl-Client: web:linux' | python3 -c "import sys,json;d=json.load(sys.stdin);print('records:',len(d['records']),'sync_id:',d['sync_id'])"
records: 3 sync_id: 3
全量同步、档案容量与用户资料(GET /sync/full、GET /users/1/profile-size、GET /users/1):
curl -s http://127.0.0.1:8144/sync/full -u 'admin@turtl.local:dGVzdC1hdXRoLWJsb2ItMDEyMzQ1Njc4OWFiY2RlZg' | python3 -c "import sys,json;d=json.load(sys.stdin);print('full records:',len(d['records']),'types:',[r['type'] for r in d['records']])"
curl -s http://127.0.0.1:8144/users/1/profile-size -u 'admin@turtl.local:dGVzdC1hdXRoLWJsb2ItMDEyMzQ1Njc4OWFiY2RlZg'; echo
curl -s http://127.0.0.1:8144/users/1 -u 'admin@turtl.local:dGVzdC1hdXRoLWJsb2ItMDEyMzQ1Njc4OWFiY2RlZg'
full records: 4 types: ['user', 'space', 'board', 'note']
53
{"body":"e30:R0dldCBubyBmcmVlIGx1bmNo","name":"admin","pubkey":"3c6b1a5d2e4f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f809","id":"1","username":"admin@turtl.local","confirmed":false,"profile_size":53}
💡 说明:
profile-size端点返回裸 JSON 数字(实测Content-Length: 2),是各空间密文体积之和(字节);带profile_size字段的完整对象要走GET /users/1。
五、数据持久化说明
- PostgreSQL 数据目录:
/opt/turtl/deploy/pgdata(bind mount,重建容器不丢数据)。 - 建表由
scripts/init-db.sh在每次容器启动时自动执行(create table if not exists,幂等),共 12 张表:app、boards、cla、errorlog、keychain、notes、spaces、spaces_invites、spaces_users、sync、sync_users、users。 - 落库核对(psql 实测,完成 4.5 全链路后):
cd /opt/turtl/deploy
docker compose exec postgres-db psql -U turtl -d turtl -c "select 'users' as tbl, count(*) from users;"
docker compose exec postgres-db psql -U turtl -d turtl -c "select 'spaces' as tbl, count(*) from spaces;"
docker compose exec postgres-db psql -U turtl -d turtl -c "select 'boards' as tbl, count(*) from boards;"
docker compose exec postgres-db psql -U turtl -d turtl -c "select 'notes' as tbl, count(*) from notes;"
docker compose exec postgres-db psql -U turtl -d turtl -c "select 'sync' as tbl, count(*) from sync;"
tbl | count
--------+-------
users | 1
(1 row)
tbl | count
--------+-------
spaces | 1
(1 row)
tbl | count
--------+-------
boards | 1
(1 row)
tbl | count
-------+-------
notes | 1
(1 row)
tbl | count
------+-------
sync | 3
(1 row)
users=1、sync=3、spaces=1、boards=1、notes=1,与推送记录一一对应。密文落库形态抽查(笔记表 data 列为 jsonb,body 仍是客户端产出的 iv:ciphertext;users 表存的 auth 是加盐后的哈希而非 curl 用的 blob):
docker compose exec postgres-db psql -U turtl -d turtl -c "select id, data from notes;"
docker compose exec postgres-db psql -U turtl -d turtl -c "select id, username, auth from users;"
id | data
------------------+----------------------------------------------------------------------------------------------------------------------------
c3d4e5f60718a1b2 | {"id": "c3d4e5f60718a1b2", "mod": 1793604202805, "body": "eyJrIjoiZW5jIn0=:SGVsbG8gVHVydGwsIHRoaXMgaXMgYSBub3Rl", "keys": [...], ...}
(1 row)
id | username | auth
----+-------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
1 | admin@turtl.local | C5HFDEARb9EzLpbNlnBgZ2f3kMbKgPGovWGBrJLvk1wfYzL13I7A+2pUkSVAdxhCdBhuMqo6o1wdelCrNWjo71IY3DLjRNFQjgCCaAaEciVVE53EE6NfxzoY3+qkG5qko6sze173gwpH5yy9X+mjVugrskHOn2OrjXpT9YIZRVE=
(1 row)
- 重启持久化验证(server 容器重启后凭原凭据拉取,数据仍在):
docker compose restart turtl-server
curl -s 'http://127.0.0.1:8144/sync?sync_id=0' -u 'admin@turtl.local:dGVzdC1hdXRoLWJsb2ItMDEyMzQ1Njc4OWFiY2RlZg' | python3 -c "import sys,json;d=json.load(sys.stdin);print('records:',len(d['records']),'sync_id:',d['sync_id'])"
Container turtl-013-stack-turtl-server-1 Restarting
Container turtl-013-stack-turtl-server-1 Started
records: 3 sync_id: 3
- 备份即备份数据库:
cd /opt/turtl/deploy
docker compose exec postgres-db pg_dump -U turtl turtl > backup-$(date +%F).sql
ls -l backup-*.sql
-rw-r--r-- 1 root root 17004 Nov 2 17:05 backup-2026-11-02.sql
实测导出 17004 字节,内含全部 12 张 CREATE TABLE(grep -c 'CREATE TABLE' backup-2026-11-02.sql → 12),恢复用 psql -U turtl -d turtl < backup-2026-11-02.sql 即可。
- 附件目录未持久化:镜像内置配置把附件写到容器内
/uploads,重建即丢。生产必须为turtl-server加- ./uploads:/uploads挂载,或改用 S3 存储(见 《Turtl 0.1.3 服务器配置参数详解》 5.10)。
六、环境变量表
server 的配置全部收敛在 config/config.yaml,任意配置项都可用环境变量覆盖,规则为 TURTL_ + YAML 路径大写(源码 helpers/config.js 用 yaml_env.load('TURTL', ...) 实现;官方 README 正文把前缀误写为 TURTLE_,其示例 TURTL_APP_API_URL 才是正确形态)。
| 变量 | 对应配置项 | 说明 |
|---|---|---|
TURTL_DB_HOST / TURTL_DB_PORT / TURTL_DB_DATABASE / TURTL_DB_USER / TURTL_DB_PASSWORD | db.host/port/database/user/password | 数据库连接五元组,本文 compose 全部设置 |
TURTL_APP_SECURE_HASH_SALT | app.secure_hash_salt | 必填,缺失 entrypoint 直接退出 |
TURTL_APP_API_URL / TURTL_APP_WWW_URL | app.api_url/www_url | 对外 API 地址 / 官网地址(邮件链接与 307 跳转目标) |
TURTL_LOGLEVEL | loglevel | debug/info,排障时临时调 debug |
DATABASE_URL | 覆盖整个 db.* | postgres://user:pass@host:port/db 形式,config.js 解析后拆填 |
PORT | server.port | 容器内保持 8181,改它需同步 compose 端口映射 |
TURTL_CONFIG_FILE | 换配置文件 | 相对 config/ 的文件名(如 my.yaml),配合挂载使用 |
TURTL_CONFIG_OVERRIDE | 整段覆盖 | JSON 字符串,逐顶层键替换 config 对象 |
七、配置参数说明
镜像内置运行配置为 config/config.yaml.docker 的拷贝(db.host: 'db'、loglevel: info、allow_unconfirmed_invites: true、插件目录 /plugins、附件目录 /uploads),与裸机默认文件 config/config.yaml.default 的差异及全部参数逐项详解,见 《Turtl 0.1.3 服务器配置参数详解》。
八、配置文件挂载
环境变量覆盖(第六章)适合改标量;要整份换配置,挂载自定义 YAML 并指定文件名:
volumes:
- ./config/my.yaml:/app/config/my.yaml:ro
environment:
TURTL_CONFIG_FILE: 'my.yaml'
⚠️ 注意:Dockerfile 构建时已把
config.yaml.docker拷成镜像内config/config.yaml,若想直接顶替该文件,需挂载同名路径(:ro);用新文件名 +TURTL_CONFIG_FILE更清晰,且改完需docker compose up -d重建容器生效(配置只在启动时读取一次)。
九、等价 docker run
不用 compose 时,以下四条与第四章 compose 等价(同镜像、同环境变量、同端口;服务验证照 4.4 执行):
cd /opt/turtl/deploy
docker build -t turtl-server:0.1.3 ./server
docker network create turtl-0.1.3-net --subnet 10.89.61.0/24
docker run -d --name turtl-db --network turtl-0.1.3-net \
-e POSTGRES_USER=turtl -e POSTGRES_PASSWORD=turtl -e POSTGRES_DB=turtl \
-v /opt/turtl/deploy/pgdata:/var/lib/postgresql/data postgres:11-alpine
docker run -d --name turtl-server --network turtl-0.1.3-net -p 8144:8181 \
-e TURTL_DB_HOST=turtl-db -e TURTL_DB_PORT=5432 \
-e TURTL_DB_DATABASE=turtl -e TURTL_DB_USER=turtl -e TURTL_DB_PASSWORD=turtl \
-e TURTL_APP_SECURE_HASH_SALT=abuHassN892MMn \
-e TURTL_APP_API_URL=http://127.0.0.1:8144 \
turtl-server:0.1.3
⚠️ 注意:
docker run形态没有 compose 的depends_on健康检查门控,PG 首次初始化需数秒,server 若先启动会在init-db.sh建表时连库失败退出——先docker start turtl-db并等pg_isready通过,再启 server 容器。
十、常用运维命令
cd /opt/turtl/deploy
docker compose ps # 查看状态
docker compose logs -f turtl-server # 跟踪访问日志(morgan 逐请求)
docker compose restart turtl-server # 环境变量不变、仅重读容器内配置时用
docker compose up -d # 改 compose 环境变量/挂载后重建生效
docker compose build --no-cache turtl-server && docker compose up -d # 更新源码后重建
docker compose down # 停止(保留 pgdata)
docker compose down -v # 停止并清理网络/卷(本文 pgdata 为 bind mount,需手工删目录才清数据)
docker compose exec postgres-db psql -U turtl -d turtl -c 'select count(*) from sync;' # 查同步日志行数
十一、常见问题
Q1:turtl-server 容器反复重启,日志只有一行 TURTL_APP_SECURE_HASH_SALT is unset.?
entrypoint 的强制校验(源码 entrypoint.sh 第 3–6 行)。在 compose environment 里设置该变量后 docker compose up -d 重建;注意此盐值之后不可随意更换,换了等于废掉全部账号的登录凭据。
Q2:未带凭据访问任何路由都返回 {"error":{"message":"bad login: (bad auth)"}}?
正常行为:认证中间件只放行公开路由白名单(GET /、POST /users、GET /health/<名称> 等),其余一律要 Authorization: Basic。探活要用 GET / 或 GET /health/db,GET /health(不带子路径)不在白名单,实测返回 403。
Q3:POST /sync 发出去没有响应码、curl 超时,日志出现 SyntaxError: Unexpected token ... in JSON at position N?
请求体 JSON 语法错误时,body-parser 解析异常(0.1.3 该异常不经过 morgan,访问日志无此行),curl 实测收到 HTTP 400、响应体为 express 默认错误页(内嵌 SyntaxError 堆栈,775 字节)。而「请求悬置不回响应码(日志记 - -)」发生在另一类错误:PUT /notes/:id/attachment 用 curl --data-binary(默认 Content-Type: application/x-www-form-urlencoded)上传时,body 被 body-parser 中间件消费,流式上传处理器收不到 data/end 事件——请求挂起、/uploads 留下 0 字节文件。附件上传必须声明 Content-Type: application/octet-stream(实测对照与回显见 《Turtl 0.1.3 REST API 使用详解》 附件域)。修复坏 JSON 后主链路即恢复。
**Q4:同步推送里对象 id 用了带横线的 UUID,报 field \id` is not the right type?** 客户端 id 校验规则是 ^[a-f0-9]+$(纯十六进制,不含横线)。标准 UUID 去掉 -` 后再用;实测错误实录见 《Turtl 0.1.3 REST API 使用详解》。
Q5:官方 compose 直接 up 能用,但重建后数据全没?
官方 compose 无数据卷(README 自己标注了这一点),必须像本文 4.1 那样给 postgres-db 配卷;附件目录 /uploads 同样要挂出来。
Q6:基座 node:11-alpine 还能拉到吗?构建失败?
Docker Hub 仍保留 node:11-alpine(本文构建实测 digest sha256:8bb56bab...)。若网络不可达,为 Docker 守护进程配置代理/加速器,或把 Dockerfile 基座换成可达的镜像源再 docker compose build。
十二、参考链接
- server 仓库(README/Dockerfile/docker-compose.yml/config):https://github.com/turtl/server (查询日期 2026-11-02)
- 官网:https://turtl.it
- 参数逐项详解与生产配置模板:见 《Turtl 0.1.3 服务器配置参数详解》
- API 全链路与错误场景实测:见 《Turtl 0.1.3 REST API 使用详解》
- 同类自托管文档系统部署可参考 《Papermerge 3.5.3 Docker Compose 部署指南》