Talkyard v1.2026.006 Docker Compose 部署指南

Talkyard v1.2026.006-fa49d1cee 官方 prod-one Docker Compose 栈部署实测:8 镜像选型与组件版本回显、.env/override/play-framework.conf 配置、七服务启动与健康检查、站长创建失败报错实录(保留名/短密码/坏邮箱)、JVM 堆 512MB 验证、建页/回帖/搜索功能冒烟、持久化卷与环境变量表、FAQ 十则。Ubuntu 24.04.5 LTS + Docker 29.9.2 实测。

TalkyardTalkyardDockerPostgreSQLRedisElasticsearch自托管2026/11/02

Talkyard v1.2026.006 Docker Compose 部署指南

目标:在一台 Ubuntu 24.04 服务器上,用官方 talkyard-prod-one 仓库的 Docker Compose 栈从零部署 Talkyard v1.2026.006-fa49d1cee,完成七服务启动、站长创建(含失败报错实录)、JVM 堆限制验证与建页/回帖/搜索功能冒烟。全部命令与回显均已在 Ubuntu 24.04.5 LTS + Docker 29.9.2 环境实测通过。

一、环境信息

本文部署过程在以下环境实测:

  • 操作系统:Ubuntu 24.04.5 LTS(x86_64)
  • Docker 版本:29.9.2(docker version --format '{{.Server.Version}}' 实测回显 29.9.2)
  • Compose 版本:v5.6.1(docker compose version 实测回显 Docker Compose version v5.6.1)
  • Talkyard 版本:v1.2026.006-fa49d1cee(官方部署仓库 debiki/talkyard-prod-one ty-prod-one-v1 分支,发布通道 RELEASE_BRANCH=tyse-v1-regular;定位与选型背景见《Talkyard 介绍与选型指南》)
  • 部署目录:/opt/data/quzuo/tmp/talkyard/talkyard-v1/(官方脚本默认假设安装在 /opt/talkyard-v1,本文按实际目录执行)
  • Compose 项目名:talkyard-v1-2026-006-stack(.env 中 COMPOSE_PROJECT_NAME,卷/网络名均以它为前缀)
  • 对外端口:宿主机 8139 → web 服务 80(本文测试环境用 8139 避免占用 80/443;生产直接映射 80:80、443:443)
  • 网络:5 个自定义网络,槽位 10.89.60/61/62/63/65 各 /24,其中 3 个 internal: true
  • 服务内存占用:全栈约 2.5 GiB(docker stats --no-stream 实测,见 10.2 节)
  • 演示站长账号:tyadmin / q1w2e3r4ty,邮箱 admin@talkyard.io(密码 10 位是服务端强制下限,见 4.9 节报错实录)

二、镜像选型说明

Talkyard 官方把每个服务拆成独立镜像,发布在 Docker Hub debiki 组织下,tag 即版本号(compose 中镜像表达式为 ${DOCKER_REG_ORG}/talkyard-<服务>:${PINNED_VERSION_TAG:-${VERSION_TAG}})。本文实测拉取并运行的 8 个镜像(docker images --format '{{.Repository}}:{{.Tag}}\t{{.ID}}\t{{.Size}}' | grep debiki 实测回显):

debiki/talkyard-web:v1.2026.006-fa49d1cee	c3940f0159ba	859MB
debiki/talkyard-rdb:v1.2026.006-fa49d1cee	516a6339972d	658MB
debiki/talkyard-egressp:v1.2026.006-fa49d1cee	640a28d78572	253MB
debiki/talkyard-search:v1.2026.006-fa49d1cee	e7d9f5028e0a	2.22GB
debiki/talkyard-cache:v1.2026.006-fa49d1cee	3f3d04590b63	202MB
debiki/talkyard-rendr:v1.2026.006-fa49d1cee	9d606b553cf9	117MB
debiki/talkyard-app:v1.2026.006-fa49d1cee	5b2a1b3c2f02	820MB

backup 镜像按需 docker compose run 启动、不随主栈常驻(profiles: [backup]),本文部署阶段未拉起,故不在上表。

单个镜像显式拉取实测(已存在时快速返回):

docker pull debiki/talkyard-app:v1.2026.006-fa49d1cee
v1.2026.006-fa49d1cee: Pulling from debiki/talkyard-app
Digest: sha256:5b2a1b3c2f026015fd0ec0cfaba683a78577351ee88b8d1a2d2d7d6017e70c54
Status: Image is up to date for debiki/talkyard-app:v1.2026.006-fa49d1cee
docker.io/debiki/talkyard-app:v1.2026.006-fa49d1cee

