Claper 3.2.0 配置参数详解

Claper 3.2.0 三层配置体系(.env 环境变量、CONFIG_DIR 文件注入、global_configs 数据库)详解:40+ 环境变量默认值与启动校验规则、布尔与整数解析陷阱、PRESENTATION_STORAGE_DIR 编译期烘焙实测、生产配置模板与九则排障实录。

ClaperClaperElixirPostgreSQL2026/11/01

Claper 3.2.0 配置参数详解

目标:吃透 Claper 3.2.0 的三层配置体系——.env 环境变量、CONFIG_DIR 文件注入、global_configs 数据库运行时配置,给出全部 40+ 参数的默认值、校验规则与生效方式。本文默认值逐项核对自官方仓库 v3.2.0 runtime.exs/.env.sample(查询日期 2026-11-01),全部改参与报错结论在 Ubuntu 22.04 的 claper-3.2.0-stack 实测环境验证(部署过程见 《Claper Docker Compose 部署指南》)。

一、环境信息

项目内容
操作系统Ubuntu 22.04(x86_64),Docker Engine 29.x + Compose v2
Claper 版本3.2.0(镜像 ghcr.io/claperco/claper:3.2.0,Elixir 1.18.4 / OTP 28 / Phoenix 1.7.24)
数据库postgres:15,库/用户/口令均为 claper
部署形态Compose 栈 claper-3.2.0-stack,项目目录 /opt/claper,应用经 env_file: .env 注入配置
对外地址BASE_URL=http://localhost:8129(宿主 8129 → 容器内 Cowboy 4000)
配置文件/opt/claper/.env(权限 600);数据库层 global_configs 表
参数来源核对官方仓库 v3.2.0 runtime.exs、lib/claper/helpers/config.ex、.env.sample、config/config.exs、Dockerfile(jsDelivr CDN 获取,查询日期 2026-11-01)

💡 说明:本文所有「实测」均以宿主机 /opt/claper 下的 compose 栈为载体;实验性改参结束后已全部恢复基线 .env(见 7.9 恢复实录),读者按章节顺序复现即可。

二、前置条件

  1. 栈已按部署篇拉起且两个服务均 healthy:
cd /opt/claper && docker compose ps
NAME               IMAGE                           COMMAND                  SERVICE   CREATED          STATUS                    PORTS
claper-3.2.0-app   ghcr.io/claperco/claper:3.2.0   "sh -c '/app/bin/cla…"   app       23 seconds ago   Up 12 seconds (healthy)   0.0.0.0:8129->4000/tcp, [::]:8129->4000/tcp
  1. HTTP 可达(后续改参验证均以此为基线):
curl -sI http://localhost:8129 | head -1
HTTP/1.1 200 OK
  1. 具备查看运行时配置的通道(release 管理脚本,原理见部署篇「二、镜像选型说明」):
docker compose exec app /app/bin/claper rpc 'IO.puts("rpc-ok")'
rpc-ok

三、参数查看与修改方式

3.1 三层配置体系总览

Claper 的配置按「读取时机与存放位置」分三层,优先级与生效方式各不相同:

层载体读取时机修改后生效方式典型内容
第一层:环境变量/opt/claper/.env(compose env_file: 注入)每次进程启动时由 runtime.exs 求值重建 app 服务(docker compose up -d app,改 .env 后 compose 会重建容器;restart 不会重读)BASE_URL、SECRET_KEY_BASE、POOL_SIZE 等全部 40+ 参数
第二层:CONFIG_DIR 文件注入默认 /run/secrets/<变量名>,每文件一个值与第一层同一次启动求值,文件优先于环境变量写入/删除文件后 docker compose restart app 即可(restart 重新求值 runtime.exs)与第一层同名全部参数(适合 swarm secrets / K8s Secret 挂载)
第三层:数据库运行时配置PostgreSQL global_configs 表 + 事件相关表每次业务调用实时读库管理面板 /admin/settings 保存即生效,无需重启transcription_enabled、lti_enabled、转写 API key;每事件的聊天/投票开关等

三层的关系一句话概括:第一/二层决定「进程怎么起」,第三层决定「功能怎么跑」。第一、二层参数非法会直接拒绝启动(见 7.1–7.5 实录);第三层参数改错只会功能异常,不影响进程存活。

3.2 CONFIG_DIR 文件注入机制

第一、二层的读取都由同一个辅助模块完成(来源:官方仓库 v3.2.0 lib/claper/helpers/config.ex,本地留存核对):

defmodule Claper.ConfigHelpers do
  def get_var_from_path_or_env(config_dir, var_name, default \\ nil) do
    var_path = Path.join(config_dir, var_name)

    if File.exists?(var_path) do
      File.read!(var_path) |> String.trim()
    else
      System.get_env(var_name, default)
    end
  end

  def get_int_from_path_or_env(config_dir, var_name, default \\ nil) do
    var = get_var_from_path_or_env(config_dir, var_name)

    case var do
      nil ->
        default

      var ->
        case Integer.parse(var) do
          {int, ""} -> int
          _ -> raise "Config variable #{var_name} must be an integer. Got #{var}"
        end
    end
  end
end

