Loomio 3.14.0 Docker Compose 部署指南

Loomio 3.14.0 官方 compose 形态主机实测部署:版本实查、内网无域名变体(去 nginx-proxy/acme/haraka 加 mailpit)、6 服务起栈回显、建管理员与群组投票全流程、CSRF 双提交与 stance 422 等 3 条排障实录、升级与备份。

LoomioLoomio自托管DockerRailsPostgreSQLRedis协作2026/11/02

Loomio 3.14.0 Docker Compose 部署指南

目标:在 Ubuntu 主机上用 Docker Compose 从零部署 Loomio 3.14.0(含内网无域名变体),走通"版本实查 → 编写 compose → 起栈 → 建管理员 → 建群组/投票/投票"全流程;所有命令与回显均为 2026-11-02 主机实测。认知与选型见 《Loomio 3.14.0 介绍与选型指南》,参数语义见 《Loomio 3.14.0 配置参数详解》,运维命令见 《Loomio 3.14.0 常用 CLI 命令与工具详解》。

一、环境信息

  • 操作系统:Ubuntu(主机),Docker 29.9.2 / Docker Compose v5.6.1
  • 主机资源:6 核 CPU / 15 GiB 内存(部署时可用 9.5 GiB)
  • 主机地址:192.168.1.172(内网 IP 直访,无域名、无 TLS)
  • 端口规划:8144=Web(app:3000)、8145=hocuspocus 协同编辑(容器 80)、8146=mailpit Web UI(8025);SMTP 1025 仅内部网络
  • 内部网络:loomio_main,子网 10.89.68.0/24
  • 组件版本(docker compose images 与 bin/rails about 实测):
组件版本说明
loomio/loomio3.14.0(镜像 ID d2db5b238845,3.48 GB)app/worker/hocuspocus 三服务共用
postgres17(PostgreSQL 17.11)数据库
redis8.4(redis_version:8.4.8)缓存与 Action Cable
axllent/mailpitv1内网变体新增的邮件捕获服务
Rails8.1.3.1应用框架
Ruby4.0.9语言运行时
Puma8.0.2Web 服务器
Solid Queue1.7.0后台任务队列

💡 技术拓展:Loomio 官方发行形态只有 Docker Compose(仓库 deploy/ 目录),没有独立二进制或 apt 包。同类自托管协作软件常见发行形态对比——

形态代表特点
Docker Compose 全家桶Loomio、Rallly应用+DB+缓存一键起栈,官方唯一支持路径
单镜像 + 外部依赖Bitpoll、IHateMoney只发应用镜像,DB 自备
裸机包安装Framadate(PHP)依赖现成 LAMP 环境

本文按官方唯一的 Compose 形态部署,并给出内网无域名变体。

二、镜像选型说明

2.1 版本实查

以 GitHub Releases API 查询最新稳定版(2026-11-02):

curl -s https://api.github.com/repos/loomio/loomio/releases/latest | python3 -c \
  "import json,sys;d=json.load(sys.stdin);print(d['tag_name'],d['published_at'],d['html_url'])"

实测输出:

v3.14.0 2026-11-01T02:08:42Z https://github.com/loomio/loomio/releases/tag/v3.14.0

近期发布序列 3.13.2 / 3.13.1 / 3.13.0 / 3.12.0,节奏约每 1–2 周一版;仓库 AGPL-3.0,stars 2624 / forks 711(2026-11-02 实查)。

2.2 镜像与 tag 核对

Docker Hub 镜像 loomio/loomio 存在 3.14.0 tag(与 latest/master 同日构建)。注意:官方 env_template 默认 LOOMIO_CONTAINER_TAG=3.5,明显滞后于当前版本,实查后必须手动钉到 3.14.0。

docker compose images

实测输出(节选,实测首尾):

CONTAINER           REPOSITORY          TAG                 IMAGE ID            SIZE
loomio-app          loomio/loomio       3.14.0              d2db5b238845        3.48GB
loomio-db           postgres            17                  3b0cda93be7c        640MB
loomio-redis        redis               8.4                 5d4cea9e22db        202MB
loomio-mailpit      axllent/mailpit     v1                  0790692be935        51.1MB

loomio/loomio:3.14.0 单镜像 3.48 GB(内含 Rails 应用、Node 协同服务与前端资产),是本组件最大的磁盘开销项;app、worker、hocuspocus 三个服务共用同一镜像,只占一份磁盘。