各镜像内组件版本(进入容器实测):

docker compose exec -T web nginx -v
docker compose exec -T app java -version
docker compose exec -T cache redis-server --version
docker compose exec -T rdb psql -U talkyard -d talkyard -c 'select version();'
docker compose exec -T search curl -s http://localhost:9200

回显依次为:

nginx version: openresty/1.29.2.5
openjdk version "11.0.30" 2026-01-20
OpenJDK Runtime Environment Temurin-11.0.30+7 (build 11.0.30+7)
OpenJDK 64-Bit Server VM Temurin-11.0.30+7 (build 11.0.30+7, mixed mode, sharing)
Redis server v=8.6.1 sha=00000000:1 malloc=jemalloc-5.3.0 bits=64 build=16af4e2dbf02df6b
                                                      version
--------------------------------------------------------------------------------------------------------------------
 PostgreSQL 18.6 (Debian 18.6-1.pgdg13+2) on x86_64-pc-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit
(1 row)
{
  "name" : "TyEsNode1",
  "cluster_name" : "TyEsCluster",
  "cluster_uuid" : "WakJPts7RXOKmVWYLy04pw",
  "version" : {
    "number" : "9.3.1",
    "build_flavor" : "default",
    "build_type" : "docker",
    "build_hash" : "0dd66e52ba3aa076cf498264e46339dbb71f0269",
    "build_date" : "2026-02-23T23:37:38.684779921Z",
    "build_snapshot" : false,
    "lucene_version" : "10.3.2",
    "minimum_wire_compatibility_version" : "8.19.0",
    "minimum_index_compatibility_version" : "8.0.0"
  }
}

⚠️ 注意:官方文档旧版常写 Elasticsearch 8.x,v1.2026.006 的 talkyard-search 镜像实际内置 ES 9.3.1(上面实测回显),做 ES 参数调优时按 9.x 语义对照。

💡 技术拓展:Talkyard 镜像的版本选择有三种写法——

写法效果适用
.env 的 VERSION_TAG=v1.2026.006-fa49d1cee锁定具体版本(tag 含 Git 修订号,不可变)生产
PINNED_VERSION_TAG=<某版>临时钉住某版(优先级高于 VERSION_TAG,升级脚本用它拉新版)灰度/回滚
latest 类浮动 tag官方 self-hosted 通道不提供,勿用—

本文采用第一种:VERSION_TAG 锁定 v1.2026.006-fa49d1cee,PINNED_VERSION_TAG 留空。

三、前置条件

  1. 服务器已安装 Docker Engine(含 Compose v2 插件):
docker version --format '{{.Server.Version}}'
29.9.2
docker compose version
Docker Compose version v5.6.1
  1. 内存 ≥4 GB:官方 mem/ 目录按内存档位提供编排覆盖文件(如 mem/4g.yml 设 PLAY_HEAP_MEMORY_MB=1024);全栈常驻含 Play(JVM)+ES(JVM)+PG+Redis 四个重进程,实测总占用约 2.5 GiB(见 10.2 节),4 GB 是下限、8 GB 更从容。
  2. 磁盘 ≥10 GB:8 个镜像合计约 5.1 GB(见第二章表),另有数据卷与日志增长空间。
  3. 对外端口空闲:本文用宿主机 8139(ss -ltn | grep :8139 部署前无输出即可);生产 80/443 需无其他服务占用。
  4. 可访问 Docker Hub(拉取 debiki/talkyard-* 镜像)。
  5. 生产服务器建议先执行官方 scripts/prepare-os.sh 配置内核参数(net.core.somaxconn 等,对应 Nginx listen ... backlog=8192 的 [BACKLGSZ] 注释要求)。
  6. 部署目录内准备 secrets/postgres_password.txt(见 4.3 节)。

四、部署步骤

4.1 部署目录与文件布局

自托管唯一官方通道是 talkyard-prod-one 仓库 ty-prod-one-v1 分支。克隆后(或直接使用其文件)目录布局如下(ls -la 实测回显,节选):

total 36
drwxr-xr-x 4 root root  4096 Nov  2 17:31 .
-rw-r--r-- 1 root root   847 Nov  2 18:01 .env
-rw-r--r-- 1 root root 11398 Nov  2 17:32 docker-compose.yml
-rw-r--r-- 1 root root   446 Nov  2 17:31 docker-compose.override.yml
drwxr-xr-x 4 root root  4096 Nov  2 17:31 conf
drwxr-xr-x 2 root root  4096 Nov  2 17:32 secrets