要点解读:

  • runtime.exs 第一行 config_dir = System.get_env("CONFIG_DIR", "/run/secrets")——目录本身只能由环境变量指定,默认 /run/secrets;
  • 取值顺序:$CONFIG_DIR/<变量名> 文件(内容 String.trim 后整串作为值)→ 环境变量 → 代码内默认值;
  • 整数类参数(MAX_FILE_SIZE_MB、POOL_SIZE、QUEUE_TARGET、PORT 等)走 get_int_from_path_or_env,非整数直接 raise(实测见 7.5);
  • 布尔类参数走 String.to_existing_atom(),只接受 true/false 字面量,其他值产生的原子不是布尔,启动能过但运行到相关页面才炸(实测 500 见 7.6)。

文件注入优先于环境变量,实测(在 app 服务内落三个文件后 restart——restart 会重新求值 runtime.exs 从而读到文件,但沿用原环境变量集合):

docker compose exec app mkdir -p /run/secrets
docker compose exec app sh -c 'echo 42 > /run/secrets/MAX_FILE_SIZE_MB; echo 800x800 > /run/secrets/GS_JPG_RESOLUTION; echo local > /run/secrets/PRESENTATION_STORAGE'
docker compose restart app
docker compose exec app /app/bin/claper rpc 'IO.inspect(Application.get_env(:claper, :presentations))'
[
  max_file_size: 42,
  storage: "local",
  s3_bucket: nil,
  resolution: "800x800",
  s3_public_url: "https://.s3..amazonaws.com"
]

.env 中 MAX_FILE_SIZE_MB 未设(默认应为 15)、GS_JPG_RESOLUTION 未设(默认应为 300x300),而运行时取到了文件里的 42 与 800x800——文件注入生效且优先级更高。自定义目录同样实测:把存放值文件的目录 bind 进 app 服务并在 .env 设 CONFIG_DIR 指向它,重建后 rpc 查询 {max_file_size, pool_size} 返回 {42, 10}(42 来自文件,10 为 POOL_SIZE 默认值),证明 CONFIG_DIR 本身可从环境变量指定。实验完成后删除文件与 CONFIG_DIR 行并恢复基线。

⚠️ 注意:exec 临时写入的 /run/secrets 位于容器可写层,restart 后仍在,但 up -d --force-recreate 重建容器即丢失。生产用文件注入务必以挂载(bind volume / swarm secrets / K8s Secret 卷默认就挂在 /run/secrets)方式落文件,不要手工 exec 写入。

3.3 查看运行时生效值

最权威的方式是让正在跑的进程自己报配置。全量 dump(节选,实测首尾):

docker compose exec app /app/bin/claper rpc 'Application.get_all_env(:claper) |> IO.inspect(limit: :infinity)'
[
  {Oban,
   [
     engine: Oban.Engines.Basic,
     queues: [default: 10, mailers: 20],
     repo: Claper.Repo,
     plugins: [
       {Oban.Plugins.Pruner, [max_age: 604800]},
       {Oban.Plugins.Lifeline, [rescue_after: 1800000]}
     ]
   ]},
  {:terms_url, nil},
  {ClaperWeb.MailboxGuard, [username: nil, password: nil, enabled: false]},
  {:enable_account_creation, true},
  {Claper.Mailer, [adapter: Swoosh.Adapters.Local]},
  {:languages, ["en", "fr", "es", "it", "de"]},
  {:allow_unlink_external_provider, true},
  {:remote_ip_proxies, []},
  {:email_confirmation, false},
  {:storage_dir, "/app/uploads"},
  {:remote_ip_headers,
   ["forwarded", "x-forwarded-for", "x-client-ip", "x-real-ip"]},
  {:logout_redirect_url, nil},
  {Claper.Repo,
   [
     url: "postgres://claper:claper@db:5432/claper",
     ssl: false,
     ssl_opts: [verify: :verify_none],
     prepare: :unnamed,
     pool_size: 10,
     queue_target: 5000
   ]},
  ...
  {ClaperWeb.Endpoint,
   [
     ...
     url: [scheme: "http", host: "localhost", path: nil, port: 8129],
     http: [
       ip: {0, 0, 0, 0},
       port: "4000",
       transport_options: [max_connections: :infinity],
       protocol_options: [
         max_request_line_length: 8192,
         max_header_value_length: 8192
       ]
     ],
     secret_key_base: "xvAqu9LWzKb+S/MChF0Zgd1M6Ixz03yRWvf+aI6VBSM2IlnOy+ng3/oX//MNaljc",
     same_site_cookie: "Lax",
     secure_cookie: false
   ]},
  {:presentations,
   [
     max_file_size: 15,
     storage: "local",
     s3_bucket: nil,
     resolution: "300x300",
     s3_public_url: "https://.s3..amazonaws.com"
   ]},
  {ClaperWeb.Gettext,
   [default_locale: "en", locales: ["fr", "en", "de", "es", "nl"]]}
]

解读三处细节:url.port: 8129 是从 BASE_URL 解析出来的对外自报地址,而 http.port: "4000" 才是实际监听端口(字符串形态直接来自 PORT 环境变量默认值);s3_public_url 在 local 模式下是 nil 插值出的占位串 https://.s3..amazonaws.com,无实际用途;locales 是编译期打进镜像的可用语言全集,languages 是运行时启用子集(见 5.5)。

日常排查建议只查目标键,避免整包 dump 泄露 secret_key_base:

docker compose exec app /app/bin/claper rpc 'IO.inspect({Application.get_env(:claper, :enable_account_creation), Application.get_env(:claper, :languages)}, label: "check")'
check: {true, ["en", "fr", "es", "it", "de"]}

3.4 修改与生效方式速查

