Hatsu 0.3.5 Docker Compose 部署指南

Hatsu 0.3.5 Docker Compose 部署全纪实:SQLite 与 PostgreSQL 双后端从零起服务、初始化 CLI 与 Admin API 建号、联邦端点逐项验证,含官方制品 healthcheck 变量不展开导致恒 unhealthy、缺省 sqlite::memory: 连接池空库 500、主账号 Feed 不可达崩溃循环三例排障实录,全部回显 2026-11-02 实测。

HatsuHatsuActivityPubFediverseDockerDocker ComposePostgreSQL2026/11/02

Hatsu 0.3.5 Docker Compose 部署指南

目标:在一台 Linux 主机上,用官方发布的 ghcr.io/importantimport/hatsu 制品,以 Docker Compose 把 Hatsu 0.3.5 从零部署到「联邦端点可访问、可建号、文章已广播」的可用状态,并给出 SQLite 与 PostgreSQL 两种数据后端的完整编排。本文全部命令与回显均为 2026-11-02 实测记录,第十章沉淀了三例真实故障的定位过程(官方制品 healthcheck 缺陷、缺省内存库连接池空表 500、主账号 Feed 不可达崩溃循环)。产品定位与选型背景见 《Hatsu 0.3.5 介绍与选型指南》,参数逐项语义见 《Hatsu 0.3.5 配置参数详解》,日常命令与工具见 《Hatsu 0.3.5 常用 CLI 命令与工具详解》。

一、环境信息

本章为实测环境口径,供读者复核。

  • 组件与版本:Hatsu 0.3.5(代号 "Strobe Nights",启动日志自报版本,见第五章回显);官方制品 ghcr.io/importantimport/hatsu:latest(稳定通道,制品内 Entrypoint=["/app/hatsu"]、WorkingDir=/app、ExposedPorts={"3939/tcp":{}},默认注入 HATSU_LISTEN_PORT=3939)。
  • 运行时:Docker Engine 与 Compose v2 子命令形态,实测宿主版本回显:
Docker version 29.9.2, build a82e528
Docker Compose version v5.6.1
  • 技术栈事实:Hatsu 是单个 Rust 可执行文件(axum + SeaORM/sqlx),非 JVM 进程,无堆内存上限调优要求,资源占用实测见第九章。
  • 数据后端:SQLite(bind 挂载单文件)与 PostgreSQL 16 两种形态各起了一套,分别占用宿主端口 8144(SQLite 栈)与 8145(PostgreSQL 栈);另有一套用于故障复现的 8146 端口栈(第十章实录)。
  • 网络规划:三套编排各挂独立自定义网络,子网依次为 10.89.68.0/24、10.89.69.0/24、10.89.70.0/24,应用服务静态 IP 分别为 10.89.68.10、10.89.70.10(PostgreSQL 栈未指定静态 IP)。
  • 域名口径:实测实例 HATSU_DOMAIN=hatsu.local(示例域名,未做真实 DNS;生产必须换成你的桥接域名,见第十一章)。

二、部署架构总览

Hatsu 的编排拓扑是联邦组件里最简的一档:一个应用服务 + 一个数据库(可选)。

服务制品角色持久化
hatsughcr.io/importantimport/hatsu:latest单进程承载 WebFinger/NodeInfo/host-meta、ActivityPub 收发件箱、Feed 轮询器、Admin API无状态,状态全在数据库
db(可选)postgres:16PostgreSQL 数据服务bind 挂载 ./data/pgdata

数据后端二选一(HATSU_DATABASE_URL 只接受 sqlite: 或 postgres: URL):

  • SQLite 路线:不引入第二个服务,把宿主一个 .sqlite3 文件 bind 挂载进应用 /app/hatsu.sqlite3,适合单站桥接、树莓派级硬件;
  • PostgreSQL 路线:compose 里加一个 db 服务,应用通过编排内部网络访问 db:5432,数据库口令只在内部网络流转、不对外暴露端口,适合多账号/长期运行。

三、准备部署目录