官方仓库还包含 scripts/(备份/升级/定时任务脚本,见第十章)、versions/(版本清单子模块)、mem/(内存档位覆盖)、secrets/ 等目录。

4.2 编写 .env

.env 全文(实测使用版本):

COMPOSE_PROJECT_NAME=talkyard-v1-2026-006-stack
DOCKER_REG_ORG=debiki
RELEASE_BRANCH=tyse-v1-regular
VERSION_TAG=v1.2026.006-fa49d1cee
PINNED_VERSION_TAG=
# ----- Networks (custom subnets for this lab)
FE_PUB_NET_SUBNET=10.89.60.0/24
FE_INT_NET_SUBNET=10.89.61.0/24
BE_INT_NET_SUBNET=10.89.62.0/24
EG_INT_NET_SUBNET=10.89.63.0/24
EG_PUB_NET_SUBNET=10.89.65.0/24
FE_PUB_NET_WEB_IP=10.89.60.21
FE_INT_NET_WEB_IP=10.89.61.21
FE_INT_NET_CACHE_IP=10.89.61.31
FE_INT_NET_APP_IP=10.89.61.41
BE_INT_NET_APP_IP=10.89.62.41
EG_INT_NET_APP_IP=10.89.63.41
BE_INT_NET_RENDR_IP=10.89.62.51
BE_INT_NET_RDB_IP=10.89.62.61
BE_INT_NET_SEARCH_IP=10.89.62.71
EG_INT_NET_EGRESSP_IP=10.89.63.99
EG_PUB_NET_EGRESSP_IP=10.89.65.99
# ----- App env passed through to containers
TALKYARD_SECURE=false
TALKYARD_HOSTNAME=localhost
BECOME_OWNER_EMAIL_ADDRESS=admin@talkyard.io

要点:

  • COMPOSE_PROJECT_NAME 决定网络/卷名前缀,多套部署务必互不相同;
  • 五个网段(10.89.60~65)需与宿主机已有 Docker 网段不冲突;
  • TALKYARD_SECURE=false + TALKYARD_HOSTNAME=localhost 是本地测试形态;生产设 true + 真实域名;
  • BECOME_OWNER_EMAIL_ADDRESS 即站长邮箱,注册时用该邮箱成为 site owner。

4.3 数据库口令 secret

compose 用 Docker file secret 传 PostgreSQL 口令(不落环境变量):

mkdir -p secrets
printf 'q1w2e3r4\n' > secrets/postgres_password.txt

cat secrets/postgres_password.txt 实测回显:

q1w2e3r4

该文件由 rdb 与 app、backup 三个服务挂载,容器内出现在 /run/secrets/postgres_password(app 固定读这个路径,compose 注释 [postgres_pw_path]:「The app always looks here, not configurable」)。

4.4 docker-compose.yml 关键结构

官方 compose(317 行)定义 7 个常驻服务 + 1 个按需 backup。骨架节选:

name: talkyard-v1-2026-006-stack
volumes:
  web-generated:
  pub-files:
  priv-files:
  redis-data:
  pg-data:
  es-data:
  es-logs:
secrets:
  postgres_password:
    file: ./secrets/postgres_password.txt
networks:
  fe_pub_net:   # 对外:仅 web
    ipam: { config: [ { subnet: ${FE_PUB_NET_SUBNET} } ] }
  fe_int_net:   # web ↔ app/cache
    internal: true
  be_int_net:   # app ↔ rdb/search/rendr
    internal: true
  eg_int_net:   # app ↔ egressp
    internal: true
  eg_pub_net:   # egressp ↔ 互联网
    ipam: { config: [ { subnet: ${EG_PUB_NET_SUBNET} } ] }
x-logging: &default_logging
  driver: local     # 驱动自带轮转,不会撑爆磁盘

app 服务节选(说明出网强制走代理与配置挂载):

  app:
    image: ${DOCKER_REG_ORG}/talkyard-app:${PINNED_VERSION_TAG:-${VERSION_TAG}}
    stdin_open: true  # otherwise Play Framework exits
    volumes:
      - ./conf/app/play-framework.conf:/opt/talkyard/app/conf/app-prod-override.conf:ro
      - pub-files:/var/talkyard/v1/pub-files
      - priv-files:/var/talkyard/v1/priv-files
    secrets:
      - postgres_password
    environment:
      - TALKYARD_SECURE
      - TALKYARD_HOSTNAME
      - BECOME_OWNER_EMAIL_ADDRESS
      - 'http_proxy=http://egressp:4750'
      - 'https_proxy=http://egressp:4750'
      - "no_proxy=''"
    security_opt: [no-new-privileges=true]
    cap_drop: [ALL]
    cap_add: [CHOWN, FOWNER, DAC_OVERRIDE, SETUID, SETGID]