三、前置条件

3.1 主机要求

  • Docker 20.10+ 与 Compose v2+(实测 29.9.2 / v5.6.1)
  • 空闲端口:Web 端口(本文 8144)、协同编辑端口(8145)、邮件 UI(8146);若走官方 HTTPS 形态则需 80/443 空闲
  • 磁盘 ≥ 15 GB(镜像 3.48 GB + 数据卷 + 升级冗余);内存 ≥ 2 GB(实测全栈稳态约 1.1 GB)

3.2 官方部署资产获取

官方 compose 与配套脚本位于 loomio/loomio 仓库 deploy/ 目录(GitHub API contents 实查,2026-11-02):docker-compose.yml、env_template、create_env.sh、update.sh、UPGRADING.md。

mkdir -p /opt/loomio && cd /opt/loomio
git clone --depth 1 https://github.com/loomio/loomio loomio-src
cp -r loomio-src/deploy ./ && cd deploy

3.3 create_env.sh 的 FQDN 限制

官方 create_env.sh 用镜像内 Ruby 生成 .env,对主机名做 FQDN 校验(源码实测读取):

abort "hostname must be a fully qualified domain name" if hostname.length > 253 || !hostname.include?(".")

即参数必须是含点的域名(如 loomio.example.com),192.168.1.172:8144 这类 IP:端口直接被拒绝。脚本还会生成 POSTGRES_PASSWORD、SECRET_COOKIE_TOKEN、RAILS_INBOUND_EMAIL_PASSWORD、SERVER_API_KEY(各 32 字节 hex)与 VAPID 推送密钥对。内网无域名场景必须手写 .env(见 4.2)。

⚠️ 注意:SERVER_API_KEY 必须 ≥17 字符,否则应用拒绝启动(env_template 注释原文:"It must be at least 17 characters; Loomio will not start with a shorter key")。手写时用 openssl rand -hex 32 生成。

四、部署步骤

4.1 官方 compose 形态与本方案变体