修改对象操作生效命令是否中断会话
.env 任一键编辑 /opt/claper/.envdocker compose up -d app(compose 检测 env_file 变化自动重建)是,LiveView 连接断开重连
CONFIG_DIR 文件增删挂载目录内文件同上是
global_configs 表/admin/settings 页面保存,或 SQL UPDATE即存即生效否
每事件开关演示/管理视图内切换实时广播否

⚠️ 注意:docker compose restart app 只重启进程但沿用旧环境变量集合,改了 .env 必须走 up -d(重建)才生效——这是实测确认的行为差异(改 MAX_FILE_SIZE_MB=42 后 restart 仍读到 15,up -d 后读到 42)。

四、参数总览

以下清单逐项核对自官方仓库 v3.2.0 runtime.exs(查询日期 2026-11-01),「默认值」列为代码内建默认(未设环境变量且无 CONFIG_DIR 文件时生效)。带 ※ 的是 runtime.exs 支持但 .env.sample 未列出的隐藏参数。

变量默认值类型作用
BASE_URLhttp://localhost:4000URL对外基地址,解析出 scheme/host/path/port
CONFIG_DIR/run/secrets路径文件注入目录(仅能用环境变量指定)
LISTEN_IP ※0.0.0.0IPCowboy 绑定地址,支持 IPv4/IPv6
PORT ※4000整数容器内监听端口
SECRET_KEY_BASE无(prod 必填)≥32 字节串Phoenix 会话签名密钥
DATABASE_URLpostgres://claper:claper@localhost:5432/postgres连接串PostgreSQL
DB_SSL ※false布尔字面量数据库 TLS
POOL_SIZE ※10整数Ecto 连接池
QUEUE_TARGET ※5000整数取连接排队目标(毫秒)
PRESENTATION_STORAGElocallocal/s3讲稿存储后端
MAX_FILE_SIZE_MB15整数上传讲稿大小上限(MB)
GS_JPG_RESOLUTION300x300WxH幻灯片缩略图分辨率
PRESENTATION_STORAGE_DIR/app/uploads路径编译期烘焙,运行时改 env 无效(见 5.4)
S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEY / S3_REGION / S3_BUCKETnil串s3 模式四件套,缺一拒绝启动
S3_SCHEME / S3_HOST / S3_PORTnil串自建 S3 兼容端点(scheme 与 host 成对)
S3_PUBLIC_URL按公式推导URL对象公开访问前缀
ENABLE_ACCOUNT_CREATIONtrue布尔字面量自助注册开关
EMAIL_CONFIRMATIONfalse布尔字面量注册邮箱验证
ALLOW_UNLINK_EXTERNAL_PROVIDERtrue布尔字面量允许解绑外部登录
LOGOUT_REDIRECT_URLnilURL登出后跳转
TERMS_URL / PRIVACY_URLnilURL注册页法务链接,两者都设才显示
LANGUAGESen,fr,es,it,de逗号列表启用界面语言
SAME_SITE_COOKIELax串Cookie SameSite 策略
SECURE_COOKIEfalse布尔字面量Cookie Secure 标志
REMOTE_IP_PROXIES空CIDR 列表可信反代 IP(默认全不信任)
REMOTE_IP_HEADERSforwarded,x-forwarded-for,x-client-ip,x-real-ip逗号列表取真实客户端 IP 的头
OIDC_ISSUERhttps://accounts.google.comURLOIDC 签发方
OIDC_CLIENT_ID / OIDC_CLIENT_SECRETnil串两者非空才启用 OIDC
OIDC_SCOPESopenid email profile空格列表申请范围
OIDC_PROVIDER_NAMEOpenID Connect串登录按钮显示名
OIDC_LOGO_URL/images/icons/openid.png路径按钮图标
OIDC_PROPERTY_MAPPINGSnilk:v,k:vclaims→用户属性映射
OIDC_AUTO_REDIRECT_LOGINfalse布尔字面量登录页直接跳 IdP
OIDC_TRUST_EMAIL_VERIFIEDfalse=="true" 宽松比较信任邮箱已验证声明
DISABLE_PASSWORD_LOGINfalse布尔字面量仅 OIDC 生效时禁密码登录
MAIL_TRANSPORTlocalsmtp/postmark/其他邮件适配器
MAIL_FROM / MAIL_FROM_NAMEnoreply@claper.co / Claper串发件人
SMTP_RELAY / SMTP_PORT / SMTP_AUTH / SMTP_USERNAME / SMTP_PASSWORDnil / 465 / always / 空 / 空—SMTP 通道
SMTP_RETRIES / SMTP_NO_MX_LOOKUPS / SMTP_SSL / SMTP_TLS / SMTP_SSL_DEPTH / SMTP_SSL_SERVER1 / false / true / if_available / 2 / relay—SMTP 细节
POSTMARK_API_KEYnil串Postmark 通道
ENABLE_MAILBOX_ROUTE / MAILBOX_USER / MAILBOX_PASSWORDfalse / nil / nil—dev mailbox 预览(prod 镜像路由不存在,见 5.6)
POOL_SIZE 之外无 Oban 环境变量——队列并发固定编译期(见 5.10)

对照解读:.env.sample 只暴露了约一半参数,LISTEN_IP、PORT、DB_SSL、POOL_SIZE、QUEUE_TARGET、CONFIG_DIR 这 6 个「隐藏参数」在 runtime.exs 中同样支持——生产调优(连接池、监听地址)不必改镜像,直接用它们即可。

五、核心参数分类详解

5.1 网络与监听:BASE_URL、LISTEN_IP、PORT