安全设计通读要点:所有服务 cap_drop: ALL 后按需补最小 capability;egressp 额外 read_only: true;search 设 ulimits.memlock -1/-1 配合 bootstrap.memory_lock: 'true';backup 用 cpu_shares: 256 + cpu_quota: 60000/cpu_period: 100000 限制最多 60% CPU(官方注释:防止小虚拟机上内核 panic [100_kernel_panic])。

4.5 docker-compose.override.yml

官方建议用 override 文件做本机改动(不动主 compose)。本文实测版本(基于官方 mem/4g.yml 档位再降 JVM 堆):

services:
  web:
    ports:
      - '8139:80'
  app:
    environment:
      PLAY_HEAP_MEMORY_MB: 512
  search:
    environment:
      ES_JAVA_OPTS: '-Xms512m -Xmx512m'
    deploy:
      resources:
        limits:
          memory: 1.6G

逐项说明:web.ports 把对外 80 映射到宿主 8139(生产删掉本行、用主 compose 的 80:80/443:443);PLAY_HEAP_MEMORY_MB=512 控制 Play JVM 堆(4.10 节验证);ES_JAVA_OPTS 限制 ES 堆 512MB;limits.memory: 1.6G 给 ES 容器整体封顶(堆 512MB + 堆外内存)。

4.6 conf/app/play-framework.conf 必填段

应用配置挂进容器覆盖 Play 配置。必填段实测内容:

talkyard.becomeOwnerEmailAddress="admin@talkyard.io"
talkyard.hostname="localhost"
talkyard.secure=false
play.http.secret.key="rCxZ9mEuSXvTgCdgGCJD3LR5PEP73IAFSK5uwd51MMz6ZD5cfICWJYjzfyy5HC6wysSnJGEhT8xFOR"

⚠️ 注意:play.http.secret.key 必须是 80 个以上随机字符,官方配置原文警告 "The server will refuse to start until you've changed this"——不改直接启动,app 会拒绝启动。生成方式示例:openssl rand -base64 64 | tr -d '=+/' | cut -c1-81。

其余段(smtp、反垃圾、featureFlags、环境变量覆盖映射等)为官方模板默认值,逐项含义见本专栏配置详解篇。

4.7 conf/web/sites-enabled/talkyard-servers.conf

Nginx 站点配置(OpenResty)定义 HTTP/HTTPS 两个 server:

  • HTTP server:listen 80 backlog=8192;,/.well-known/acme-challenge 用 content_by_lua_block 交给 resty.acme.autossl 应答 Let's Encrypt HTTP-01 挑战;
  • HTTPS server:listen 443 ssl default_server backlog=8192; + http2 on;,ssl_certificate_by_lua_block 自动签发证书,未签发前用自签兜底证书 /etc/nginx/generated/https-cert-self-signed-fallback.pem(官方注释:没有它 Nginx 起不来);
  • 文件尾部还有注释掉的「通配符自有证书 server」模板。

本地 http 测试(talkyard.secure=false + 只映射 80)时,该文件原样可用,无需域名与证书。

4.8 启动与验证

docker compose up -d

首次会拉取 8 个镜像(约 5 GB,数分钟)。启动后健康检查实测:

docker compose ps --format '{{.Service}}\t{{.Status}}'
app	Up 43 minutes (healthy)
cache	Up About an hour (healthy)
egressp	Up About an hour (healthy)
rdb	Up About an hour (healthy)
rendr	Up About an hour
search	Up About an hour (healthy)
web	Up About an hour (healthy)

rendr 无 healthcheck 显示为 Up(当前版本它是 SSR 预留位,容器内是 trap 'exit 0' TERM; sleep infinity & wait 占位进程,属正常形态)。

端口与页面验证:

ss -ltn | grep 8139
curl -s -o /dev/null -w 'http_code=%{http_code} time=%{time_total}s\n' http://localhost:8139/
LISTEN 0      4096         0.0.0.0:8139       0.0.0.0:*
LISTEN 0      4096            [::]:8139          [::]:*
http_code=200 time=0.012596s

4.9 创建站长(含失败实录)