官方 deploy/docker-compose.yml 共 8 服务:nginx-proxy / nginx-proxy-acme(自动签 Let's Encrypt 证书)/ app / worker / db / redis / haraka(收信 SMTP)/ hocuspocus(Yjs 协同编辑),app 仅 expose: 3000 由 nginx-proxy 反代。

内网无域名、无公网证书场景按下表裁剪(本文实测变体,6 服务):

官方服务变体处理理由
nginx-proxy / nginx-proxy-acme删除,app 直接 ports: "8144:3000"无域名无法签 LE 证书
haraka删除不需要邮件收件回帖
hocuspocus保留,ports: "8145:80"协同编辑必需
app/worker/db/redis保留核心四件套
mailpit新增 axllent/mailpit:v1,ports: "8146:8025"内网捕获发信,验证 SMTP 链路

4.2 编写 .env

完整 .env(密钥值均为本次实验实值,生产环境请全部重生成):

# 关键密钥生成方式
openssl rand -hex 32   # SERVER_API_KEY / SECRET_COOKIE_TOKEN / RAILS_INBOUND_EMAIL_PASSWORD
# /opt/loomio/deploy/.env(内网无域名部署)
POSTGRES_CONTAINER_TAG=17
LOOMIO_CONTAINER_IMAGE=loomio/loomio
LOOMIO_CONTAINER_TAG=3.14.0

SITE_NAME=Loomio
CANONICAL_HOST=192.168.1.172
CANONICAL_PORT=8144
PUBLIC_APP_URL=http://192.168.1.172:8144
PRIVATE_APP_URL=http://app:3000
SUPPORT_EMAIL=admin@loomio.local
HOCUSPOCUS_URL=ws://192.168.1.172:8145
RAILS_INBOUND_EMAIL_URL=http://app:3000/rails/action_mailbox/relay/inbound_emails

SMTP_DOMAIN=mailpit
SMTP_SERVER=mailpit
SMTP_PORT=1025

WEB_CONCURRENCY=2
MIN_THREADS=3
MAX_THREADS=3
# FORCE_SSL=1          # 无 TLS 场景必须整行注释(设 0 无效)
USE_RACK_ATTACK=1
RACK_ATTACK_RATE_MULTPLIER=1
RACK_ATTACK_TIME_MULTPLIER=1

POSTGRES_PASSWORD=q1w2e3r4
POSTGRES_DB=loomio_production
DATABASE_URL=postgresql://postgres:q1w2e3r4@db/loomio_production
REDIS_CACHE_URL=redis://redis:6379/0
REDIS_MAXMEMORY=256mb
ACTIVE_STORAGE_SERVICE=local

LOOMIO_SYSTEM_RELOAD=1
LOOMIO_DISABLE_DEMO_GROUPS=1

SERVER_API_KEY=e1af7a533f349783e691a57bfef50c592af9994f9dbd2ba45b0a710c764fe034
SECRET_COOKIE_TOKEN=aed935f4d492c2014b1dead298b72e1b04675b55962c4424320a45a59f4f9a75
RAILS_INBOUND_EMAIL_PASSWORD=47600e7b9a55d8d9645b23f9e622c13ba3d762a2e1b5cba98a5649612dbdcf7f
RAILS_ENV=production

逐项语义见 《Loomio 3.14.0 配置参数详解》。最关键的书写规则在 env_template 首行原文:要禁用某项必须注释/删除整行,设 KEY=0 仍是启用态。

4.3 编写 docker-compose.yml

变体 compose 全文(/opt/loomio/deploy/docker-compose.yml):

name: loomio

services:
  app:
    container_name: loomio-app
    image: ${LOOMIO_CONTAINER_IMAGE}:${LOOMIO_CONTAINER_TAG}
    restart: unless-stopped
    healthcheck:
      test: ['CMD-SHELL', 'curl --fail http://localhost:3000/ || exit 1']
    ports:
      - "8144:3000"
    env_file: ./.env
    volumes:
      - ./uploads:/loomio/public/system
      - ./storage:/loomio/storage
      - ./files:/loomio/public/files
      - ./plugins:/loomio/plugins/docker
      - ./import:/import
      - ./tmp:/loomio/tmp
      - client3:/loomio/public/client3
    networks:
      - main
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy

  worker:
    container_name: loomio-worker
    image: ${LOOMIO_CONTAINER_IMAGE}:${LOOMIO_CONTAINER_TAG}
    restart: always
    networks:
      - main
    env_file: ./.env
    environment:
      - TASK=worker
    depends_on:
      app:
        condition: service_healthy
    volumes:
      - ./uploads:/loomio/public/system
      - ./storage:/loomio/storage
      - ./files:/loomio/public/files
      - ./plugins:/loomio/plugins/docker
      - ./tmp:/loomio/tmp

  db:
    container_name: loomio-db
    image: postgres:${POSTGRES_CONTAINER_TAG}
    restart: unless-stopped
    healthcheck:
      test: ['CMD', 'pg_isready', '-U', 'postgres']
    networks:
      - main
    env_file: ./.env
    volumes:
      - ./pgdata:/pgdata
    environment:
      - PGDATA=/pgdata

  redis:
    container_name: loomio-redis
    image: redis:8.4
    restart: unless-stopped
    command:
      - redis-server
      - --save
      - ""
      - --appendonly
      - "no"
      - --maxmemory
      - "${REDIS_MAXMEMORY:-256mb}"
      - --maxmemory-policy
      - allkeys-lru
    healthcheck:
      test: ['CMD', 'redis-cli', 'ping']
    networks:
      - main

  hocuspocus:
    container_name: loomio-hocuspocus
    image: ${LOOMIO_CONTAINER_IMAGE}:${LOOMIO_CONTAINER_TAG}
    restart: unless-stopped
    networks:
      - main
    env_file: ./.env
    ports:
      - "8145:80"
    environment:
      - TASK=hocuspocus

  mailpit:
    container_name: loomio-mailpit
    image: axllent/mailpit:v1
    restart: unless-stopped
    networks:
      - main
    ports:
      - "8146:8025"

volumes:
  client3:

networks:
  main:
    ipam:
      config:
        - subnet: 10.89.68.0/24

要点解读:

  • app 的 7 个卷把上传件、附件存储、前端资产全部落到宿主机(client3 是唯一例外,用命名卷,原因见排障实录 1);worker 复用同一组卷以便处理附件类任务。
  • worker 与 hocuspocus 靠环境变量 TASK 切换启动形态(镜像入口 docker_start.sh 按 $TASK 分支 exec),与官方设计一致。
  • redis 关闭持久化(--save "" --appendonly no):官方定位 Redis 仅做缓存与 Action Cable,丢了可重建。
  • db 的 PGDATA=/pgdata 配合 bind mount ./pgdata,数据直接落宿主机目录。

⚠️ 注意(排障实录 1):初稿删掉官方 compose 顶部 volumes: client3: 声明块时,docker compose up 直接报错:

service "app" refers to undefined volume client3: invalid compose project

app 服务引用了命名卷 client3,就必须在顶层 volumes: 声明它——删服务可以连带删卷声明,删卷声明不能留引用。补上后恢复。

4.4 起栈与健康检查

cd /opt/loomio/deploy
docker compose up -d

等待约 2 分钟后实测 docker compose ps:

loomio-app          Up 8 minutes (healthy)    80/tcp, 0.0.0.0:8144->3000/tcp
loomio-db           Up 9 minutes (healthy)    5432/tcp
loomio-hocuspocus   Up 9 minutes              0.0.0.0:8145->80/tcp
loomio-mailpit      Up 26 minutes (healthy)   1025/tcp, 0.0.0.0:8146->8025/tcp
loomio-redis        Up 26 minutes (healthy)   6379/tcp
loomio-worker       Up 8 minutes              80/tcp

首次启动时 app 入口脚本自动执行 script/sync_client_assets 与 bundle exec rake db:prepare(见 docker_start.sh 源码,4.5 节),无需手工建库。

验证 Web 与协同服务:

curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' http://192.168.1.172:8144/
curl -s http://192.168.1.172:8144/up
curl -s -o /dev/null -w '%{http_code}\n' http://192.168.1.172:8145/

实测输出:

302 http://192.168.1.172:8144/dashboard
ok
200

根路径 302 跳 /dashboard、健康检查 /up 返回 ok、hocuspocus 端口 200,栈即起净。

4.5 数据库初始化确认

db:prepare 在 app 启动入口自动执行。实测建表数:

docker compose exec db psql -U postgres -d loomio_production -tAc \
  "select count(*) from information_schema.tables where table_schema='public'"
80

⚠️ 注意:若手工跑 rake db:setup,会提示 Database 'loomio_production' already exists - 跳过——因为 POSTGRES_DB=loomio_production 已由 postgres 镜像初始化时创建,属正常现象,schema 仍会载入。官方推荐入口用的就是幂等的 db:prepare。

4.6 创建管理员账号

Loomio 首次启动没有任何用户,注册入口虽开放,但演示/管理账号用 rails runner 直建最可控:

docker compose exec app bin/rails runner '
u = User.new(name: "admin", username: "admin_lmo", email: "admin@loomio.local")
u.password = "q1w2e3r4"
puts u.save
puts u.errors.full_messages.inspect'

实测输出(弱口令被拒):

false
["Password This password has previously appeared in a data breach and should never be used"]

⚠️ 注意:Loomio 内置泄露密码库校验,固定测试口令 q1w2e3r4 会被直接拒绝,这是与其他组件演示账号规范不同的实测点。演示账号改用叠加后缀的强口令(本次实测环境为 admin / admin@loomio.local / q1w2e3r4!Loomio):

docker compose exec app bin/rails runner '
u = User.new(name: "admin", username: "admin_lmo", email: "admin@loomio.local")
u.password = "q1w2e3r4!Loomio"
u.is_admin = true
u.email_verified = true
puts u.save!'

创建成功(实测终态核验):

docker compose exec db psql -U postgres -d loomio_production -tAc \
  "select id,name,username,email,is_admin,email_verified from users"
1|Admin|admin_lmo|admin@loomio.local|t|t

4.7 登录验证(CSRF 双提交)

Loomio 的 JSON API 走浏览器会话时受 Rails CSRF 保护,POST 必须携带与 csrftoken cookie 同值的 X-CSRF-Token 头:

curl -s -c lk.txt http://192.168.1.172:8144/dashboard -o /dev/null
TOK=$(grep csrftoken lk.txt | awk '{print $7}')
echo "cookie_len=${#TOK}"
curl -s -b lk.txt -H "Content-Type: application/json" -H "X-CSRF-Token: $TOK" \
  -X POST http://192.168.1.172:8144/api/v1/sessions \
  -d '{"user":{"email":"admin@loomio.local","password":"q1w2e3r4!Loomio"}}' | python3 -c \
  "import json,sys;d=json.load(sys.stdin);print('current_user_id=',d['current_user_id'],'user=',d['users'][0]['username'],'flash=',d['flash']['notice'])"

实测输出:

cookie_len=86
current_user_id= 1 user= admin_lmo flash= Signed in successfully

⚠️ 注意(排障实录 2):漏带或错值 X-CSRF-Token 时,所有 POST/PATCH 返回 422 且响应体只有 {"status":422},看不到任何原因;必须翻 app 日志才能看到 ActionController::InvalidAuthenticityToken。这是本文档实测踩过的最隐蔽坑。

4.8 建群组、发投票、投第一票

登录会话内依次调用(均带 X-CSRF-Token 头,载荷为实测成功形态):

# 1) 建群组
curl -s -b lk.txt -H "Content-Type: application/json" -H "X-CSRF-Token: $TOK" \
  -X POST http://192.168.1.172:8144/api/v1/groups -d '{"group":{
    "name":"决策实验组","handle":"demo-group","group_privacy":"open",
    "membership_granted_upon":"request","description_format":"md"}}'

