hitobito 2.10 Docker Compose 部署实战

用官方 hitobito/development 工具箱以 Docker Compose 从零部署 hitobito 2.10.0 全栈,含 assets 崩溃循环定位修复与端到端验证。

hitobitohitobitoRubyRails会员管理2026/11/02

hitobito 2.10 Docker Compose 部署实战

目标:用官方 hitobito/development 工具箱以 Docker Compose 形态从零部署一套可用的 hitobito 2.10.0 演示实例(含 PostgreSQL、Redis、后台作业、邮件捕获与前端编译服务),并完整记录 assets 服务崩溃循环的定位与修复。本文全部命令与回显在 Ubuntu 22.04 服务器(Docker 29.9.2 / Compose v5.6.1,项目目录 /opt/hitobito/generic)实测通过。

一、环境信息

  • 操作系统:Ubuntu 22.04 LTS(x86_64)
  • Docker Engine:29.9.2(docker --version 实测)
  • Docker Compose:v5.6.1(docker compose version 实测)
  • 应用版本:hitobito core 2.10.0(release tag 2.10.0,提交 28d23c0)
  • 工具箱:hitobito/development main 分支(提交 596a223)
  • 演示 wagon:hitobito_generic(提交 e546fc0)
  • 组件版本:Ruby 3.3.13、Rails 8.0.5、PostgreSQL 16、Redis 8-alpine、Node v22.23.4、yarn 1.22.22、webpacker 5.4.3
  • 宿主端口分配(全部仅绑定 127.0.0.1):
服务容器宿主端口容器端口
Web 应用generic-rails-181523000
PostgreSQLgeneric-postgres-181545432
Redisgeneric-redis-181556379
Mailcatcher Web UIgeneric-mailcatcher-181561080
SMTP(仅内网)generic-mailcatcher-1—1025
作业处理generic-worker-1——
前端编译generic-assets_js-1 / generic-assets_css-1——
  • 演示账号:admin@hitobito.ch / q1w2e3r4(密码策略放宽说明见"常见问题"11.3)

二、镜像选型说明

hitobito 没有可直接拉取的生产镜像。官方分发两条路径:

  1. 开发/演示工具箱:hitobito/development 仓库提供 compose 编排与 ghcr.io/hitobito/development/rails、ghcr.io/hitobito/development/mailcatcher 两个镜像,本文采用此形态;
  2. 生产部署:官方运维文档建议 Apache + Passenger 裸机部署,仓库另附生产 Dockerfile(两阶段构建:ruby:3.2-trixie 构建层 + ruby:3.2-slim-trixie 运行层,CMD ["bundle", "exec", "puma"],以 Wagonfile.production 打入 vendor/wagons/ 的 wagon)。

本次实测拉取的镜像与摘要(pull 日志原文节选):

ghcr.io/hitobito/development/rails:latest
Digest: sha256:2d91293ab13a60e473f0017ac8acf5d8fd5e0db0e77773ffeaaf607437b105db
ghcr.io/hitobito/development/mailcatcher:latest
Digest: sha256:4eca5d7b0a6c0fa189a767c2ef5cb207d9f2bdc34aa4480868dd375c4013e156
docker.io/library/postgres:16
Digest: sha256:5db6dac2ef5dcff512f4f00a009c3c332b9f2f28f24277175bf3e4a3d02ef83a
docker.io/library/redis:8-alpine
Digest: sha256:21090af93f8324db2f0f0f4f38a7c60369d47d917e000c3423d89d2ae2883537

💡 技术拓展:像 hitobito 这类 Rails 组织应用的常见部署形态对比——

形态载体优点代价
官方工具箱 composeghcr 开发镜像 + bind mount 源码起全栈最快、自带种子数据面向开发,非生产加固
自构建生产镜像仓库 Dockerfile 两阶段构建源码打包进镜像、可上 K8s/OpenShift需自建 CI,资产预编译
裸机 Apache+Passenger官方运维文档推荐官方生产主流、进程模型成熟依赖 Ruby/DB/Memcached 手工运维