Talkyard 无 CLI 建站长命令,走 Web API:用 becomeOwnerEmailAddress 邮箱在 /-/login-password-create-user 端点注册即成为站长。XSRF 防护要求把响应 Cookie TyCoXsrf 的值放进 X-XSRF-TOKEN 头:

jar=/opt/data/quzuo/tmp/talkyard/cookies.txt
curl -s -c $jar http://localhost:8139/ -o /dev/null
token=$(grep TyCoXsrf $jar | awk '{print $NF}')

失败实录一:用户名过短(2 字符)

curl -s -b $jar -H "X-XSRF-TOKEN: $token" -H 'Content-Type: application/json' \
  -d '{"username":"ab","password":"q1w2e3r4ty","email":"admin@talkyard.io","userData":{"email":"admin@talkyard.io"}}' \
  http://localhost:8139/-/login-password-create-user
422 Unprocessable Entity
The username is too short; it must be at least 3 characters, please try again. [DwE805T4]

失败实录二:保留用户名 admin

curl -s -b $jar -H "X-XSRF-TOKEN: $token" -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"q1w2e3r4ty","email":"admin@talkyard.io","userData":{"email":"admin@talkyard.io"}}' \
  http://localhost:8139/-/login-password-create-user
403 Forbidden
Username is reserved: 'admin'; choose another username [EdE5PKW01]

失败实录三:.test 保留 TLD 邮箱

curl -s -b $jar -H "X-XSRF-TOKEN: $token" -H 'Content-Type: application/json' \
  -d '{"username":"tyadmin2","password":"q1w2e3r4ty","email":"x@y.test","userData":{"email":"x@y.test"}}' \
  http://localhost:8139/-/login-password-create-user
422 Unprocessable Entity
Bad email address: Invalid email address [TyEBADEMLADR_-PWD]

失败实录四:密码 8 位(低于 10 位下限)

curl -s -b $jar -H "X-XSRF-TOKEN: $token" -H 'Content-Type: application/json' \
  -d '{"username":"tyadmin","password":"q1w2e3r4","email":"admin@talkyard.io","userData":{"email":"admin@talkyard.io"}}' \
  http://localhost:8139/-/login-password-create-user
400 Bad Request
Password too short, should be 10 chars [TyEPWMIN_]

结论:用户名 ≥3 字符且避开保留名(admin 等)、邮箱避开保留 TLD(.test 等)、密码 ≥10 位。本文最终用 tyadmin / q1w2e3r4ty / admin@talkyard.io 创建成功。

⚠️ 注意:常见测试口令 q1w2e3r4 只有 8 位,会被上述第四条报错直接拒绝;本文演示口令相应加长为 q1w2e3r4ty。

登录验证(正确端点是 /-/login-password,/-/login 返回 404 HTML Not Found):

curl -s -b $jar -c $jar -H "X-XSRF-TOKEN: $token" -H 'Content-Type: application/json' \
  -d '{"usernameOrEmail":"tyadmin","password":"q1w2e3r4ty","email":"tyadmin"}' \
  http://localhost:8139/-/login-password
{"weakSessionId":"","xsrfTokenIfNoCookies":""}

登录态佐证(后续 /-/reply 响应中的 usersBrief 实测回显):

[{"id": 100, "username": "tyadmin", "fullName": null, "isAuthenticated": true, "isAdmin": true}]

4.10 JVM 堆限制验证

PLAY_HEAP_MEMORY_MB=512 与 ES_JAVA_OPTS='-Xms512m -Xmx512m' 是否真生效,进容器看进程命令行实测:

docker compose exec -T app sh -c "ps -ef | grep -o 'Xms[0-9]*m -Xmx[0-9]*m' | head -1"
Xms512m -Xmx512m

💡 技术拓展:Talkyard 栈里两个 JVM 的堆控制方式不同——

JVM控制变量位置
app(Play/Scala)PLAY_HEAP_MEMORY_MB(官方封装,映射为 -Xms/-Xmx)override app.environment;官方 mem/4g.yml 档位设 1024
search(Elasticsearch)ES_JAVA_OPTS(ES 原生写法)+ 容器 deploy.resources.limits.memory 封顶override search.environment/deploy

本文按 256–512MB 规范统一取 512;两 JVM 兜底复核方法都是 ps -ef 看实际 -Xms/-Xmx 命令行(如上实测)。

4.11 功能冒烟:建页、回帖、搜索

登录后的 cookie jar 直接可用。创建页面(pageRole=21 通用页、状态 Published):