HTTP 200,响应 groups[0]:id=1、handle=demo-group、name=决策实验组、group_privacy=open(群组字段是 name 而非 title)。

# 2) 发 proposal 投票(一次到位的正确载荷)
curl -s -b lk.txt -H "Content-Type: application/json" -H "X-CSRF-Token: $TOK" \
  -X POST http://192.168.1.172:8144/api/v1/polls -d '{"poll":{
    "group_id":1,"title":"是否采用弹性加班制度","details":"试行三个月后复盘",
    "details_format":"md","poll_type":"proposal",
    "options":["agree","abstain","disagree","block"],
    "closing_at":"2026-11-09T07:00:00Z"}}'

HTTP 200,polls[0]:id=1、key=9bvixmJ0、poll_type=proposal;opened_at 自动置位(closing_at 有值且未设 opening_at 即立即开放);4 个选项自动生成。

# 3) 投票(proposal 的 score 必须为 1)
curl -s -b lk.txt -H "Content-Type: application/json" -H "X-CSRF-Token: $TOK" \
  -X POST http://192.168.1.172:8144/api/v1/stances -d '{"stance":{
    "poll_id":1,"reason":"同意试行,建议先跑三个月","reason_format":"md",
    "stance_choices_attributes":[{"score":1,"poll_option_id":1}]}}'

