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 实测。
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 的编排拓扑是联邦组件里最简的一档:一个应用服务 + 一个数据库(可选)。
| 服务 | 制品 | 角色 | 持久化 |
|---|---|---|---|
hatsu | ghcr.io/importantimport/hatsu:latest | 单进程承载 WebFinger/NodeInfo/host-meta、ActivityPub 收发件箱、Feed 轮询器、Admin API | 无状态,状态全在数据库 |
db(可选) | postgres:16 | PostgreSQL 数据服务 | 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 配置参数详解》):
- 环境变量必须一次给全,Hatsu 没有交互式初始化流程;其中
HATSU_PRIMARY_ACCOUNT是实例签名主体账号(signed_fetch_actor),且不可删除(实测删除返回 405,见第七章)。 HATSU_LISTEN_HOST缺省是127.0.0.1,编排形态必须显式写0.0.0.0,否则宿主端口映射打不通。HATSU_ACCESS_TOKEN不设置则 Admin API 整体不可用(实测 401,见第十章 10.2 节旁证);官方建议用 uuid v4,生产环境请更换示例值。HATSU_DATABASE_URL: sqlite://hatsu.sqlite3是相对路径,相对于应用工作目录/app,因此必须与 bind 挂载目标/app/hatsu.sqlite3对齐;缺省值是sqlite::memory:(内存库),生产绝不能留空该项,后果见第十章 10.2 节。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<p>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.</p>\n<p>The post <a href="https://github.blog/open-source/gaming/game-off-2026-the-theme-is-shift/">Game Off 2026: The theme is SHIFT</a> appeared first on <a href="https://github.blog">The GitHub Blog</a>.</p>\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<p>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.</p>\n<p>The post <a href="https://github.blog/open-source/gaming/game-off-2026-the-theme-is-shift/">Game Off 2026: The theme is SHIFT</a> appeared first on <a href="https://github.blog">The GitHub Blog</a>.</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 节):
HATSU_DOMAIN改为你的桥接实例域名,并在该域名上配置 HTTPS 反代(Hatsu 只监听 HTTP,443 由 Nginx/Caddy/Traefik 终结),/.well-known/*与接受application/activity+json的页面请求按 A1 篇四种重定向路线之一指向本实例;- 站点 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 与数据库检查工具面。