curl -s -b $jar -H "X-XSRF-TOKEN: $token" -H 'Content-Type: application/json' \
  -d '{"pageRole":21,"pageTitle":"部署冒烟测试页","pageBody":"正文来自 B2 复测。","pageStatus":"Published"}' \
  http://localhost:8139/-/create-page
{"newPageId":"17"}

在页面 17 的 1 楼(页面正文本身)下回帖:

curl -s -b $jar -H "X-XSRF-TOKEN: $token" -H 'Content-Type: application/json' \
  -d '{"pageId":"17","postNrs":[1],"parentNr":1,"postType":1,"text":"回帖实测:Talkyard 论坛式回复。"}' \
  http://localhost:8139/-/reply

响应节选(实测,完整 JSON 含前端增量 storePatch):

{
 "appVersion": "0.00.79",
 "pageVersionsByPageId": {"17": 2},
 "postsByPageId": {"17": [
   {"uniqueId": 38, "nr": 2, "parentNr": 1, "postType": 1,
    "sanitizedHtml": "<p>回帖实测:Talkyard 论坛式回复。</p>\n"}
 ]}
}

全文搜索(走 Elasticsearch,标题命中高亮):

curl -s -b $jar -H "X-XSRF-TOKEN: $token" -H 'Content-Type: application/json' \
  -d '{"rawQuery":"部署"}' \
  http://localhost:8139/-/search

响应节选(实测):

{
 "thisIsAll": true,
 "pagesAndHits": [
  {"pageId": "16", "pageTitle": "部署取证页", "pageRole": 21, "urlPath": "/-16",
   "hits": [{"postNr": 1, "titleHighlightsHtmlSafe": ["<mark>部</mark><mark>署</mark>取证页"]}]}
 ]
}

建页、回帖、搜索三条链路全通,部署验收完成。

五、数据持久化说明

7 个命名卷承载全部状态(docker volume ls --format '{{.Name}}' | grep talkyard 实测回显):

talkyard-v1-2026-006-stack_es-data
talkyard-v1-2026-006-stack_es-logs
talkyard-v1-2026-006-stack_pg-data
talkyard-v1-2026-006-stack_priv-files
talkyard-v1-2026-006-stack_pub-files
talkyard-v1-2026-006-stack_redis-data
talkyard-v1-2026-006-stack_web-generated
卷挂载点内容实测占用
pg-datardb /var/lib/postgresqlPostgreSQL 主数据(唯一真源)73.08MB
es-datasearch /usr/share/elasticsearch/data索引数据(可由 PG 重建)156.3kB
es-logssearch /usr/share/elasticsearch/logsES deprecation 日志,自动轮转上限 5G59.9kB
redis-datacache /data会话/缓存 RDB119B
pub-filesweb(ro)/app(rw) /var/talkyard/v1/pub-files上传的公开文件0B
priv-filesapp /var/talkyard/v1/priv-files私有文件(备份等)0B
web-generatedweb /etc/nginx/generatedACME 账户密钥与生成证书8.63kB

占用数据取 docker system df -v 实测输出(部署+冒烟测试后)。备份口径:pg-data + pub-files + priv-files + .env/conf/secrets 是必须备份项;es-data/redis-data/web-generated 丢了可重建。宿主机侧备份输出目录为 /var/opt/backups/talkyard/v1(backup 服务挂载,见第十章)。

六、环境变量表

.env 与 compose 透传给容器的环境变量汇总(含义与实测取值):

变量作用于实测值/说明
COMPOSE_PROJECT_NAMEcomposetalkyard-v1-2026-006-stack,卷/网络前缀
DOCKER_REG_ORGcomposedebiki,镜像组织
RELEASE_BRANCH升级脚本tyse-v1-regular(发布分支,upgrade-if-needed.sh 校验须含 -v1-)
VERSION_TAGcompose/升级脚本v1.2026.006-fa49d1cee,当前版本;升级脚本成功后 sed 回写
PINNED_VERSION_TAGcompose空;非空时优先于 VERSION_TAG 决定镜像 tag
FE/BE/EG__SUBNET、_IPcompose5 网段与固定 IP(见 4.2 节 .env)
TALKYARD_SECUREapp→Play conffalse,透传后覆盖 talkyard.secure
TALKYARD_HOSTNAMEapp→Play conflocalhost,覆盖 talkyard.hostname
BECOME_OWNER_EMAIL_ADDRESSapp→Play confadmin@talkyard.io,覆盖站长邮箱
PLAY_HEAP_MEMORY_MBapp512(override 注入,4.10 节实测生效)
ES_JAVA_OPTSsearch-Xms512m -Xmx512m
bootstrap.memory_locksearch'true',配合 ulimits memlock
POSTGRES_PASSWORD_FILErdb/run/secrets/postgres_password
http_proxy/https_proxyapphttp://egressp:4750,出网强制走 SSRF 代理
TY_NGX_ERROR_LOG_LEVELwebinfo(可 notice/debug)