HTTP 200,stances[0]:id=1、cast_at 置位、option_scores={"1":1}。

终态核验(psql 实测):

docker compose exec db psql -U postgres -d loomio_production -tAc \
  "select id,key,title,poll_type,opened_at,closing_at from polls"
docker compose exec db psql -U postgres -d loomio_production -tAc \
  "select id,name from poll_options order by id"
docker compose exec db psql -U postgres -d loomio_production -tAc \
  "select id,poll_id,option_scores,reason,cast_at from stances"
1|9bvixmJ0|是否采用弹性加班制度|proposal|2026-11-02 07:08:09.41894|2026-11-09 07:00:00
1|Agree
2|Abstain
3|Disagree
4|Block
1|1|{"1": 1}|同意试行,建议先跑三个月|2026-11-02 07:08:18.4367

⚠️ 注意(排障实录 3):建投票时若不带 poll_type,后续投票必 422。根因链(源码实测):poll_type 缺省 → 模板值不加载 → ballot_rule=nil → ValidatesBallot 的 ballot_policy_invalid? 走 else 分支报 "stance_choices is not a valid value"。proposal 的 defaults 为 score_min=score_max=1、choice_count 1..1,所以 score 必须传 1。

4.9 邮件链路验证(mailpit)

SMTP 指向内置 mailpit(mailpit:1025,无认证无 TLS)。验证捕获服务在线:

curl -s http://192.168.1.172:8146/api/v1/messages | head -c 120
{"total":0,"unread":0,"count":0,"messages_count":0,"messages_unread":0,"start":0,"tags":[],"messages":[]}

单用户实例无通知对象,0 封属正常;邀请成员、投票截止提醒等邮件都会在此可见(Web UI 端口 8146)。

4.10 演示群组脏数据与库重置

新库首次建群组时实测被自动塞入 3 个假用户与 24 个演示投票——源码 app/extras/app_config.rb:203 的 demo_groups_enabled? 只要 CANONICAL_HOST 有值默认启用演示群组。处置:.env 增加 LOOMIO_DISABLE_DEMO_GROUPS=1 后重置数据库:

