Turtl 0.1.3 Docker Compose 部署指南

从官方 turtl/server 源码构建镜像并用 Docker Compose 部署 Turtl server 0.1.3,覆盖 PostgreSQL 持久化、注册认证同步全链路验证、备份恢复与常见故障排查。

TurtlTurtlDockerDocker ComposePostgreSQL自托管2026/11/02

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(Dockerfile EXPOSE 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_PASSWORDdb.host/port/database/user/password数据库连接五元组,本文 compose 全部设置
TURTL_APP_SECURE_HASH_SALTapp.secure_hash_salt必填,缺失 entrypoint 直接退出
TURTL_APP_API_URL / TURTL_APP_WWW_URLapp.api_url/www_url对外 API 地址 / 官网地址(邮件链接与 307 跳转目标)
TURTL_LOGLEVELlogleveldebug/info,排障时临时调 debug
DATABASE_URL覆盖整个 db.*postgres://user:pass@host:port/db 形式,config.js 解析后拆填
PORTserver.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。

十二、参考链接

Turtl 0.1.3 Docker Compose 部署指南 - Quzuo