⚠️ 注意:compose 原文注释——TALKYARD_SECURE: '${TALKYARD_SECURE}' 这种插值写法不可用(会把变量插成空串,Play 要 bool 会拒绝启动),官方写法是只列裸变量名 - TALKYARD_SECURE 由宿主环境透传;另外改环境变量后必须 docker compose rm app 再 up -d 重建容器才生效(play-framework.conf 注释原文)。

七、配置参数说明

本文用到的配置面共四处,逐项详解见本专栏配置篇,此处给部署视角的最小说明:

  1. conf/app/play-framework.conf(101 行):必填四件套 becomeOwnerEmailAddress / hostname / secure / play.http.secret.key(≥80 随机字符);talkyard.redis.host="cache"、talkyard.postgresql.host="rdb" 是服务名寻址,勿改;文件尾部 ${?ENV_VAR} 段把环境变量映射进 Play 配置(环境变量优先于文件内值);smtp/google/akismet 留空即关闭对应功能。
  2. .env:版本、网段、透传变量(见 4.2 节)。
  3. docker-compose.override.yml:端口映射与内存档位(见 4.5 节)。
  4. conf/web/sites-enabled/talkyard-servers.conf:HTTP/HTTPS server 与 ACME 自动证书(见 4.7 节)。

八、配置文件挂载

宿主路径容器内路径读写挂载到
./conf/app/play-framework.conf/opt/talkyard/app/conf/app-prod-override.confroapp
./conf/web/sites-enabled/etc/nginx/sites-enabledroweb
./secrets/postgres_password.txt/run/secrets/postgres_passwordsecretapp / rdb / backup
./(整个部署目录)/opt/talkyard-v1robackup
宿主 /var/opt/backups/talkyard/v1同路径rwbackup

改 play-framework.conf 或 .env 后需重建 app:docker compose rm -s -f app && docker compose up -d app;改 Nginx 站点配置后 docker compose exec web nginx -s reload 或重启 web。

九、等价 docker run

Talkyard 官方自托管通道只支持 Docker Compose:5 张网络(3 张 internal)、file secrets、depends_on 顺序、backup 的 profiles 均无法用单条 docker run 等价表达,本文不提供整体 run 命令(不提供即正确做法,硬拼会破坏 SSRF 隔离设计)。仅给出「理解 compose ↔ run 对应关系」的示意(未实测,勿直接投产):compose 中