BASE_URL 是 Claper 的「自我认知」地址:runtime.exs 用 URI.parse 拆出 scheme/host/path/port 写入 ClaperWeb.Endpoint.url,LiveView WebSocket 回连、邮件内链接、OIDC 回调都基于它。它不改变监听行为(监听由 LISTEN_IP/PORT 决定),只改变对外宣称的地址——反代场景两者必然不一致(容器监听 4000、对外 443),此时 BASE_URL=https://claper.example.com 而 PORT 保持 4000。

scheme 非法直接拒绝启动,实测(.env 改 BASE_URL=ftp://localhost 后重建):

docker compose up -d --force-recreate app 2>&1 | tail -3
docker compose logs app | grep -m1 "Runtime terminating"
Runtime terminating during boot ({#{message=><<"BASE_URL must start with `http` or `https`. Currently configured as `ftp://localhost`">>,'__struct__'=>'Elixir.RuntimeError','__exception__'=>true},[{elixir_eval,'__FILE__',1,[{file,"/app/releases/3.2.0/runtime.exs"},{line,62}]},...
Crash dump is being written to: erl_crash.dump...done

LISTEN_IP 支持 IPv4/IPv6,经 :inet.parse_address 校验,非法值同样拒绝启动(Invalid LISTEN_IP 'not-an-ip' error: :einval,见 7.4)。默认 0.0.0.0 适合容器内全绑定;裸机只想本机暴露可设 127.0.0.1。

PORT 是字符串直通的整数端口(dump 里可见 port: "4000" 保持字符串形态,Cowboy 能识别),改动后记得同步 compose 的 ports: 映射与 BASE_URL 的端口段。

Cowboy 层还有三个不可经环境变量调整的固定值(实测 dump):transport_options: [max_connections: :infinity](并发连接不设上限,LiveView 长连接场景必须)、max_request_line_length: 8192、max_header_value_length: 8192(反代注入超长 Cookie 时注意 8K 上限,超限 Cowboy 直接断连)。

SECRET_KEY_BASE 是 Phoenix 会话/CSRF token 的签名密钥。prod 启动强制校验:未设报错、短于 32 字节报错(实测见 7.1)。生成方式:

openssl rand -base64 48

⚠️ 注意:更换 SECRET_KEY_BASE 会使所有既有会话 cookie 失效(所有人重新登录);泄露则等同源码里 .env.sample 警告的「可伪造 admin cookie 直接登录」。每个环境独立生成、纳入备份、泄露即轮换。

SAME_SITE_COOKIE 与 SECURE_COOKIE 控制登录 cookie 的跨站策略,默认 Lax/false。实测把两者改为 None/true 并重建后,登录响应头:

set-cookie: _claper_key=SFMyNTY...; path=/; expires=Tue, 01 Dec 2026 10:57:57 GMT; max-age=2592000; secure; HttpOnly; SameSite=None

secure; SameSite=None 均已生效。取值组合规则:SameSite=None 必须搭配 Secure=true 且全站 HTTPS,否则浏览器直接丢弃 cookie;纯内网 HTTP 部署保持默认 Lax/false。

💡 技术拓展:SameSite 不止本文用到的两种——

取值行为适用
Lax(默认)跨站 POST 不带 cookie,顶层 GET 导航带常规独立站点
Strict任何跨站请求都不带高安全内网应用,代价是从邮件链接点进也要重登
None跨站全带,必须 Secure需要把 Claper 嵌入第三方页面 iframe 的场景

本实验演示 None+Secure 是为验证参数贯通;生产默认保持 Lax 即可,观众端 /e/:code 页面本身不受 SameSite 影响(顶层导航)。

5.3 数据库与连接池:DATABASE_URL、DB_SSL、POOL_SIZE、QUEUE_TARGET

DATABASE_URL 为标准 PostgreSQL 连接串。DB_SSL 走 String.to_existing_atom(),只接受 true/false;开启后 ssl_opts 固定 verify: :verify_none(源码硬编码,不校验服务端证书),如需严格校验需自行改配置产物——内网直连可保持默认 false。

POOL_SIZE 是每个 app 进程的 Ecto 连接池大小(默认 10),QUEUE_TARGET 是池满后取连接的排队等待目标毫秒数(默认 5000,超时则报 timeout 错误)。估算口径:并发观众 LiveView 连接数 × 平均每连接查询密度远小于连接常驻数,真正吃池的是翻页/投票写入的瞬时并发;单实例 500 观众内默认 10 足够,PG 服务端默认 max_connections=151,多 app 副本时注意 POOL_SIZE × 副本数 < 151。

非法值拒绝启动,实测(.env 加 POOL_SIZE=abc 重建):

Runtime terminating during boot ({#{message=><<"Config variable POOL_SIZE must be an integer. Got abc">>,'__struct__'=>'Elixir.RuntimeError','__exception__'=>true},[{'Elixir.Claper.ConfigHelpers',get_int_from_path_or_env,3,[{file,"lib/claper/helpers/config.ex"},{line,22},...

报错直接点名变量与非法值,定位成本极低。

5.4 演示文稿与存储:PRESENTATION_STORAGE、MAX_FILE_SIZE_MB、GS_JPG_RESOLUTION、PRESENTATION_STORAGE_DIR、S3_*

PRESENTATION_STORAGE 仅接受 local 与 s3 两值,其他值拒绝启动,实测(PRESENTATION_STORAGE=gcs 重建):

Runtime terminating during boot ({#{message=><<"Invalid PRESENTATION_STORAGE value gcs">>,'__struct__'=>'Elixir.RuntimeError','__exception__'=>true},[{elixir_eval,'__FILE__',1,[{file,"/app/releases/3.2.0/runtime.exs"},{line,80}]},...

MAX_FILE_SIZE_MB(默认 15)是上传讲稿的硬上限,改 42 实测生效:

docker compose exec app /app/bin/claper rpc 'IO.puts("max_file_size=#{Application.get_env(:claper, :presentations)[:max_file_size]}")'
max_file_size=42

(恢复默认重建后同命令输出 max_file_size=15。)

GS_JPG_RESOLUTION(默认 300x300)控制 PDF 逐页转换的缩略图分辨率——名字里的 GS 指 Ghostscript,即服务端调它把讲稿转成逐页 JPG。投屏清晰度主要取决于原 PDF 渲染链路,此参数影响的是观众端/管理端列表里的小图;带宽紧张的大厅投影可调大(如 800x800,实测文件注入生效见 3.2),代价是转换耗时与存储翻倍。

PRESENTATION_STORAGE_DIR 是本文最重要的「反直觉」参数。.env.sample 把它列为普通环境变量(PRESENTATION_STORAGE_DIR=/app/uploads),但它的真实读取点在编译期:官方仓库 v3.2.0 config/config.exs 第 82 行

config :claper, :storage_dir, System.get_env("PRESENTATION_STORAGE_DIR", "priv/static")

config.exs 在 release 构建时求值并被烘焙进镜像,runtime.exs 全文不读这个变量。实测对照(一次性起临时进程,注入不同 env 值直接查配置):

docker run --rm -e PRESENTATION_STORAGE_DIR=/tmp/zzz -e SECRET_KEY_BASE="0123456789012345678901234567890123456789012345678901234567890123" \
  --entrypoint /app/bin/claper ghcr.io/claperco/claper:3.2.0 \
  eval 'IO.inspect(Application.get_env(:claper, :storage_dir), label: "storage_dir_with_env")'
storage_dir_with_env: "/app/uploads"

设了 PRESENTATION_STORAGE_DIR=/tmp/zzz,生效值仍是镜像构建期烘焙的 /app/uploads(官方 Dockerfile 第 46 行 ENV PRESENTATION_STORAGE_DIR /app/uploads)。结论:想换存储目录,唯一可行方式是把卷挂到 /app/uploads(部署篇 compose 已如此配置),改环境变量或 CONFIG_DIR 文件都无效。CONFIG_DIR 机制对它也不生效——探针显示环境变量有值而文件路径查询返回 nil(该变量根本不走 get_var_from_path_or_env):

STORAGE_DIR_env="/app/uploads" config_dir=nil

S3 模式四个必填项缺一拒绝启动(报错原文点名四个变量名);S3_SCHEME/S3_HOST/S3_PORT 用于自建 MinIO 等兼容端点(scheme 形如 https://,含结尾冒号斜杠,与 host 必须成对出现);S3_PUBLIC_URL 未设时按 https://<bucket>.s3.<region>.amazonaws.com 推导(AWS 标准路径风格),自建端点务必显式设置,否则观众端取图 404。

5.5 账户、注册与界面:ENABLE_ACCOUNT_CREATION、EMAIL_CONFIRMATION、TERMS_URL/PRIVACY_URL、LANGUAGES 等

ENABLE_ACCOUNT_CREATION=false 关闭自助注册,实测闭环(改 .env → 重建 → 注册页从 200 变 302):

curl -s -o /dev/null -w "BEFORE register: %{http_code}\n" http://localhost:8129/users/register
BEFORE register: 200
echo 'ENABLE_ACCOUNT_CREATION=false' >> .env && docker compose up -d app
curl -s -b jar.txt -o /dev/null -w "AFTER register: %{http_code}\n" http://localhost:8129/users/register
curl -s -b jar.txt -c jar.txt -o /dev/null -w "REGISTER_HTTP=%{http_code} REDIRECT=%{redirect_url}\n" \
  -X POST http://localhost:8129/users/register \
  --data-urlencode "user[email]=newbie@x.co" --data-urlencode "user[password]=q1w2e3r4" \
  --data-urlencode "user[password_confirmation]=q1w2e3r4" --data-urlencode "_csrf_token=$CSRF"
AFTER register: 302
REGISTER_HTTP=302 REDIRECT=http://localhost:8129/

运行配置同步变为 enable_account_creation=: false。关闭后管理员仍可在 /admin/users 建号,属「白名单制」部署的标准姿势。

EMAIL_CONFIRMATION 默认 false(注意:.env.sample 示例注释是 true,与代码默认相反,以代码为准)。开启后注册需点邮件确认链接,因此必须同时配好 MAIL_TRANSPORT=smtp,否则确认邮件躺在 local adapter 内存里永远发不出去——这是组合约束,单独开它等于封死注册入口。

TERMS_URL/PRIVACY_URL 控制注册页法务链接,规则是两者都非空才显示(.env.sample 原文注明),且各自过 http/https scheme 校验。实测两步:只设 TERMS_URL 时注册页 grep 命中 0;两者齐设后 privacy 与 terms 各命中 1:

TERMS_ONLY: 0 hits
      1 https://example.com/privacy
      1 https://example.com/terms

LANGUAGES 逗号分隔启用语言子集,默认 en,fr,es,it,de;镜像内编译期可用的全集是 fr,en,de,es,nl(dump 中 ClaperWeb.Gettext.locales),超出全集的值不会报错但无效。实测收紧为 LANGUAGES=en,de 后运行时值变为 ["en", "de"],界面语言下拉同步只剩两项。

ALLOW_UNLINK_EXTERNAL_PROVIDER 代码默认 true(.env.sample 示例给 false,同样以代码为准),控制已绑 OIDC/LTI 外部身份的用户能否解绑——企业 SSO-only 场景设 false 防绕过。LOGOUT_REDIRECT_URL 设登出后跳转地址(默认 nil=留在本站登录页),配合对外门户统一出口时有用。

布尔类参数的解析陷阱(yes/no/1 等写法导致的 500)实测完整,见 7.6。

5.6 邮件:MAIL_TRANSPORT、SMTP_、POSTMARK_API_KEY、MAILBOX_

MAIL_TRANSPORT 三选一:smtp、postmark、其他任意值都落到 local adapter(源码 case _ -> 分支;实测 dump 显示默认 adapter: Swoosh.Adapters.Local)。local 模式邮件只进内存队列,对外不可达——注册确认、magic link、改密邮件全部静默丢弃,单机试用可以,生产必须接真实通道。

SMTP 分支参数与默认值(全部经 runtime.exs 实测核对):SMTP_RELAY(无默认,必填)、SMTP_PORT 默认 465、SMTP_AUTH 默认 always(可 if_available/never)、SMTP_USERNAME/SMTP_PASSWORD 默认空串、SMTP_RETRIES 默认 1、SMTP_NO_MX_LOOKUPS 默认 false、SMTP_SSL 默认 true、SMTP_TLS 默认 if_available、SMTP_SSL_DEPTH 默认 2、SMTP_SSL_SERVER 默认取 relay 值(SNI 用)。

值得注意:SMTP_SSL=true(默认)时 sockopts 硬编码 verify: :verify_peer + 系统 CA + TLS1.3/1.2——用自签证书的内网 SMTP 中继会握手失败,此时要么 SMTP_SSL=false 走 STARTTLS(SMTP_TLS=if_available),要么把中继证书喂进系统 CA 存储。这是「默认安全」取向的参数,排障时先想到它。

POSTMARK_API_KEY 仅 MAIL_TRANSPORT=postmark 时读取,同时源码切换 Swoosh api_client 为 Finch。

ENABLE_MAILBOX_ROUTE/MAILBOX_USER/MAILBOX_PASSWORD 是 dev 邮箱预览路由(/dev/mailbox)的 BasicAuth 开关,但路由本身在 router.ex 里包在 if Mix.env() == :dev 内——prod 镜像里该路由不存在,设了也不会有页面;dump 中 MailboxGuard 配置项仍在,属编译期残留,忽略即可。

5.7 OIDC 与 SSO:OIDC_* 全家与 DISABLE_PASSWORD_LOGIN

OIDC 启用判定是双非空:OIDC_CLIENT_ID 与 OIDC_CLIENT_SECRET 都是非空白字符串时 enabled: true,否则整条链路关闭(dump 实测 oidc=[enabled: false, ...] 时 issuer 仍显示默认值 https://accounts.google.com——未启用时其他 OIDC 参数只是占位)。

OIDC_SCOPES 按空格分隔(默认 openid email profile,与其他逗号分隔参数风格不同);OIDC_PROPERTY_MAPPINGS 按 k:v,k:v 解析成 claim 映射表(如 roles:custom_attributes.roles);OIDC_AUTO_REDIRECT_LOGIN=true 时访问登录页直接 302 去 IdP。

OIDC_TRUST_EMAIL_VERIFIED 的解析方式与其他布尔不同:源码用 String.trim |> String.downcase == "true" 宽松比较,TRUE/True 都能识别,不会踩 7.6 的原子陷阱;它跳过「OIDC 首登与同邮箱既有账号需邮件确认」的安全步骤,仅当 IdP 确实验证过每个地址(如 Google)才开。

DISABLE_PASSWORD_LOGIN 是运维最容易翻车的参数:逻辑为 请求值 and oidc_enabled——OIDC 未配置时设 true 无效,且启动日志给出显式警告(实测两条重复,boot 与 config 阶段各一次):

claper-3.2.0-app  | warning: DISABLE_PASSWORD_LOGIN=true has no effect because OIDC is not configured (OIDC_CLIENT_ID / OIDC_CLIENT_SECRET are missing or blank). Password login stays enabled.

这个「无效」是救命的:若 OIDC 配好后再设 true,密码登录立即封死,包括种子管理员 admin@claper.co。.env.sample 给出的启用顺序必须严格遵守:先用种子 admin 登录 → 用目标 OIDC 账号登录一次 → 在管理面板把该 OIDC 账号提为 admin → 最后才设 DISABLE_PASSWORD_LOGIN=true。跳步的后果是全员(含自己)无法登录,只能进数据库手工提权。

5.8 反向代理与真实 IP:REMOTE_IP_PROXIES、REMOTE_IP_HEADERS

REMOTE_IP_PROXIES 逗号分隔的可信代理 IP/CIDR 列表(如 10.0.0.0/8),默认空=不信任任何转发头;REMOTE_IP_HEADERS 指定按序检查哪些头取真实客户端 IP,默认 forwarded,x-forwarded-for,x-client-ip,x-real-ip。不配 REMOTE_IP_PROXIES 时,Claper 日志与限流(登录接口 10 次/分钟,router.ex 内建 rate_limit_auth)看到的全部是反代 IP——公网反代部署必配,否则限流会误伤反代后面的所有用户。

实测基线 dump:remote_ip_proxies=[]、remote_ip_headers=["forwarded", "x-forwarded-for", "x-client-ip", "x-real-ip"],与代码默认一致。

5.9 数据库层运行时配置:global_configs 与每事件状态

第三层配置存 PostgreSQL,不经重启。管理面板 /admin/settings 读写 global_configs 表,实测查询:

docker compose exec db psql -U claper -d claper -c "SELECT key, value FROM global_configs ORDER BY key;"
              key               | value 
--------------------------------+-------
 lti_enabled                    | false
 transcription_api_key          | 
 transcription_default_language | 
 transcription_enabled          | false
(4 rows)

直接 SQL 改也即时生效(适合脚本化),实测翻转实时字幕开关:

docker compose exec db psql -U claper -d claper -c "UPDATE global_configs SET value='true' WHERE key='transcription_enabled';"
docker compose exec db psql -U claper -d claper -c "SELECT key, value FROM global_configs WHERE key='transcription_enabled';"
UPDATE 1
          key          | value 
-----------------------+-------
 transcription_enabled | true
(1 row)

(实验后已按同样方式改回 false,回显 UPDATE 1 + transcription_enabled | false。)

事件级状态存于 presentation_states 表,每场演示的互动开关与默认值一查便知(实测 \d presentation_states 节选):

 position                 | integer                        |          | 0
 chat_visible             | boolean                        |          | false
 poll_visible             | boolean                        |          | false
 join_screen_visible      | boolean                        |          | false
 banned                   | character varying(255)[]       |          | ARRAY[]::character varying[]
 chat_enabled             | boolean                        |          | true
 anonymous_chat_enabled   | boolean                        |          | true
 show_only_pinned         | boolean                        |          | false
 message_reaction_enabled | boolean                        |          | true
 show_attendee_count      | boolean                        |          | true

即:聊天/匿名提问/表情反应/人数显示默认全开,大屏显示开关默认关,banned 数组存封禁观众标识。这些开关在演示/管理视图切换后实时广播,无需任何重启。事件主表 events 的 audience_peak(观众峰值)默认 1,code 非空唯一,外键级联删除覆盖 posts/polls/stats/presentation_files 等全部子表——删事件即删全场数据,备份口径见部署篇第五节。

5.10 BEAM 运行时与后台任务(不可经环境变量调整项)

三项与配置强相关但没有对应环境变量,在此点名避免读者徒劳试错:

  1. Oban 队列并发:default: 10, mailers: 20,Pruner 保留 7 天(max_age: 604800),Lifeline 30 分钟救援(实测 dump 原文,见 3.3)——编译期固定,改需重建 release;
  2. ELIXIR_ERL_OPTIONS=+fnu:不是 Claper 参数而是 Erlang VM 开关,容器 locale 非 UTF-8 时必须注入,否则中文文件名/事件名双重编码(排障实录见部署篇 11.2);
  3. MIX_ENV:镜像构建期 ENV MIX_ENV="prod" 已烘焙,runtime.exs 的 prod 校验分支(SECRET_KEY_BASE 长度、BASE_URL scheme)靠它激活,运行时无需也不应改动。

六、生产配置模板

适用场景:公网 HTTPS 单实例,4GB 内存 / 40GB SSD,单场 ≤500 观众(更大规模先加 POOL_SIZE 与 PG max_connections,再考虑多副本)。/opt/claper/.env:

# —— 对外地址与监听 ——
BASE_URL=https://claper.example.com
LISTEN_IP=0.0.0.0
PORT=4000

# —— 安全 ——
SECRET_KEY_BASE=<openssl rand -base64 48 生成,勿复用>
SAME_SITE_COOKIE=Lax
SECURE_COOKIE=true

# —— 数据库 ——
DATABASE_URL=postgres://claper:<强口令>@db:5432/claper
DB_SSL=false
POOL_SIZE=10
QUEUE_TARGET=5000

# —— 存储 ——
PRESENTATION_STORAGE=local
MAX_FILE_SIZE_MB=50
GS_JPG_RESOLUTION=300x300

# —— 账户策略(白名单制) ——
ENABLE_ACCOUNT_CREATION=false
EMAIL_CONFIRMATION=false
ALLOW_UNLINK_EXTERNAL_PROVIDER=false
LANGUAGES=en,de,fr

# —— 邮件(注册确认/magic link 必须真实可达) ——
MAIL_TRANSPORT=smtp
SMTP_RELAY=smtp.example.com
SMTP_PORT=465
SMTP_AUTH=always
SMTP_USERNAME=claper@example.com
SMTP_PASSWORD=<注入,勿明文入库>
MAIL_FROM=noreply@example.com
MAIL_FROM_NAME=Claper

# —— 反代真实 IP ——
REMOTE_IP_PROXIES=10.89.56.0/24
REMOTE_IP_HEADERS=x-forwarded-for,x-real-ip

# —— 法务与跳转 ——
TERMS_URL=https://example.com/terms
PRIVACY_URL=https://example.com/privacy
LOGOUT_REDIRECT_URL=https://example.com

# —— BEAM 编码修复(中文环境必加) ——
ELIXIR_ERL_OPTIONS=+fnu

模板要点:SECURE_COOKIE=true 只在反代终结 TLS 后开(反代须传 X-Forwarded-Proto https,否则浏览器拒收 cookie 造成登录后死循环);ENABLE_ACCOUNT_CREATION=false + 管理员建号适合企业内训;REMOTE_IP_PROXIES 填反代所在网段(本例 compose 子网),配合 router.ex 内建登录限流才不误伤;S3 化时把存储四项换成 PRESENTATION_STORAGE=s3 + S3_* 四件套并显式设 S3_PUBLIC_URL。

七、常见问题

7.1 启动报 SECRET_KEY_BASE must be at least 32 bytes long

现象(实测原文,节选):

Runtime terminating during boot ({#{message=><<"SECRET_KEY_BASE must be at least 32 bytes long. See https://docs.claper.co/configuration.html#production-docker">>,'__struct__'=>'Elixir.RuntimeError','__exception__'=>true},[{elixir_eval,'__FILE__',1,[{file,"/app/releases/3.2.0/runtime.exs"},{line,39}]},...
Crash dump is being written to: erl_crash.dump...done

定位:runtime.exs 对 prod 的显式长度校验(byte_size(key) < 32 即 raise)。解决:openssl rand -base64 48 重新生成写入 .env,docker compose up -d app 重建。

7.2 启动报 Invalid LISTEN_IP 'not-an-ip' error: :einval

现象(实测原文,节选):

Runtime terminating during boot ({#{message=><<"Invalid LISTEN_IP 'not-an-ip' error: :einval">>,'__struct__'=>'Elixir.RuntimeError','__exception__'=>true},...{line,25}...

定位::inet.parse_address 校验失败,LISTEN_IP 必须是合法 IPv4/IPv6 字面量。解决:改回 0.0.0.0 或填具体地址;报错直接带原值,秒级定位。

7.3 启动报 Invalid PRESENTATION_STORAGE value gcs

见 5.4。合法值只有 local/s3;GCS/Azure 需走 S3 兼容端点(S3_SCHEME/S3_HOST 指向其兼容网关)。

7.4 启动报 Config variable POOL_SIZE must be an integer. Got abc

见 5.3。所有整数类参数(PORT/POOL_SIZE/QUEUE_TARGET/MAX_FILE_SIZE_MB/SMTP_PORT/SMTP_RETRIES/SMTP_SSL_DEPTH)共用 get_int_from_path_or_env,非纯数字直接 raise,报错点名变量。

7.5 BASE_URL 报 scheme 错误

见 5.1 与 7.1 同形态:BASE_URL must start with \http` or `https`. Currently configured as `ftp://localhost``。实测原文见 5.1 代码块。

7.6 注册页 500:BadBooleanError(布尔参数写 yes/no 的陷阱)

现象:把 ENABLE_ACCOUNT_CREATION 以 CONFIG_DIR 文件方式写成 yes,启动一切正常,访问注册页直接 500,日志实测原文:

claper-3.2.0-app  | 11:02:30.555 request_id=GOPm5aF_WgPgFSwAAB4D [info] GET /users/register
claper-3.2.0-app  | 11:02:30.555 [error] #PID<0.3053.0> running ClaperWeb.Endpoint (connection #PID<0.3052.0>, stream id 1) terminated
Server: localhost:8129 (http)
Request: GET /users/register
** (exit) an exception was raised:
    ** (BadBooleanError) expected a boolean on left-side of "and", got: :no
        (claper 3.2.0) lib/claper_web/controllers/user_registration_controller.ex:60: ClaperWeb.UserRegistrationController.account_creation_allowed?/0

定位:布尔参数经 String.to_existing_atom() 解析——yes/no 恰好是 BEAM 预存原子,解析成 :yes/:no 能通过启动,直到控制器里 :no and ... 才在运行期炸出 500。探针确认了配置值形态:

value_of_yes=: :yes
register: 500
value_of_no=: :no
register_with_no: 500

更冷门的值(如 1、on)因对应原子不存在,to_existing_atom 会直接在启动期 ArgumentError——两种形态都是「写法不规范」的代价。

解决:布尔类参数(ENABLE_ACCOUNT_CREATION/EMAIL_CONFIRMATION/DB_SSL/SECURE_COOKIE/OIDC_AUTO_REDIRECT_LOGIN/DISABLE_PASSWORD_LOGIN/ALLOW_UNLINK_EXTERNAL_PROVIDER/ENABLE_MAILBOX_ROUTE)只写 true 或 false。改回规范值后实测恢复:register_restored: 200。

7.7 DISABLE_PASSWORD_LOGIN=true 设了没反应

启动日志已有答案(实测见 5.7):OIDC 未配置时该参数被 and oidc_enabled 短路,属故意设计——防止把包括种子 admin 在内的所有人锁在门外。先按 5.7 的四步顺序配好 OIDC 再启用。

7.8 改了 PRESENTATION_STORAGE_DIR 上传目录不变

不是 bug,是机制(实测见 5.4):该变量在 config.exs 编译期求值并烘焙进镜像,运行时环境变量与 CONFIG_DIR 文件都不生效。换目录 = 把卷挂到 /app/uploads;确需其他路径只能自 build 镜像时传 --build-arg/改 Dockerfile ENV。

7.9 恢复基线

本文全部实验(MAX_FILE_SIZE_MB=42、LANGUAGES=en,de、SameSite=None/Secure=true、ENABLE_ACCOUNT_CREATION=false、transcription_enabled=true、CONFIG_DIR 文件、DISABLE_PASSWORD_LOGIN=true)在取证结束后已全部还原并重建验证,实测回显:

max_file_size=15
languages=["en", "fr", "es", "it", "de"]
cookie=[same_site_cookie: "Lax", secure_cookie: false]
account_creation=true
register: 200

八、参考链接

Claper 3.2.0 配置参数详解 - Quzuo