本文选第一种做"可复现实验环境";上生产前应按第二种或第三种重建(差异见 11.5)。

三、前置条件

  1. Docker 与 compose 插件已安装(见"一"实测版本),当前用户在 docker 组:
$ docker --version
Docker version 29.9.2, build a82e528
$ docker compose version
Docker Compose version v5.6.1
  1. git 可用;宿主机用户 UID 若非 1000 需导出 RAILS_UID(compose 以 user: "${RAILS_UID:-1000}" 运行 rails)。
  2. 磁盘与内存:本实例宿主 15 GiB 内存、镜像+依赖卷占用数 GB,建议预留 ≥20 GB。
  3. 网络:ghcr.io、github.com、RubyGems/npm 源均为境外资源。本次实测直连超时后改走代理 192.168.1.72:7897(在 compose 的 environment 中为 rails/assets 服务注入 http_proxy/https_proxy,并以 no_proxy=localhost,127.0.0.1,postgres,redis,mailcatcher 排除内网互访)。无代理环境请确保能访问上述源。
  4. 创建外部依赖卷(gem 与 yarn 缓存,官方 README 要求,只建一次):
$ docker volume create hitobito_bundle
hitobito_bundle
$ docker volume create hitobito_yarn_cache
hitobito_yarn_cache

四、部署步骤

4.1 获取代码(工具箱 + core + wagon 三仓库)

hitobito 实例 = 工具箱 development + 核心 hitobito + 组织 wagon 三个仓库并排放入同一目录。以演示组织 generic 为例:

$ mkdir -p /opt/hitobito && cd /opt/hitobito
$ git clone https://github.com/hitobito/development.git generic
$ cd generic
$ git clone https://github.com/hitobito/hitobito.git
$ git clone https://github.com/hitobito/hitobito_generic.git