image: debiki/talkyard-app:v1.2026.006-fa49d1cee
environment: [ TALKYARD_SECURE, http_proxy=http://egressp:4750 ]

大致对应

docker run -d -e TALKYARD_SECURE -e http_proxy=http://egressp:4750 \
  --network talkyard-v1-2026-006-stack_be_int_net \
  --mount type=bind,src=.../secrets/postgres_password.txt,dst=/run/secrets/postgres_password,readonly \
  debiki/talkyard-app:v1.2026.006-fa49d1cee

镜像拉取本身与 compose 无关,实测可用(见第二章 docker pull 回显)。

十、常用运维命令

10.1 Compose 常规

docker compose up -d          # 拉起/更新全栈
docker compose ps             # 状态与健康检查
docker compose logs -f app    # 跟踪 app 日志(local 驱动自动轮转)
docker compose restart web    # 单服务重启
docker compose down           # 停止并删除容器/网络(保留卷)
docker compose exec app sh    # 进容器

10.2 资源实测

docker stats --no-stream --format '{{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}' | grep -i talkyard
talkyard-v1-2026-006-stack-app-1	1.185GiB / 15.57GiB	7.61%
talkyard-v1-2026-006-stack-web-1	33.63MiB / 15.57GiB	0.21%
talkyard-v1-2026-006-stack-cache-1	13.64MiB / 15.57GiB	0.09%
talkyard-v1-2026-006-stack-rendr-1	3.602MiB / 15.57GiB	0.02%
talkyard-v1-2026-006-stack-rdb-1	123.3MiB / 15.57GiB	0.77%
talkyard-v1-2026-006-stack-search-1	1.185GiB / 1.6GiB	74.06%
talkyard-v1-2026-006-stack-egressp-1	7.277MiB / 15.57GiB	0.05%

解读:常驻合计约 2.5 GiB,大头是 app(Play JVM,堆 512MB+堆外)与 search(ES,容器限额 1.6G 已用 74%);rendr 仅数 MB(sleep 占位)。

10.3 官方运维脚本(scripts/ 目录)

  • ./scripts/backup.sh [标签]:起一次性 backup 容器做 pg_dump+文件打包,产物落 /var/opt/backups/talkyard/v1;
  • ./scripts/delete-old-backups.sh:清理过期备份;
  • ./scripts/upgrade-if-needed.sh:自动升级(备份→拉新版→维护页→迁移→回写 VERSION_TAG),详解见本专栏 CLI 篇;
  • ./scripts/find-admin-login-link.sh:psql 查站长邮箱与近 1 天一次性登录链接(忘了站长身份时用);
  • ./scripts/schedule-daily-backups.sh / schedule-automatic-upgrades.sh:向 crontab 注入每日 2:10 备份、3:10 清理、4:10 升级检查;
  • ./view-logs / ./view-stats:日志(去 JSON 化)与资源统计快捷命令。

十一、常见问题(FAQ)

Q1: app 起不来,日志说 secret key 有问题? A: play.http.secret.key 未改成 ≥80 位随机值,服务器按设计拒绝启动(见 4.6 节 ⚠️ 与生成命令)。

Q2: 直接 docker run 起 app 后进程立刻退出? A: compose 里 stdin_open: true # otherwise Play Framework exits——run 形态需加 -i 并保持 stdin 打开;但整体仍不建议绕开 compose(见第九章)。

Q3: 创建站长报 Username is reserved: 'admin' [EdE5PKW01]? A: admin 是保留名,换 tyadmin 之类(4.9 节实录二)。同理 The username is too short [DwE805T4] 要求 ≥3 字符。

Q4: 密码明明够长还是 400 Password too short, should be 10 chars [TyEPWMIN_]? A: 服务端下限 10 位;8 位通用测试口令不可用,本文用 q1w2e3r4ty(4.9 节实录四)。

Q5: 登录返回 404? A: 路径应为 /-/login-password;/-/login 是 404 HTML Not Found。

Q6: 建页报 DwE4FKW8、pageRole 6/7 被拒? A: v1 版本 Blog(6)/Forum(7) 分类未开放,通用页用 pageRole=21;合法枚举 1-5,9,10,11,12,14,15,16,18,20,21,22(4.11 节实测)。

Q7: 回帖报 Bad JSON: ... 'postNrs' is undefined ... [TyEBADJSN]? A: /-/reply 必须带 postNrs 数组与被覆盖字段名 text(不是 postText),且 parentNr 不能为 0(违反 DB CHECK dw2_posts_parent__c_not_title)。正确请求体见 4.11 节。

Q8: /-/save-category 怎么调都报错? A: v1 论坛分类功能未就绪:补 permissions 后报 'sectionPageId' is undefined,传 0 报 TyESECTPG0SECT——根因是 DB CHECK(page_role<>13) 禁止 SiteSection 页(约束 pages_c_not_todo),属产品未完成面,勿在生产依赖。

Q9: Nginx 启动报 [emerg] socket() [::]:80 failed (97: Address family not supported by protocol)? A: 宿主禁用 IPv6 时注释掉 listen [::]:80 ... 行(官方配置 [ipv6_probl] 注释原文)。

Q10: 自己加 server 块后 Nginx 报 "duplicate listen options"? A: backlog=8192 只能出现在一个 listen 指令处([BACKLGSZ] 注释),新增 server 不要带它。

十二、参考链接

  1. 自托管部署仓库(ty-prod-one-v1 分支):https://github.com/debiki/talkyard-prod-one/tree/ty-prod-one-v1
  2. 源码与镜像 Dockerfile:https://github.com/debiki/talkyard
  3. 官方文档站:https://docs.talkyard.io/
  4. Docker Compose file 规范:https://github.com/compose-spec/compose-spec/blob/main/spec.md
  5. Docker secrets 用法:https://docs.docker.com/compose/how-tos/use-secrets/
  6. lua-resty-acme(证书自动签发):https://github.com/zmap/lua-resty-acme
  7. smokescreen(SSRF 出网代理):https://github.com/stripe/smokescreen

(本文全部命令与回显实测于 2026-11-02,Ubuntu 24.04.5 LTS + Docker 29.9.2 + Compose v5.6.1。)

Talkyard v1.2026.006 Docker Compose 部署指南 - Quzuo