docker compose stop app worker hocuspocus
docker compose exec db psql -U postgres -c \
  "DROP DATABASE IF EXISTS loomio_production WITH (FORCE);" -c \
  "CREATE DATABASE loomio_production;"
docker compose up -d   # 入口 db:prepare 自动载 schema,80 表、0 用户

重置后核验:users=0、群组数 0,再走 4.6–4.8 全流程即得干净终态(1 user / 1 group / 1 poll / 1 stance / 4 options)。

五、数据持久化说明

宿主路径挂载点内容备份优先级
./pgdata/pgdataPostgreSQL 数据目录最高
./uploads/loomio/public/system旧式上传件高
./storage/loomio/storageActive Storage 本地附件(ACTIVE_STORAGE_SERVICE=local)高
./files/loomio/public/files主题 logo 等静态文件中
./import/import数据导入目录中
./tmp/loomio/tmp运行时临时文件低
命名卷 loomio_client3/loomio/public/client3前端构建资产(镜像内同步而来,可再生)低

client3 用命名卷而非 bind mount:入口脚本 script/sync_client_assets 每次启动把镜像内新资产合并进卷("New files win"),升级时自动滚动。

六、环境变量表

.env 关键项与实测值(完整语义见 《Loomio 3.14.0 配置参数详解》):

变量实测值作用
LOOMIO_CONTAINER_TAG3.14.0钉版本(默认 3.5 滞后)
CANONICAL_HOST / CANONICAL_PORT192.168.1.172 / 8144应用生成 URL 的主机与端口
PUBLIC_APP_URL / PRIVATE_APP_URLhttp://192.168.1.172:8144 / http://app:3000浏览器侧 / 内部侧基址
HOCUSPOCUS_URLws://192.168.1.172:8145协同编辑 WebSocket
SMTP_SERVER / SMTP_PORTmailpit / 1025发信出口
WEB_CONCURRENCY / MAX_THREADS2 / 3Puma 并发(官方 auto=CPU 数)
FORCE_SSL整行注释无 TLS 必须注释而非置 0
USE_RACK_ATTACK1限速中间件开关
POSTGRES_PASSWORD / DATABASE_URLq1w2e3r4 / postgresql://postgres:q1w2e3r4@db/loomio_production库连接
REDIS_CACHE_URL / REDIS_MAXMEMORYredis://redis:6379/0 / 256mb缓存
ACTIVE_STORAGE_SERVICElocal附件本地盘
LOOMIO_DISABLE_DEMO_GROUPS1禁演示群组
LOOMIO_SYSTEM_RELOAD1升级后通知客户端重载
SERVER_API_KEYopenssl rand -hex 32Server API Bearer 密钥(≥17 字符)
SECRET_COOKIE_TOKEN / RAILS_INBOUND_EMAIL_PASSWORDopenssl rand -hex 32会话签名 / 收信 relay 口令

七、配置参数说明

本变体与官方形态的三处结构性差异,均已在 4.1–4.3 落地:

  1. 无 nginx-proxy:app 直接发布 8144:3000,官方 compose 里 app 是 expose: 3000 且带 VIRTUAL_HOST/ACME_HOST 标签,变体全部删除。
  2. 无 haraka:收信链路(邮件回帖、bounce 处理)整体不启用,RAILS_INBOUND_EMAIL_URL 保留默认值无害。
  3. SMTP 无认证:SMTP_SERVER=mailpit 且未设 SMTP_USERNAME/SMTP_PASSWORD/SMTP_USE_SSL——注意 SMTP_USE_SSL 同样必须整行删除,置空串仍算启用。

八、配置文件挂载

Loomio 无独立应用配置文件,全部配置经 .env 注入(compose 的 env_file: ./.env 同时给 app/worker/db/hocuspocus)。官方形态另有一个 nginx 层配置:configs: nginx-custom → /etc/nginx/conf.d/custom.conf(设置 client_max_body_size,配合 FILE_UPLOAD_MAX_MB),变体删了 nginx-proxy 即不需要。主题静态文件放 ./files(对应 THEME_*_SRC 变量引用的 /files/ 前缀路径)。

九、等价 docker run

compose 六服务对应的单命令等价形态(网络/卷先行创建;仅演示依赖关系,生产建议仍用 compose):

docker network create --subnet 10.89.68.0/24 loomio_main
docker volume create loomio_client3