工具箱 README 说明:部分 wagon 依赖其他 wagon,需按 hitobito_generic/*.gemspec 中 add_dependency 声明补克隆;generic 演示组织无额外依赖。

把 core 固定在 2.10.0 发布点(默认克隆的是 master,混用会导致 11.1 的 assets 任务错位问题):

$ git -C hitobito checkout 2.10.0
$ cat hitobito/VERSION
2.10.0

4.2 调整 compose 编排

工具箱自带 docker-compose.yml,本次实测做了三处调整(均为合理生产化/环境适配,非上游缺陷):

  1. 端口避让与回环绑定:默认 3000/5432/6379/1080 改绑 127.0.0.1 的 8152/8154/8155/8156,避免与宿主其他服务冲突;
  2. 代理注入:见"三"第 4 条;
  3. rails 健康检查:curl --fail http://localhost:3000/healthz,start_period: 30m(首次启动要装依赖、建库、灌种子,官方 README 明示"可能耗时很长")。

调整后的 rails 服务关键段(实测生效文件节选):

services:
  rails:
    image: ghcr.io/hitobito/development/rails
    restart: unless-stopped
    user: "${RAILS_UID:-1000}"
    env_file: docker/rails/env
    environment:
      BUNDLE_GEMFILE: Gemfile.local
      http_proxy: http://192.168.1.72:7897
      https_proxy: http://192.168.1.72:7897
      no_proxy: localhost,127.0.0.1,postgres,redis,mailcatcher
    ports:
      - "127.0.0.1:8152:3000"
    volumes:
      - .:/usr/src/app
      - hitobito_bundle:/opt/bundle
      - seed:/seed
    healthcheck:
      test: ["CMD", "curl", "--fail", "http://localhost:3000/healthz"]
      interval: 500s
      timeout: 5s
      start_period: 30m
    depends_on:
      postgres:
        condition: service_healthy
      mailcatcher:
        condition: service_healthy

worker 服务复用 rails 定义,仅把命令换成作业处理器并跳过重复初始化:

  worker:
    <<: *rails
    command: [ 'rails', 'jobs:work' ]
    ports: []
    environment:
      SKIP_RAILS_MIGRATIONS: 1
      SKIP_SEEDS: 1
      SKIP_BUNDLE_INSTALL: 1

4.3 启动与等待就绪

$ docker compose up -d

首次启动 rails 入口脚本依次执行 bundle install、db:migrate、种子加载(以 /seed/done-* 标记幂等,重启不会重灌)。就绪判定用健康端点实测:

$ curl -s -i http://localhost:8152/healthz | head -3
HTTP/1.1 200 OK
x-frame-options: SAMEORIGIN
x-xss-protection: 0

⚠️ 注意:assets_js/assets_css 两个服务在 2.10.0 代码 + 最新工具箱镜像组合下会崩溃循环,导致 rails 的 depends_on 条件长期不满足。这是本部署形态最大的坑,定位与修复见 4.4 与 11.1,务必先按 4.4 处理再验证全栈。

4.4 修复 assets 服务崩溃循环(实测攻坚)

现象:generic-assets_js-1、generic-assets_css-1 反复重启,日志原文:

rake aborted!
Don't know how to build task 'assets:watch_js' ... Did you mean?  assets:clean

定位:工具箱镜像的 assets-entrypoint.sh 末尾执行 exec bundle exec rake "assets:watch_$target",而 assets:watch_js/watch_css 任务只存在于 hitobito master 的新资产管线;检出的 2.10.0 release 仍是 webpacker 5(rake -T 实测只有 webpacker:* 系列任务)。镜像(新)与代码(release)版本错位。

手工编译验证 webpacker 管线(第一次失败,Node v22 + webpack 4 的 md4 哈希问题):

Error: error:0308010C:digital envelope routines::unsupported
    at new Hash (node:internal/crypto/hash:101:19)
    at Object.createHash (node:crypto:139:10)
  library: 'digital envelope routines', reason: 'unsupported', code: 'ERR_OSSL_EVP_UNSUPPORTED'
Node.js v22.23.4

加 NODE_OPTIONS=--openssl-legacy-provider 后第二次编译成功(日志节选):

Compiling...
Compiled all packs in /usr/src/app/hitobito/public/packs
Hash: 9d5dd76a40b2826d4af0
Version: webpack 4.46.0
Time: 22506ms
Built at: 11/01/2026 3:37:57 PM
     css/application-f460fb4e.css     340 KiB  [emitted] [immutable] [big]  application
     js/application-f21f8ac8a63316cbbfe4.js     1.2 MiB

修复:把 assets_js 的入口换成 webpacker 周期编译循环,assets_css 改为等 manifest 出现后保活(webpacker 单次编译同时产出 JS+CSS,双 watcher 冗余)。实测生效的 compose 段:

  assets_js:
    image: ghcr.io/hitobito/development/rails
    # FIX 2026-11-02: 上游 assets-entrypoint.sh exec 的 rake assets:watch_js
    # 仅存在于 master;2.10.0 用 webpacker 5,无此任务 -> 崩溃循环。
    entrypoint: [ "/bin/bash", "-lc" ]
    command:
      - |
        cd /usr/src/app/hitobito
        until bundle check >/dev/null 2>&1; do sleep 2; done
        export NODE_OPTIONS=--openssl-legacy-provider
        yarn install
        while true; do bundle exec rake webpacker:compile; sleep 300; done
    restart: unless-stopped
    env_file: docker/rails/env
    environment:
      SKIP_BUNDLE_INSTALL: 1
      BUNDLE_GEMFILE: Gemfile.local
    user: "${RAILS_UID:-1000}"
    volumes:
      - .:/usr/src/app
      - hitobito_bundle:/opt/bundle
      - hitobito_yarn_cache:/home/developer/.cache/yarn

  assets_css:
    <<: *assets
    command:
      - |
        cd /usr/src/app/hitobito
        until bundle check >/dev/null 2>&1; do sleep 2; done
        until [ -f public/packs/manifest.json ]; do sleep 5; done
        echo "packs manifest present; css watcher not needed under webpacker 5"
        while true; do sleep 3600; done

验证:docker compose up -d assets_js assets_css 后两服务 Up;js 日志实测首行 yarn install v1.22.22,随后 success Already up-to-date.、Done in 0.16s. 与 Everything's up-to-date. Nothing to do;css 日志实测 packs manifest present; css watcher not needed under webpacker 5;页面资源实测 GET /packs/css/application-f460fb4e.css 返回 200。

4.5 全栈状态与端到端验证

$ docker compose ps --format 'table {{.Name}}\t{{.Status}}\t{{.Ports}}'
NAME                    STATUS                    PORTS
generic-assets_css-1    Up 20 minutes
generic-assets_js-1     Up 20 minutes
generic-mailcatcher-1   Up 2 hours (healthy)      127.0.0.1:8156->1080/tcp
generic-postgres-1      Up 2 hours (healthy)      127.0.0.1:8154->5432/tcp
generic-rails-1         Up 13 minutes (healthy)   127.0.0.1:8152->3000/tcp
generic-redis-1         Up 2 hours                127.0.0.1:8155->6379/tcp
generic-worker-1        Up 2 hours

Web 入口实测(未登录 302 到登录页):

$ curl -s -I http://localhost:8152/ | grep -iE '^HTTP|^location'
HTTP/1.1 302 Found
location: http://localhost:8152/de/users/sign_in

登录验证(表单字段 person[login_identity]/person[password] + CSRF 令牌):

$ curl -s -b cookies.txt -c cookies.txt -i -X POST http://localhost:8152/de/users/sign_in \
    --data-urlencode "authenticity_token=$TOKEN" \
    --data-urlencode "person[login_identity]=admin@hitobito.ch" \
    --data-urlencode "person[password]=q1w2e3r4" \
    --data-urlencode "person[remember_me]=0" | head -1
HTTP/1.1 303 See Other

💡 说明:$TOKEN 为登录页 authenticity_token 隐藏域实测值(86 字符),提取方式:curl -s http://localhost:8152/de/users/sign_in | python3 -c 'import sys,re; print(re.search(r"name=\"authenticity_token\" value=\"([^\"]+)\"", sys.stdin.read()).group(1))'。用 grep+cut 截取得到的令牌不完整,登录会 422。

作业与邮件链路实测:worker 日志出现 [Worker(host:d24d2888c23f pid:1)] Job WalletSynchronizationJob (id=192) COMPLETED after 0.0068;触发"忘记密码"后 mailcatcher 实测收到 1 封,主题 Anleitung für das Setzen Deines Passworts。

💡 技术拓展:Rails 本地/测试环境的邮件捕获工具不止 Mailcatcher——

工具形态特点
MailcatcherSMTP 服务 + Web UI(1080)轻量,官方工具箱内置
MailHogSMTP + Web UI + APIGo 单二进制,支持消息保留策略
MailpitSMTP + Web UI + APIMailHog 活跃替代,功能更多
letter_opener进程内免端口,仅开发机浏览器弹出

本方案采用官方 compose 内置的 Mailcatcher(ghcr.io/hitobito/development/mailcatcher),SMTP 指向它即可。

五、数据持久化说明

docker compose down(不加 -v)后以下卷保留数据:

卷挂载点内容类型
generic_postgres/var/lib/postgresql/data全部业务数据(111 张表)命名卷
generic_seed/seed种子完成标记 done-*、导入中转命名卷
generic_redis_data/data缓存/Action Cable 状态命名卷
hitobito_bundle/opt/bundleRuby gem 依赖外部卷(预先创建,跨项目共享)
hitobito_yarn_cache/home/developer/.cache/yarnnpm 依赖缓存外部卷
bind mount ./usr/src/app三仓库源码(改代码即生效)宿主目录

实测 docker volume ls 节选:

local     generic_postgres
local     generic_redis_data
local     generic_seed
local     hitobito_bundle
local     hitobito_yarn_cache

⚠️ 注意:源码走 bind mount,git checkout 切版本后需 docker compose restart rails worker 才生效;数据库备份用 docker compose exec postgres pg_dump -U hitobito hitobito_development > backup.sql(compose 内命令,实测可用)。

六、环境变量表

rails/worker/assets 通过 env_file: docker/rails/env 统一注入,本实例实测文件内容全量如下:

RAILS_DB_ADAPTER=postgresql
RAILS_DB_HOST=postgres
RAILS_DB_PORT=5432
RAILS_DB_USERNAME=hitobito
RAILS_DB_PASSWORD=hitobito
RAILS_DB_NAME=hitobito_development
RAILS_ACTIONCABLE_REDIS_URL=redis://hitobito_redis@redis:6379/1
RAILS_HOST_NAME=localhost:8152
RAILS_MAIL_DOMAIN=localhost
RAILS_MAIL_DELIVERY_METHOD=smtp
RAILS_MAIL_DELIVERY_CONFIG=address: mailcatcher, port: 1025
ANNOTATERB_SKIP_ON_DB_TASKS=1
RAILS_LOG_TO_STDOUT=1
IS_DOCKER_DEV_ENV=1
DISABLE_SPRING=true

配套服务侧:docker/postgres.env(POSTGRES_DB=hitobito_development、POSTGRES_USER=hitobito、POSTGRES_PASSWORD=hitobito)与 docker/redis.env(HITOBITO_REDIS_PASSWORD=hitobito_redis,redis 启动命令 redis-server --requirepass $HITOBITO_REDIS_PASSWORD 实测引用)。

compose 级变量汇总:

变量实测值/来源作用
RAILS_UID默认 1000,${RAILS_UID:-1000}rails/assets 运行用户
SKIP_SEEDS / SKIP_RAILS_MIGRATIONS / SKIP_BUNDLE_INSTALLworker=1避免多服务重复初始化
BUNDLE_GEMFILEGemfile.local防止本地 wagon 路径写入锁文件
NODE_OPTIONS--openssl-legacy-providerNode 22 下 webpack 4 编译必需(见 4.4)
http_proxy/https_proxy/no_proxy192.168.1.72:7897境外源访问(见"三")

七、配置参数说明

compose 形态下需要动到的应用配置只有三处,实测说明如下:

  1. 数据库/邮件/主机名:全部走"六"的 env 文件,应用 config/database.yml 等只读环境变量,无需改代码;
  2. 登录密码策略:hitobito/config/initializers/devise.rb 实测默认 config.password_length = 12..128(硬编码,非环境变量)。本实例为使用演示口令 q1w2e3r4(8 位)临时改为 8..128 并 docker compose restart rails(healthz 实测约 165 秒恢复);入口脚本幂等:migrate 空跑、seed 因 done 标记跳过。生产请保持默认下限;
  3. wagon 选择:hitobito/Wagonfile.development 声明开发环境加载哪些 wagon,实测 bundle exec rake wagon:list 输出 generic。

八、配置文件挂载

本 compose 实测挂载的配置相关文件:

宿主路径容器路径模式用途
docker/rails/env(env_file 注入)—应用环境变量
docker/postgresql-setup.sql/docker-entrypoint-initdb.d/ro首次建库脚本
docker/test-setup-postgresql.sql/docker-entrypoint-initdb.d/ro测试库准备
docker/empty/usr/src/app/dumpsro空目录覆盖 dumps,防止误加载生产 dump
docker/rails/home/.bashrc 等/home/developer/ 下同名—开发体验定制
/etc/localtime、/etc/timezone同名ro容器与宿主时区一致

九、等价 docker run

compose 的 rails 服务可拆解为如下等价单条命令(供理解编排与排障用;depends_on/healthcheck 语义需自行保证先后顺序):

$ docker run -d --name hitobito-rails \
    --network generic_default \
    -p 127.0.0.1:8152:3000 \
    --env-file docker/rails/env \
    -e BUNDLE_GEMFILE=Gemfile.local \
    -v /opt/hitobito/generic:/usr/src/app \
    -v hitobito_bundle:/opt/bundle \
    -v generic_seed:/seed \
    ghcr.io/hitobito/development/rails

postgres/redis/mailcatcher 同理分别用 postgres:16(--env-file docker/postgres.env)、redis:8-alpine(--requirepass)、ghcr.io/hitobito/development/mailcatcher 单条运行。注意:单条 docker run 形态失去了 compose 的 service_healthy 依赖门控,rails 必须等 postgres 就绪后再启动,否则会报连库失败。

十、常用运维命令

$ docker compose ps                      # 全栈状态(见 4.5 实测输出)
$ docker compose logs -f rails           # 应用日志(RAILS_LOG_TO_STDOUT=1 直出)
$ docker compose logs worker | tail -2   # 作业日志实测节选:
[Worker(host:d24d2888c23f pid:1)] Job WalletSynchronizationJob (id=192) COMPLETED after 0.0068
1 jobs processed at 108.4782 j/s, 0 failed

$ docker compose restart rails           # 改 initializer/代码后重启(healthz 实测 ~165s 恢复)
$ docker compose exec rails bundle exec rake hitobito:check_config   # 配置自检,实测末行:
✅ SelfRegistrationRoleTypes configured correctly
$ docker compose exec rails bundle exec rails runner 'puts Person.count'
224
$ docker compose down                    # 停栈保留卷;加 -v 连卷删除(数据销毁)

十一、常见问题

11.1 assets 服务无限重启,rails 一直不健康

现象与修复见 4.4 攻坚实录。判定命令:docker compose ps 中 assets 服务无 healthy/长时间 Restarting;日志关键字 Don't know how to build task 'assets:watch_js'。根因是工具箱镜像按 master 新管线构建、代码检出 2.10.0 只有 webpacker 任务。短期修复用 4.4 的 webpacker 周期编译循环;长期方案是代码与工具箱镜像版本配套(master 代码 + 新镜像,或改用生产 Dockerfile 自构建)。

11.2 webpacker:compile 报 ERR_OSSL_EVP_UNSUPPORTED

Node 17+ 默认 OpenSSL 3 不再支持 webpack 4 的 md4 哈希。实测解决:编译前 export NODE_OPTIONS=--openssl-legacy-provider(4.4 已固化进 compose command)。成功标志实测为 Compiled all packs in /usr/src/app/hitobito/public/packs。

11.3 演示账号登录报"密码太短"或重置失败

hitobito 默认密码下限 12 位(devise.rb config.password_length = 12..128),直接设 8 位演示口令会实测抛 ActiveRecord::RecordInvalid: Gültigkeitsprüfung ist fehlgeschlagen: Passwort ist zu kurz (weniger als 12 Zeichen)。本实例已改 8..128 并重启 rails 后重置成功(实测回显 RESET_OK admin@hitobito.ch id=222 sign_in_count=0),随后登录实测 HTTP/1.1 303 See Other。

11.4 首次启动半小时仍不可用

正常现象:入口脚本要完成 bundle install、建库迁移与 generic 种子灌入,compose 因此把 rails 健康检查 start_period 设为 30m。观察 docker compose logs -f rails 直到出现 Puma Listening on http://0.0.0.0:3000,再用 curl http://localhost:8152/healthz 判定。

11.5 这套 compose 能直接上生产吗

不能。它是官方开发工具箱:源码 bind mount、种子数据、Mailcatcher 假 SMTP、无 SECRET_KEY_BASE 强校验。生产按官方运维文档(Apache + Passenger、真实 SMTP 与收信服务、Wagonfile.production)或生产 Dockerfile 自构建镜像部署;ENV 必填项(如 SECRET_KEY_BASE 128 位 hex,实测 bundle exec rails secret 可生成)参见配置篇同类实测。

十二、参考链接

hitobito 2.10 Docker Compose 部署实战 - Quzuo