以 SQLite 路线为例,准备目录与数据库文件。注意:官方文档的 Docker 开发流程明确要求先创建数据库文件再启动(原文 # create database / touch hatsu.sqlite3)——bind 挂载一个宿主不存在的路径时,编排层会把它创建成目录,应用随后因无法打开数据库而失败,这是单文件 bind 挂载的经典坑,务必先 touch:

mkdir -p /opt/hatsu-stack/data && cd /opt/hatsu-stack
touch data/hatsu.sqlite3

实测部署完成后该文件形态(宿主视角):

-rw-r--r-- 1 root root 434176 Nov  2 12:26 hatsu.sqlite3

四、Compose 编排文件

实测部署所用的 docker-compose.yml(SQLite 路线,healthcheck 段为第十章 10.1 节缺陷的修复写法,可先照抄):

name: hatsu-v0.3.5-stack

services:
  hatsu:
    image: ghcr.io/importantimport/hatsu:latest
    container_name: hatsu-035-server
    restart: unless-stopped
    hostname: hatsu.local
    ports:
      - "8144:3939"
    environment:
      HATSU_LOG: info
      HATSU_DATABASE_URL: sqlite://hatsu.sqlite3
      HATSU_DOMAIN: hatsu.local
      HATSU_LISTEN_HOST: 0.0.0.0
      HATSU_LISTEN_PORT: "3939"
      HATSU_PRIMARY_ACCOUNT: github.blog
      HATSU_ACCESS_TOKEN: 123e4567-e89b-12d3-a456-426614174000
      HATSU_NODE_NAME: My Hatsu
      HATSU_NODE_DESCRIPTION: S8 test instance
    volumes:
      - ./data/hatsu.sqlite3:/app/hatsu.sqlite3
    healthcheck:
      test: ["CMD-SHELL", "curl --fail http://localhost:$${HATSU_LISTEN_PORT:-3939}/api/v0/generate_204"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 15s
    networks:
      hatsu-net:
        ipv4_address: 10.89.68.10

networks:
  hatsu-net:
    driver: bridge
    ipam:
      config:
        - subnet: 10.89.68.0/24
          gateway: 10.89.68.1

要点说明(参数逐项语义见 《Hatsu 0.3.5 配置参数详解》):

  1. 环境变量必须一次给全,Hatsu 没有交互式初始化流程;其中 HATSU_PRIMARY_ACCOUNT 是实例签名主体账号(signed_fetch_actor),且不可删除(实测删除返回 405,见第七章)。
  2. HATSU_LISTEN_HOST 缺省是 127.0.0.1,编排形态必须显式写 0.0.0.0,否则宿主端口映射打不通。
  3. HATSU_ACCESS_TOKEN 不设置则 Admin API 整体不可用(实测 401,见第十章 10.2 节旁证);官方建议用 uuid v4,生产环境请更换示例值。
  4. HATSU_DATABASE_URL: sqlite://hatsu.sqlite3 是相对路径,相对于应用工作目录 /app,因此必须与 bind 挂载目标 /app/hatsu.sqlite3 对齐;缺省值是 sqlite::memory:(内存库),生产绝不能留空该项,后果见第十章 10.2 节。
  5. healthcheck 段:官方制品自带的探针有缺陷(exec 形式不展开变量),这里用 CMD-SHELL 覆盖,$$ 是 Compose 的变量转义,让表达式留给运行时 shell 展开——完整定位过程见第十章 10.1 节。

五、首次启动与日志核对

docker compose up -d

实测首次拉取与启动回显(节选):

 Image ghcr.io/importantimport/hatsu:latest Pulled 
 Network hatsu-v035-stack_hatsu-net Creating 
 Network hatsu-v035-stack_hatsu-net Created 
 Container hatsu-035-server Creating 
 Container hatsu-035-server Created 
 Container hatsu-035-server Starting 
 Container hatsu-035-server Started 

启动日志的完整序列(docker compose logs 节选,已去除 ANSI 着色与逐条 SQL 噪音):

  2026-11-02T03:14:12.200537Z  INFO hatsu::commands::run: Hatsu v0.3.5 "Strobe Nights"
  2026-11-02T03:14:12.200547Z  INFO hatsu::commands::run: loading environment variables
  2026-11-02T03:14:12.201110Z  INFO hatsu::commands::run: connecting database: sqlite://hatsu.sqlite3
  2026-11-02T03:14:12.201222Z  INFO hatsu::commands::run: running database migration
  2026-11-02T03:14:12.205714Z  INFO sqlx::query: summary: "CREATE TABLE IF NOT …", db.statement: "\n\nCREATE TABLE IF NOT EXISTS \"seaql_migrations\" ( \"version\" varchar NOT NULL PRIMARY KEY, \"applied_at\" integer NOT NULL )\n", rows_affected: 0, rows_returned: 0, elapsed: 4.43233ms, elapsed_secs: 0.00443233

之后依次出现 checking primary account: github.blog、setup federation config、starting backend、creating app,即进入服务态。迁移是启动时自动执行的(SeaORM Migrator,11 个迁移版本),无需任何手工初始化命令。

主账号校验环节会真实拉取一次目标站点的 Feed:github.blog 有有效 RSS,校验通过、账号自动入库;如果域名拉不到 Feed,进程会直接报错退出并进入重启循环——该故障实录见第十章 10.3 节。

启动后核对服务态:

docker compose ps

实测回显(healthcheck 修复生效后):

NAME               IMAGE                                  COMMAND        SERVICE   CREATED       STATUS                   PORTS
hatsu-035-server   ghcr.io/importantimport/hatsu:latest   "/app/hatsu"   hatsu     8 minutes ago Up 8 minutes (healthy)   0.0.0.0:8144->3939/tcp, [::]:8144->3939/tcp

六、服务可用验证(联邦端点逐项实测)

以下全部为宿主端口 8144 上的实测回显。

6.1 健康探针

curl -si http://127.0.0.1:8144/api/v0/generate_204 | head -3
HTTP/1.1 204 No Content
vary: origin, access-control-request-method, access-control-request-headers
access-control-allow-origin: *

6.2 WebFinger 账号发现

curl -s 'http://127.0.0.1:8144/.well-known/webfinger?resource=acct:github.blog@hatsu.local'
{"subject":"acct:github.blog@hatsu.local","links":[{"rel":"http://webfinger.net/rel/profile-page","type":"text/html","href":"https://hatsu.local/users/github.blog","template":null},{"rel":"self","type":"application/activity+json","href":"https://hatsu.local/users/github.blog","template":null}]}

6.3 NodeInfo

curl -s http://127.0.0.1:8144/.well-known/nodeinfo
{"links":[{"rel":"http://nodeinfo.diaspora.software/ns/schema/2.1","href":"https://hatsu.local/nodeinfo/2.1.json"},{"rel":"http://nodeinfo.diaspora.software/ns/schema/2.0","href":"https://hatsu.local/nodeinfo/2.0.json"}]}
curl -s http://127.0.0.1:8144/nodeinfo/2.1.json

首次启动(仅主账号)时实测:

{"version":"2.1","software":{"name":"hatsu","version":"0.3.5","repository":"https://github.com/importantimport/hatsu","homepage":"https://hatsu.cli.rs"},"protocols":["activitypub"],"services":{"inbound":[],"outbound":[]},"openRegistrations":false,"usage":{"users":{"total":1},"localPosts":10},"metadata":{"nodeName":"My Hatsu","nodeDescription":"S8 test instance"}}

localPosts: 10 说明 Feed 轮询器在启动后已把 github.blog 最新 10 篇文章入库并广播。

6.4 Web Host Metadata

curl -si http://127.0.0.1:8144/.well-known/host-meta | head -4
HTTP/1.1 307 Temporary Redirect
location: /.well-known/host-meta.xml
vary: origin, access-control-request-method, access-control-request-headers
access-control-allow-origin: *

JSON 形态:

{"links":[{"rel":"lrdd","type":"application/json","template":"https://hatsu.local/.well-known/webfinger?resource={uri}"}]}

6.5 Actor 与发件箱

curl -s -H 'accept: application/activity+json' http://127.0.0.1:8144/users/github.blog
{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1","https://purl.archive.org/socialweb/webfinger",{"xrd":"http://docs.oasis-open.org/ns/xri/xrd-1.0#","aliases":{"@id":"xrd:Alias","@type":"@id","@container":"@list"}}],"id":"https://hatsu.local/users/github.blog","type":"Service","name":"The GitHub Blog","preferredUsername":"github.blog","summary":"Updates, ideas, and inspiration from GitHub to help developers build and design software.","icon":{"type":"Image","url":"https://github.blog/wp-content/uploads/2019/01/cropped-github-favicon-512.png?fit=32%2C32"},"attachment":[{"type":"PropertyValue","name":"Website","value":"<a href=\"https://github.blog/\" rel=\"nofollow noreferrer noopener me\" target=\"_blank\" translate=\"no\">https://github.blog/</a>"},{"type":"PropertyValue","name":"RSS Feed","value":"<a href=\"https://github.blog/feed/\" rel=\"nofollow noreferrer noopener\" target=\"_blank\" translate=\"no\">https://github.blog/feed/</a>"}],"inbox":"htt

(回显较长,此处照抄至截断处。注意 actor 的 type 是 Service 而非 Person,头像、简介、RSS 链接均从站点 Feed 自动提取。)

发件箱集合与首页:

curl -s http://127.0.0.1:8144/users/github.blog/outbox
{"@context":"https://www.w3.org/ns/activitystreams","type":"OrderedCollection","id":"https://hatsu.local/users/github.blog/outbox","first":"https://hatsu.local/users/github.blog/outbox?page=1","last":"https://hatsu.local/users/github.blog/outbox?page=1","totalItems":10}
curl -s 'http://127.0.0.1:8144/users/github.blog/outbox?page=1'
{"@context":"https://www.w3.org/ns/activitystreams","type":"OrderedCollectionPage","id":"https://hatsu.local/users/github.blog/outbox?page=1","partOf":"https://hatsu.local/users/github.blog/outbox","orderedItems":[{"id":"https://hatsu.local/activities/01a19a49-f968-76e2-905c-3f0b35d4a895","type":"Create","actor":"https://hatsu.local/users/github.blog","published":"2026-11-02T03:25:01Z","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://hatsu.local/users/github.blog/followers"],"object":{"id":"https://hatsu.local/posts/https://github.blog/open-source/gaming/game-off-2026-the-theme-is-shift/","type":"Note","published":"2026-11-02T03:25:01Z","attributedTo":"https://hatsu.local/users/github.blog","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://hatsu.local/users/github.blog/followers"],"content":"<p>Game Off 2026: The theme is SHIFT</p>\n&lt;p&gt;GitHub’s annual month-long game jam is back for its 14th year! This November, make a game inspired by the theme SHIFT. Change gears, swap perspectives, bend reality, or just press a certain key.&lt;/p&gt;\n&lt;p&gt;The post &lt;a href=&quot;https://github.blog/open-source/gaming/game-off-2026-the-theme-is-shift/&quot;&gt;Game Off 2026: The theme is SHIFT&lt;/a&gt; appeared first on &lt;a href=&quot;https://github.blog&quot;&gt;The GitHub Blog&lt;/a&gt;.&lt;/p&gt;\n<p><a href=\"https://github.blog/open-source/gaming/game-off-2026-the-theme-is-shift/\">https://github.blog/open-source/gaming/game-off

对象 ID 采用 A1 篇 3.3 节描述的可预测映射:帖子原始 URL 直接编码进 https://hatsu.local/posts/<原URL>。

6.6 文章对象与 notice 兼容路径

curl -s http://127.0.0.1:8144/posts/https://github.blog/open-source/gaming/game-off-2026-the-theme-is-shift/
{"@context":"https://www.w3.org/ns/activitystreams","id":"https://hatsu.local/posts/https://github.blog/open-source/gaming/game-off-2026-the-theme-is-shift/","type":"Note","published":"2026-11-02T03:25:01Z","attributedTo":"https://hatsu.local/users/github.blog","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://hatsu.local/users/github.blog/followers"],"content":"<p>Game Off 2026: The theme is SHIFT</p>\n&lt;p&gt;GitHub’s annual month-long game jam is back for its 14th year! This November, make a game inspired by the theme SHIFT. Change gears, swap perspectives, bend reality, or just press a certain key.&lt;/p&gt;\n&lt;p&gt;The post &lt;a href=&quot;https://github.blog/open-source/gaming/game-off-2026-the-theme-is-shift/&quot;&gt;Game Off 2026: The theme is SHIFT&lt;/a&gt; appeared first on &lt;a href=&quot;https://github.blog&quot;&gt;The GitHub Blog&lt;/a&gt;.&lt;/p

仿 Pleroma 的 /notice/<base64url> 路径(供 oom-comments 前端反查回复树,见 A1 篇 4.2 节)返回 308 重定向到规范帖子 URL:

HTTP/1.1 308 Permanent Redirect
location: /posts/https://github.blog/open-source/gaming/game-off-2026-the-theme-is-shift/
vary: origin, access-control-request-method, access-control-request-headers
access-control-allow-origin: *
access-control-expose-headers: *
content-length: 0
date: Mon, 02 Nov 2026 03:26:26 GMT

七、多账号初始化(Admin API)

一个实例可桥接多个站点域名,建号走 Admin API(需 HATSU_ACCESS_TOKEN)。curl 形态与官方 Justfile 内部实现一致:

curl -s -w '\nHTTP %{http_code}\n' -X POST 'http://127.0.0.1:8144/api/v0/admin/create-account?name=blog.codingnow.com&token=123e4567-e89b-12d3-a456-426614174000'

实测回显:

{"name":"云风的 BLOG","message":"The account was successfully created: 云风的 BLOG"}
HTTP 201

新账号的 actor 立即联邦可达(简介同样自动从 Feed 抓取):

{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1","https://purl.archive.org/socialweb/webfinger",{"xrd":"http://docs.oasis-open.org/ns/xri/xrd-1.0#","aliases":{"@id":"xrd:Alias","@type":"@id","@container":"@list"}}],"id":"https://hatsu.local/users/blog.codingnow.com","type":"Service","name":"云风的 BLOG","preferredUsername":"blog.codingnow.com","summary":"思绪来得快去得也快,偶尔会在这里停留"

重复建号与删除主账号的实测行为:

{"error":"The account already exists: 云风的 BLOG","error_id":"01a19a81-fe14-7532-bbb1-56c3eb75832e"}
HTTP 400
{"name":"github.blog","message":"Remove account API not yet implemented: github.blog"}
HTTP 405
{"name":"blog.codingnow.com","message":"Remove account API not yet implemented: blog.codingnow.com"}
HTTP 405

即:0.3.5 的删号 API 尚未实现(主账号与普通账号一律 405),建错账号只能手工清理数据库;token 错误则直接 401 空响应。实例封禁(block-url/unblock-url)实测可用:

{"url":"https://mastodon.social/","message":"The url was successfully blocked: https://mastodon.social/"}
HTTP 200
{"url":"https://mastodon.social/","message":"The url was successfully unblocked: https://mastodon.social/"}
HTTP 200

建号后 NodeInfo 计数随之更新(users.total 从 1 变 2):

"usage":{"users":{"total":2},"localPosts":25}

Admin API 的完整命令族见 《Hatsu 0.3.5 常用 CLI 命令与工具详解》。

八、PostgreSQL 后端变体

数据量大或多账号长期运行时,把 HATSU_DATABASE_URL 切到 postgres:// 即可,编排加一个 db 服务。实测 docker-compose.yml:

name: hatsu-v0.3.5-pg-stack

services:
  hatsu:
    image: ghcr.io/importantimport/hatsu:latest
    container_name: hatsu-035-pg
    restart: unless-stopped
    depends_on:
      - db
    ports:
      - "8145:3939"
    environment:
      HATSU_LOG: info
      HATSU_DATABASE_URL: postgres://hatsu:q1w2e3r4@db:5432/hatsu
      HATSU_DOMAIN: hatsu.local
      HATSU_LISTEN_HOST: 0.0.0.0
      HATSU_LISTEN_PORT: "3939"
      HATSU_PRIMARY_ACCOUNT: github.blog
      HATSU_ACCESS_TOKEN: 123e4567-e89b-12d3-a456-426614174000
    healthcheck:
      test: ["CMD-SHELL", "curl --fail http://localhost:$${HATSU_LISTEN_PORT:-3939}/api/v0/generate_204"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 15s
    networks:
      - hatsu-pg-net

  db:
    image: postgres:16
    container_name: hatsu-035-db
    restart: unless-stopped
    environment:
      POSTGRES_USER: hatsu
      POSTGRES_PASSWORD: q1w2e3r4
      POSTGRES_DB: hatsu
    volumes:
      - ./data/pgdata:/var/lib/postgresql/data
    networks:
      - hatsu-pg-net

networks:
  hatsu-pg-net:
    driver: bridge
    ipam:
      config:
        - subnet: 10.89.69.0/24
          gateway: 10.89.69.1

docker compose ps 实测:

NAME           IMAGE                                  COMMAND                  SERVICE   CREATED          STATUS                             PORTS
hatsu-035-db   postgres:16                            "docker-entrypoint.s…"   db        18 seconds ago   Up 18 seconds                      5432/tcp
hatsu-035-pg   ghcr.io/importantimport/hatsu:latest   "/app/hatsu"             hatsu     18 seconds ago   Up 18 seconds (health: starting)   0.0.0.0:8145->3939/tcp, [::]:8145->3939/tcp

数据库口令只在编排内部网络流转(db 服务未映射宿主端口),应用经 db:5432 访问。迁移同样自动完成,psql 核对建表结果:

psql -U hatsu -d hatsu -c '\dt'
             List of relations
 Schema |       Name        | Type  | Owner 
--------+-------------------+-------+-------
 public | activity          | table | hatsu
 public | blocked_url       | table | hatsu
 public | post              | table | hatsu
 public | received_announce | table | hatsu
 public | received_follow   | table | hatsu
 public | received_like     | table | hatsu
 public | seaql_migrations  | table | hatsu
 public | user              | table | hatsu
 public | user_feed_item    | table | hatsu
(9 rows)
psql -U hatsu -d hatsu -c 'select name, preferred_username, local from "user";'
      name       | preferred_username | local 
-----------------+--------------------+-------
 The GitHub Blog | github.blog        | t
(1 row)
psql -U hatsu -d hatsu -c 'select version from seaql_migrations order by version;'
                  version                   
--------------------------------------------
 m20240131_000001_user
 m20240131_000002_user_feed_item
 m20240131_000003_post
 m20240131_000004_activity
 m20240131_000005_received_follow
 m20240501_000001_received_like
 m20240501_000002_received_announce
 m20240515_000001_user_feed_hatsu_extension
 m20240515_000002_user_feed
 m20240926_000001_blocked_url
 m20241028_000001_user_language
(11 rows)

9 张表、11 个迁移版本,与 SQLite 路线完全同构(同一套 SeaORM 迁移);received_follow/received_like/received_announce 三张表就是 backfeed 的互动存储面。

九、资源占用实测

CONTAINER ID   NAME               CPU %     MEM USAGE / LIMIT     MEM %     NET I/O           BLOCK I/O    PIDS
1d9279451512   hatsu-035-server   0.00%     7.988MiB / 15.57GiB   0.05%     1.15MB / 95.3kB   0B / 950kB   9

常驻内存 约 8 MiB、9 个线程、拉取 10 篇文章后 SQLite 库文件 434176 字节(约 424 KiB)——印证 A1 篇「单二进制 + 单库、树莓派级硬件即可」的定位;Rust 无 JVM 堆参数需要调,restart: unless-stopped 即可长期驻留。

十、排障实录

本章三例均为本次实测中真实发生并定位的故障,按「现象→定位→根因→修复」记录。

10.1 服务明明正常,状态却恒为 unhealthy

现象:SQLite 栈与 PostgreSQL 栈的应用服务创建后长期显示 (unhealthy),但宿主端口 curl 一切正常(204/200 皆有回显,见第六、八章)。

定位:先看探针配置与失败记录:

docker inspect hatsu-035-server --format '{{json .Config.Healthcheck}}'
{"Test":["CMD","curl","--fail","http://localhost:${HATSU_LISTEN_PORT}/api/v0/generate_204"]}
docker inspect hatsu-035-server --format '{{json .State.Health}}'
{"Status":"unhealthy","FailingStreak":335,"Log":[{"Start":"2026-11-02T14:09:59.35692015+08:00","End":"2026-11-02T14:09:59.382223399+08:00","ExitCode":3,"Output":"curl: (3) URL rejected: Port number was not a decimal number between 0 and 65535\n"}, ...]}

失败输出是决定性证据:curl: (3) URL rejected: Port number was not a decimal number between 0 and 65535——探针 URL 里的 ${HATSU_LISTEN_PORT} 被原样传给了 curl。原因:官方制品的 HEALTHCHECK 用的是 exec 形式(CMD 数组),该形式不经过 shell,不会做环境变量展开;而探针又写死了变量占位。于是无论应用多健康,探针恒失败(FailingStreak 335 即连续失败 335 次),应用本身其实一直正常。

修复:在 compose 里用 CMD-SHELL 形式覆盖探针(shell 形式会展开变量,$$ 转义让 Compose 解析阶段不动它、留给运行时 shell),并补上节奏参数:

    healthcheck:
      test: ["CMD-SHELL", "curl --fail http://localhost:$${HATSU_LISTEN_PORT:-3939}/api/v0/generate_204"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 15s

docker compose up -d 重建回显:

 Container hatsu-035-server Recreate 
 Container hatsu-035-server Recreated 
 Container hatsu-035-server Starting 
 Container hatsu-035-server Started 

核对覆盖已生效:

docker inspect hatsu-035-server --format '{{json .Config.Healthcheck}}'
{"Test":["CMD-SHELL","curl --fail http://localhost:${HATSU_LISTEN_PORT:-3939}/api/v0/generate_204"],"Interval":30000000000,"Timeout":5000000000,"StartPeriod":15000000000,"Retries":3}

等待一个探针周期后两栈同时转绿:

hatsu-035-pg	Up 2 minutes (healthy)
hatsu-035-server	Up 2 minutes (healthy)

结论:这是官方制品 HEALTHCHECK 的实现缺陷(exec 形式 + 变量占位互斥),不是部署者配置错误;凡使用该制品做编排,都建议按上文覆盖探针。给制品上游提修复是正解,覆盖探针是当下可用的绕行。

10.2 缺省内存库:接口间歇 500「no such table: user」

现象:故障复现栈(8146 端口)应用进程正常存活、首页 200,但 NodeInfo 等接口返回:

{"error":"Query Error: error returned from database: (code: 1) no such table: user","error_id":"01a19ae5-2ca0-7252-8de1-f20a0d270f74"} [HTTP 500]

同一端点在刚启动后的某些时刻又返回过 200——间歇性是本案最大迷惑点。

定位:第一步看启动日志第三行,数据库连的不是文件:

  2026-11-02T06:18:44.398962Z  INFO hatsu::commands::run: Hatsu v0.3.5 "Strobe Nights"
  2026-11-02T06:18:44.398982Z  INFO hatsu::commands::run: loading environment variables
  2026-11-02T06:18:44.399001Z  INFO hatsu::commands::run: connecting database: sqlite::memory:
  2026-11-02T06:18:44.399366Z  INFO hatsu::commands::run: running database migration
  2026-11-02T06:18:44.400337Z  INFO sea_orm_migration::migrator::exec: Applying all pending migrations
  2026-11-02T06:18:44.400340Z  INFO sea_orm_migration::migrator::exec: Applying migration 'm20240131_000001_user'

第二步核对环境:该栈的 compose 环境变量里只有 5 个 HATSU_*,没有 HATSU_DATABASE_URL:

HATSU_DOMAIN=hatsu.local
HATSU_LISTEN_HOST=0.0.0.0
HATSU_LISTEN_PORT=3939
HATSU_LOG=info
HATSU_PRIMARY_ACCOUNT=github.blog

于是吃到缺省值 sqlite::memory:。第三步把两者串起来:sqlite::memory: 是每个连接各自独立的私有内存库,而 SeaORM 走连接池——启动时迁移与主账号写入发生在池里第一个连接上(所以刚启动时打到该连接的请求返回 200,users.total 还能显示 1),后续请求被派到其他新建连接时,那个库里连表都没有,直接 500。间歇性、以及「日志明明显示迁移全部成功」的矛盾,都源于此。

根因:官方文档对缺省值自述「sqlite::memory: is used by default to ensure that hatsu does not report errors as a result」——缺省内存库只为「让 Hatsu 不因缺配置而直接报错」,与连接池模型根本不兼容,不具备任何生产可用性,官方推荐值实为 sqlite://hatsu.sqlite3。

修复:显式设置 HATSU_DATABASE_URL 并 bind 挂载持久文件(第四章 compose 即是)。旁证:该栈同时未设 HATSU_ACCESS_TOKEN,Admin API 实测 401 空响应,与「不设置则 Admin API 不可用」的文档口径一致:

HTTP 401

10.3 主账号 Feed 不可达:启动即崩、重启循环

现象:docker compose ps 显示服务状态 Restarting (1) Less than a second ago,反复重启。

定位:日志尾部直接给出致命错误:

  2026-11-02T03:14:27.539879Z  INFO hatsu::commands::run: checking primary account: example.com
  ...
Error: AppError { error: "Unable to find Feed Url named example.com", error_id: 01a19a40-6c59-7a72-8824-fa925477e5a1, error_details: None, status: 404, context: SpanTrace [] }

根因:HATSU_PRIMARY_ACCOUNT 是实例的签名主体账号,启动流程会在数据库找不到该账号时现场创建——而创建依赖真实拉取该域名的站点 Feed 以发现 Feed URL。example.com 没有任何 Feed,发现失败即 panic 退出,restart: unless-stopped 再把进程拉起,循环往复。

修复:主账号必须填真实可达且带有效 RSS/Atom/JSON Feed 的域名。换成 github.blog 后同一编排一次启动成功(第五、六章即其结果)。这条约束也是选型时的运维要点:主账号域名挂了,实例就起不来。

十一、生产化注意事项

实测环境用 hatsu.local 示例域名在单机闭环验证了全部端点;上生产还需完成两件事(机制详见 A1 篇 3.4 节):

  1. HATSU_DOMAIN 改为你的桥接实例域名,并在该域名上配置 HTTPS 反代(Hatsu 只监听 HTTP,443 由 Nginx/Caddy/Traefik 终结),/.well-known/* 与接受 application/activity+json 的页面请求按 A1 篇四种重定向路线之一指向本实例;
  2. 站点 Feed 可被公网访问:主账号与后续建号都要拉 Feed,出站请求自 0.3.5 起携带 User-Agent(A1 篇 4.1 节),若站点有反爬规则请为 Hatsu 域名放行。

十二、小结

  • 一条 compose、一个二进制、一个数据库文件即可把 Hatsu 0.3.5 跑成联邦可达的桥接实例:迁移、主账号创建、Feed 广播全自动,无手工初始化命令。
  • 三例故障各有归属:制品缺陷(healthcheck 变量不展开→覆盖探针)、缺省值陷阱(sqlite::memory: 与连接池冲突→显式配库)、配置约束(主账号必须有有效 Feed→换真实域名)。
  • 删号 API 在 0.3.5 尚未实现(405),建号前核对域名。

下一篇 《Hatsu 0.3.5 配置参数详解》 把第四章的每个环境变量逐一展开实测语义;《Hatsu 0.3.5 常用 CLI 命令与工具详解》 汇总 CLI、Admin API 与数据库检查工具面。

Hatsu 0.3.5 Docker Compose 部署指南 - Quzuo