docker run -d --name loomio-db --network loomio_main \
  -e POSTGRES_PASSWORD=q1w2e3r4 -e POSTGRES_DB=loomio_production -e PGDATA=/pgdata \
  -v /opt/loomio/deploy/pgdata:/pgdata postgres:17

docker run -d --name loomio-redis --network loomio_main \
  redis:8.4 redis-server --save "" --appendonly no --maxmemory 256mb --maxmemory-policy allkeys-lru

docker run -d --name loomio-app --network loomio_main --env-file /opt/loomio/deploy/.env \
  -p 8144:3000 \
  -v /opt/loomio/deploy/uploads:/loomio/public/system \
  -v /opt/loomio/deploy/storage:/loomio/storage \
  -v /opt/loomio/deploy/files:/loomio/public/files \
  -v /opt/loomio/deploy/plugins:/loomio/plugins/docker \
  -v /opt/loomio/deploy/import:/import \
  -v /opt/loomio/deploy/tmp:/loomio/tmp \
  -v loomio_client3:/loomio/public/client3 \
  loomio/loomio:3.14.0

docker run -d --name loomio-worker --network loomio_main --env-file /opt/loomio/deploy/.env \
  -e TASK=worker \
  -v /opt/loomio/deploy/uploads:/loomio/public/system \
  -v /opt/loomio/deploy/storage:/loomio/storage \
  -v /opt/loomio/deploy/files:/loomio/public/files \
  -v /opt/loomio/deploy/plugins:/loomio/plugins/docker \
  -v /opt/loomio/deploy/tmp:/loomio/tmp \
  loomio/loomio:3.14.0

docker run -d --name loomio-hocuspocus --network loomio_main --env-file /opt/loomio/deploy/.env \
  -e TASK=hocuspocus -p 8145:80 loomio/loomio:3.14.0

十、常用运维命令

docker compose ps                    # 服务状态
docker compose logs -f app           # 应用日志
docker compose logs worker | grep -i recurring   # 定时任务流水
docker compose restart app           # 改 .env 后重启生效
docker compose exec app bin/jobs check           # 队列健康(见 D1 篇)
docker compose exec db pg_dump -U postgres loomio_production > backup_$(date +%F).sql
docker compose down                  # 停止(数据保留);加 -v 才会删命名卷

升级按官方 deploy/update.sh(原文全文):

#!/bin/bash
docker system prune -a -f && docker compose pull && docker compose down && \
docker compose run --rm app rake db:migrate && docker compose down && \
docker compose up -d --remove-orphans

即"清旧镜像 → 拉新 → 停栈 → 一次性跑迁移 → 起新栈";LOOMIO_SYSTEM_RELOAD=1 会让浏览器端收到 boot/version 的 reload:true 提示自动重载。

十一、常见问题

Q1:起栈报 service "app" refers to undefined volume client3: invalid compose project? 删了顶层 volumes: client3: 声明但 app 仍引用该卷。补回声明即可(见 4.3 排障实录 1)。

Q2:API POST 一律 422,响应体只有 {"status":422}? 漏带 X-CSRF-Token(须与 csrftoken cookie 同值)。看 app 日志确认 InvalidAuthenticityToken(见 4.7 排障实录 2)。若走 Server API(/api/server0/* + Bearer)则免 CSRF,见 《Loomio 3.14.0 常用 CLI 命令与工具详解》。

Q3:投票 422 报 stance_choices is not a valid value? 建 poll 时没带 poll_type,ballot_rule 为空;proposal 的 score 必须为 1(见 4.8 排障实录 3)。

Q4:新群组里莫名多出 3 个假用户和一堆演示投票? 演示群组默认启用。.env 加 LOOMIO_DISABLE_DEMO_GROUPS=1 并重置库(见 4.10)。

Q5:create_env.sh 拒绝 192.168.1.172 这种主机名? 它强制 FQDN(含点)。内网场景手写 .env,密钥用 openssl rand -hex 32(见 3.3)。

Q6:FORCE_SSL=0 了还是强制跳 https? 禁用必须注释/删除整行,任何非空值都算启用(env_template 首行警告原文)。

Q7:rake db:setup 提示 Database already exists 是否失败? 不是失败:POSTGRES_DB 已由 postgres 镜像初始化建库,schema 仍正常载入(实测 80 表)。入口用的 db:prepare 天然幂等。

十二、参考链接

Loomio 3.14.0 Docker Compose 部署指南 - Quzuo