Debian 12 安装 Keila 0.30.3 指南

在 Debian 12 服务器上从源码完整构建并部署 Keila 0.30.3:依赖链、数据库初始化、管理员账号、登录验证与测试邮件发送全流程实测。

KeilaKeila邮件服务邮件列表PostgreSQL2026/10/26

Debian 12 安装 Keila 0.30.3 指南

一、环境信息

项目值
操作系统Debian 12(bookworm,x86_64)
Keila 版本0.30.3(写作时 GitHub 最新稳定版)
Erlang/OTP27.3.4.9(erts-15.2.7.6)
Elixir1.18.4
数据库PostgreSQL 15.19(Debian 12 官方仓库版本)
Node.js / npm18.20.4 / 9.2.0(Debian 12 官方仓库版本)
应用监听端口4000(对外发布为 8139,见下文说明)
测试收信端Mailpit(内网 10.89.60.3:1025,Web 界面 http://192.168.146.131:8138)

服务器为一台全新安装的 Debian 12 主机,root 权限,可访问公网(GitHub、hex.pm)。Keila 的定位、架构与同类产品对比见《Keila 0.30.3 介绍与选型指南》,本文专注落地:从零开始在一台 Debian 12 服务器上完成源码构建、数据库接入、管理员账号初始化,并验证「注册登录 → 建项目 → 建联系人 → 发测试邮件」的完整闭环。

二、前置条件

  1. 一台 Debian 12 服务器,具备 root 权限与至少 2 GB 内存(mix deps.compile 与 mix release 对内存有一定要求,1 GB 可能被 OOM 杀掉编译进程);
  2. 磁盘剩余空间不少于 5 GB(源码、依赖目录与发布产物合计约 1.5 GB,加上 PostgreSQL 数据留出余量);
  3. 服务器可访问公网,安装过程需要从 GitHub 与 hex.pm 下载源码与依赖;
  4. 一个用于接收测试邮件的 SMTP 收信端。生产环境使用真实 SMTP 中继(如 Mailgun、SES 或企业邮箱),本文实验环境使用内网 Mailpit(SMTP 监听 10.89.60.3:1025,Web 收件箱 http://192.168.146.131:8138)替代,其安装不在本文范围内,读者可替换为任意可收信的 SMTP 服务。

三、版本与发布形态说明

写作时 Keila 的最新稳定版本为 0.30.3(2026 年发布线)。选它而非更旧的 0.8.x/0.9.x 系列,是因为 0.30.x 是当前 GitHub 默认 release 线,依赖栈已升级到 Elixir 1.18 / Phoenix 1.7.21 / Ecto 3.13。

需要特别说明的是发布形态:Keila 官方没有提供面向 Debian/Ubuntu 的 .deb 仓库包,也没有裸二进制发行包。官方发布的只有两种形态:

  • 源码(GitHub tag tarball),需要自行编译出 release 产物后运行;
  • 官方打包的部署形态(拉取即用,属于另一种部署路线,不在本文范围内,本专栏另有专文)。

因此本文走源码构建路线,这也是把 Keila 部署在传统服务器上的唯一路径。整个链路为:预编译 Erlang/OTP 27 + 预编译 Elixir 1.18.4 + 源码 mix release,无需在服务器上现场编译 BEAM 本身,构建时间可控制在 30 分钟以内。

四、安装系统依赖

Debian 12 官方仓库即可满足全部构建依赖:

apt-get update
apt-get install -y build-essential git cmake openssl npm postgresql

安装完成后核对版本:

dpkg -l build-essential cmake git npm postgresql | grep ^ii | awk '{print $2, $3}'

实测回显:

build-essential 12.9
cmake 3.25.1-1
git 1:2.39.5-0+deb12u3
npm 9.2.0~ds1-1
postgresql 15+248+deb12u1

两个容易漏装的包说明:

  • cmake 是必需的:依赖 fast_html(HTML 解析库,底层是 C 实现的 lexbor)通过 cmake 构建。缺 cmake 时 mix deps.compile 会在编译 fast_html 时报 Error 127: cmake: not found;
  • npm 是必需的:Keila 的前端资源(CodeMirror 编辑器、Tailwind CSS 等)需要通过 npm 安装 node_modules 后再由 esbuild 打包,nodejs 会作为 npm 的依赖一并装上(Debian 12 提供 node 18.20.4,实测满足要求)。

五、安装 PostgreSQL 并初始化数据库

Debian 12 仓库的 PostgreSQL 15 安装后默认已启动:

pg_lsclusters

实测回显:

Ver Cluster Port Status Owner    Data directory              Log file
15  main    5432 online postgres /var/lib/postgresql/15/main /var/log/postgresql/postgresql-15-main.log

创建 Keila 专用的数据库账号与库(密码 q1w2e3r4 为本文实验口令,生产环境请使用强密码):

su postgres -c "psql -c \"CREATE USER keila WITH PASSWORD 'q1w2e3r4';\""
su postgres -c "psql -c \"CREATE DATABASE keila OWNER keila;\""

验证 TCP 密码登录(Keila 走的就是 TCP 连接,必须验证这一步):

PGPASSWORD=q1w2e3r4 psql -h 127.0.0.1 -U keila -d keila -c "select current_user, current_database();"

实测回显:

 current_user | current_database 
--------------+------------------
 keila        | keila
(1 row)

为什么必须独立建库建账号:Keila 的全部状态都存在 PostgreSQL 里——业务表、Oban 作业队列表、会话数据都在同一个库中,首次启动会自动执行约 30 张表的迁移。它不像某些组件依赖 Redis 或外部消息队列,PostgreSQL 是 Keila 唯一的外部服务依赖,DB_URL 环境变量是唯一必填的数据库配置。

六、安装 Erlang/OTP 27 与 Elixir 1.18.4

Debian 12 仓库自带的 OTP 25 与 Elixir 1.13 无法满足 Keila 0.30.3 的要求(mix.exs 声明 elixir: "~> 1.18"),需要安装新版。为避免现场编译 BEAM(耗时且吃内存),采用官方预编译产物:

第一步:下载并校验预编译 OTP。hex.pm 的构建服务提供 linux/amd64 预编译包(没有 debian-12 专用 target,ubuntu-22.04 的 glibc 版本与 Debian 12 兼容,实测可用):

cd /opt && mkdir -p downloads && cd downloads
wget https://builds.hex.pm/builds/otp/amd64/ubuntu-22.04/OTP-27.3.4.9.tar.gz
sha256sum OTP-27.3.4.9.tar.gz

实测回显(校验值与官方构建产物一致,确认无损):

8ff93b2f89fd3c47342e22a031eb303c24305aeddaae0736462103ea04565e83  OTP-27.3.4.9.tar.gz

第二步:安装 OTP。预编译包解压后需要运行其自带的 Install 脚本绑定安装路径:

tar -xzf OTP-27.3.4.9.tar.gz
mv OTP-27.3.4.9 /usr/local/otp-27
cd /usr/local/otp-27 && ./Install -sasl /usr/local/otp-27
ln -sf /usr/local/otp-27/bin/erl /usr/local/bin/erl
ln -sf /usr/local/otp-27/bin/escript /usr/local/bin/escript

第三步:安装 Elixir 1.18.4 官方预编译包(注意该 zip 无顶层目录,要直接解压到目标路径):

cd /opt/downloads
wget https://github.com/elixir-lang/elixir/releases/download/v1.18.4/elixir-otp-27.zip
sha256sum elixir-otp-27.zip
unzip -o elixir-otp-27.zip -d /usr/local/elixir-1.18.4
ln -sf /usr/local/elixir-1.18.4/bin/elixir /usr/local/bin/elixir
ln -sf /usr/local/elixir-1.18.4/bin/mix /usr/local/bin/mix

实测 elixir-otp-27.zip 的 sha256:

5be18f35e329f7c5914a80dd9f323d7bbb144616df1ed16f6f0862a1900b4bb5  elixir-otp-27.zip

第四步:验证:

elixir --version

实测回显:

Erlang/OTP 27 [erts-15.2.7.6] [source] [64-bit] [smp:128:6] [ds:128:6:10] [async-threads:1] [jit:ns]

Elixir 1.18.4 (compiled with Erlang/OTP 27)

七、下载源码并构建 release

7.1 下载源码并校验

cd /opt/downloads
wget https://github.com/pentacent/keila/archive/refs/tags/0.30.3.tar.gz -O keila-0.30.3.tar.gz
sha256sum keila-0.30.3.tar.gz

实测回显:

c18c2bb32cd0f0bda3305cefb1e16c9456dcfe0d43e53d6043a95c6c0d42d4fc  keila-0.30.3.tar.gz

解压到工作目录:

tar -xzf keila-0.30.3.tar.gz -C /opt
cd /opt/keila-0.30.3

7.2 拉取并编译 Elixir 依赖

首次使用 mix 前先初始化 hex 与 rebar:

export LANG=C.UTF-8 LC_ALL=C.UTF-8
export MIX_ENV=prod
mix local.hex --force && mix local.rebar --force
mix deps.get

首次执行会逐个拉取全部依赖(约 85 个,包含 ecto_sql、postgrex、oban、phoenix 等);依赖已就位时再次执行会输出:

All dependencies are up to date

随后编译依赖(这一步耗时最长,约 5–10 分钟):

mix deps.compile

编译过程会滚动输出各依赖的编译信息,形如:

==> phoenix_ecto
Compiling 7 files (.ex)
Generated phoenix_ecto app

注意:编译 fast_html 时若报 Error 127: cmake: not found,说明漏装了 cmake,apt-get install -y cmake 后重跑 mix deps.compile 即可。

7.3 安装前端依赖并构建静态资源

Keila 官方构建脚本使用 npm ci,但在 Debian 12 自带的 npm 9.2.0 下会因 package.json 与 package-lock.json 锁文件版本不一致而失败:

61 error `npm ci` can only install packages when your package.json and package-lock.json
or npm-shrinkwrap.json are in sync. Please update your lock file with `npm install` before continuing.

实测改用 npm install(npm 会自动修补锁文件差异)即可正常安装:

npm install --prefix assets --no-audit --no-fund
ls assets/node_modules | wc -l

实测回显:

227

接着构建并摘要静态资源(Tailwind CSS、esbuild 打包、phx.digest 指纹化):

mix assets.deploy

实测回显(尾部):

Done in 847ms.
Check your digested files at "priv/static"

构建完成后 priv/static/ 下应出现 css/ js/ fonts/ images/ vendor/ cache_manifest.json 等完整产物。如果跳过这一步直接 release,Web 界面会因为缺少 CSS/JS 而完全残缺——这是源码构建 Keila 最常见的坑。

7.4 生成 release 产物

mix release

若已存在旧产物会交互式询问是否覆盖,脚本化部署建议直接加 --overwrite。构建成功后尾部输出:

    _build/prod/rel/keila/bin/keila start

Once the release is running:

    # To connect to it remotely
    _build/prod/rel/keila/bin/keila remote

    # To stop it gracefully (you may also send SIGINT/SIGTERM)
    _build/prod/rel/keila/bin/keila stop

To list all commands:

    _build/prod/rel/keila/bin/keila

产物位于 /opt/keila-0.30.3/_build/prod/rel/keila/,它是自包含的:内含 BEAM 运行所需的全部 BEAM 文件与静态资源,不依赖系统里安装的 Elixir/mix。确认静态资源已进入产物:

ls _build/prod/rel/keila/lib/keila-0.30.3/priv/static/

实测回显:

cache_manifest.json
css
downloads
favicon-c43e2492ba322d2b6e759812ebcd79f0.ico
favicon.ico
fonts
images
js
robots-f4ae1c90665e2972cb598ebf2c8d23b0.txt
robots-f4ae1c90665e2972cb598ebf2c8d23b0.txt.gz
robots.txt
robots.txt.gz
social-icons
vendor

八、环境变量与首次启动

Keila release 通过环境变量完成全部配置(完整清单见《Keila 0.30.3 配置参数详解》)。本文按最小可用集配置:

cd /opt/keila-0.30.3/_build/prod/rel/keila
export LANG=C.UTF-8 LC_ALL=C.UTF-8
export DB_URL="postgres://keila:q1w2e3r4@127.0.0.1:5432/keila"
export SECRET_KEY_BASE="$(openssl rand -base64 48)"
export URL_HOST="192.168.146.131"
export URL_PORT="8139"
export URL_SCHEMA="http"
export MAILER_SMTP_HOST="10.89.60.3"
export MAILER_SMTP_PORT="1025"
export MAILER_SMTP_FROM_EMAIL="newsletter@keila.local"
export MAILER_SMTP_AUTH_METHOD="none"
export MAILER_SMTP_TLS_MODE="none"

三点说明:

  • SECRET_KEY_BASE:会话签名密钥,要求至少 64 字符,openssl rand -base64 48 恰好生成 64 字符。该值必须固定下来(写入 env 文件或 systemd 单元),每次重启都换会导致所有登录会话失效;
  • URL_HOST / URL_PORT:对外的访问地址,用于生成邮件中的退订链接等。本文实验环境的应用监听 4000 端口(PORT 默认值),经 8139 端口对外发布,所以两者不同;直连部署时 URL_PORT 与 PORT 通常一致;
  • MAILER_SMTP_*:系统邮件发信通道(注册激活、密码重置等走这里)。AUTH_METHOD=none + TLS_MODE=none 对应内网测试收信端;生产环境接真实 SMTP 时改回默认的 auto 即可。

8.1 管理员账号(KEILA_USER / KEILA_PASSWORD)

Keila 没有预置管理员,实例管理员通过初始化种子创建。在首次启动前设置:

export KEILA_USER="admin@keila.local"
export KEILA_PASSWORD="q1w2e3r4q1"

两点实测得出的重要结论:

  1. 每次应用启动都会自动执行数据库迁移与初始化种子(等价于 bin/keila eval "Keila.ReleaseTasks.init()"),所以 KEILA_USER/KEILA_PASSWORD 必须在第一次启动前就位。若首次启动时未设置,种子会以默认值创建 root@localhost 并随机生成密码(日志中会打印),后续再设置环境变量也不会重建(种子只在库为空时执行);
  2. 密码最少 10 个字符——这是 Keila 的硬性校验(Argon2id 哈希前校验 password, min: 10),常见演示口令 q1w2e3r4 只有 8 位,无法通过注册与种子创建,本文使用其衍生 q1w2e3r4q1。

8.2 启动

bin/keila daemon

daemon 模式以后台守护进程运行,日志写入 release 目录下 tmp/log/erlang.log.1。首次启动会跑完全部迁移并创建管理员,从日志可确认:

2026-10-26T06:22:17.883 [info] == Running 20260529100000 Keila.Repo.Migrations.ConvertMjRawLiquidToKeilaCode.up/0 forward
2026-10-26T06:22:17.884 [info] == Running 20260607120000 Keila.Repo.Migrations.AddMessageHeadersAndCcFields.change/0 forward
2026-10-26T06:22:17.886 [info] == Running 20260818093917 Keila.Repo.Migrations.AddMessageReceiptIndex.change/0 forward
2026-10-26T06:22:17.888 [info] == Running 20260818122205 Keila.Repo.Migrations.AddMessageStatsIndex.change/0 forward
2026-10-26T06:22:18.057 [info] Created root user with admin@keila.local

确认进程与 HTTP 服务:

bin/keila pid
curl -s -o /dev/null -w 'HTTP %{http_code} -> %{redirect_url}\n' http://192.168.146.131:8139/

实测回显:

40795
HTTP 302 -> http://192.168.146.131:8139/auth/login

302 跳转到登录页即为正常(未登录访问首页被重定向)。同时可在数据库侧验证迁移结果:

PGPASSWORD=q1w2e3r4 psql -h 127.0.0.1 -U keila -d keila \
  -c "select count(*) as tables from information_schema.tables where table_schema = 'public';" \
  -c "select email, activated_at is not null as activated from users;"

实测回显:

 tables 
--------
     30
(1 row)

       email       | activated 
-------------------+-----------
 admin@keila.local | t
(1 row)

30 张业务表迁移完成、管理员账号已激活(activated = t)。

九、登录验证与发信闭环

以下步骤完整走通「登录 → 建项目 → 生成 API Key → 建联系人 → 建 Campaign → 发送测试邮件」。

9.1 登录

Keila 登录是普通表单提交,先取 CSRF token 再 POST:

CSRF=$(curl -s -c cookies.txt http://192.168.146.131:8139/auth/login \
  | grep -oE 'name="_csrf_token"[^>]*' | grep -oE 'value="[^"]+"' | sed 's/value="//;s/"//')
curl -s -b cookies.txt -c cookies.txt -o /dev/null \
  -w 'POST /auth/login -> %{http_code} redirect=%{redirect_url}\n' \
  --data-urlencode "user[email]=admin@keila.local" \
  --data-urlencode "user[password]=q1w2e3r4q1" \
  --data-urlencode "_csrf_token=$CSRF" \
  http://192.168.146.131:8139/auth/login

实测回显:

POST /auth/login -> 302 redirect=http://192.168.146.131:8139/

302 回首页即登录成功(首个管理员拥有实例的全部管理能力,包括 /admin/users 用户管理)。

9.2 创建项目

Keila 以项目(Project)组织联系人、发件人与 Campaign:

CSRF=$(curl -s -b cookies.txt -c cookies.txt http://192.168.146.131:8139/projects/new \
  | grep -oE 'name="_csrf_token"[^>]*' | grep -oE 'value="[^"]+"' | sed 's/value="//;s/"//')
curl -s -b cookies.txt -c cookies.txt -o /dev/null \
  -w 'POST /projects/new -> %{http_code} redirect=%{redirect_url}\n' \
  --data-urlencode "project[name]=Keila Demo" \
  --data-urlencode "_csrf_token=$CSRF" \
  http://192.168.146.131:8139/projects/new

实测回显:

POST /projects/new -> 302 redirect=http://192.168.146.131:8139/projects/np_weLJnLY5

项目 ID 为 np_weLJnLY5(下文复用)。

9.3 生成 API Key

API Key 在 Web 界面的「Settings → API Keys」创建,同样可以通过表单 POST 完成:

CSRF=$(curl -s -b cookies.txt -c cookies.txt \
  http://192.168.146.131:8139/projects/np_weLJnLY5/api_keys/new \
  | grep -oE 'name="_csrf_token"[^>]*' | grep -oE 'value="[^"]+"' | sed 's/value="//;s/"//')
curl -s -b cookies.txt --data-urlencode "api_key[name]=b1-verify" \
  --data-urlencode "_csrf_token=$CSRF" \
  http://192.168.146.131:8139/projects/np_weLJnLY5/api_keys -o apikey.html
grep -oE 'value="[A-Za-z0-9_-]{40,}"' apikey.html

实测回显(页面提示 The private API Key is displayed only once and cannot be recovered,仅显示一次):

value="lkp0Henf2NsXM8Vehf4R6zKdAE23eGzSRB-ftkwqTJQ"

9.4 创建联系人

Keila 的联系人模型没有独立的「联系人列表」层级,联系人在项目内扁平管理,通过**分群(Segment)**圈选投递范围;不指定 Segment 的 Campaign 默认发送给项目内全部 active 联系人。用 REST API 建两个联系人:

curl -s -X POST -H "Authorization: Bearer lkp0Henf2NsXM8Vehf4R6zKdAE23eGzSRB-ftkwqTJQ" \
  -H "Content-Type: application/json" \
  -d '{"data":{"email":"alice@example.com","first_name":"Alice","last_name":"Chen"}}' \
  http://192.168.146.131:8139/api/v1/contacts

实测回显:

{"data":{"data":null,"id":"nc_weLJnLY5","status":"active","email":"alice@example.com","inserted_at":"2026-10-26T06:29:39Z","updated_at":"2026-10-26T06:29:39Z","external_id":null,"first_name":"Alice","last_name":"Chen"}}

注意请求体必须是 {"data": {...}} 包裹——Keila 的 OpenAPI 规范把业务字段包在 data 键下,直接传平铺字段会得到 400 "Bad Request"。

9.5 创建发件人(Sender)

发件人(发信身份)在 Web 界面中以 LiveView 表单创建,HTTP POST 无法直达。无浏览器环境时可用 release 自带的 eval 命令调用内部 API 实测创建(注意 bin/keila eval 启动的是不加载应用的裸节点,必须先 ensure_all_started,且执行时需停掉 daemon 释放 4000 端口):

bin/keila stop && sleep 3
export DB_URL="postgres://keila:q1w2e3r4@127.0.0.1:5432/keila"
export SECRET_KEY_BASE="$(cat /opt/keila-0.30.3/b1.env | cut -d= -f2)"
export URL_HOST="192.168.146.131" URL_PORT="8139" URL_SCHEMA="http"
export MAILER_SMTP_HOST="10.89.60.3" MAILER_SMTP_PORT="1025"
export MAILER_SMTP_FROM_EMAIL="newsletter@keila.local"
export MAILER_SMTP_AUTH_METHOD="none" MAILER_SMTP_TLS_MODE="none"
bin/keila eval "Application.ensure_all_started(:keila); {:ok, s} = Keila.Mailings.create_sender(\"np_weLJnLY5\", %{name: \"Newsletter\", from_email: \"newsletter@keila.local\", from_name: \"Keila Demo\", config: %{type: \"smtp\", smtp_relay: \"10.89.60.3\", smtp_port: 1025}}); IO.puts(\"SENDER_ID=\" <> s.id)"
bin/keila daemon && sleep 6

实测回显:

SENDER_ID=nms_weLJnLY5

若漏掉 smtp_relay,changeset 会报 smtp_relay: {"can't be blank", [validation: :required]};若漏掉 Application.ensure_all_started(:keila),会报 could not lookup Ecto repo Keila.Repo because it was not started。

9.6 创建 Campaign 并发送

Campaign 的类型需与内容字段匹配:settings.type: "text" 对应 text_body:

curl -s -X POST -H "Authorization: Bearer lkp0Henf2NsXM8Vehf4R6zKdAE23eGzSRB-ftkwqTJQ" \
  -H "Content-Type: application/json" \
  -d '{"data":{"subject":"Keila B1 验证邮件","text_body":"Hello from Keila 0.30.3 built from source!","sender_id":"nms_weLJnLY5","settings":{"type":"text"}}}' \
  http://192.168.146.131:8139/api/v1/campaigns

实测回显:

{"data":{"data":null,"id":"nmc_weLJnLY5","subject":"Keila B1 验证邮件","settings":{"type":"text"},"inserted_at":"2026-10-26T06:32:39Z","updated_at":"2026-10-26T06:32:39Z","html_content":null,"text_body":"Hello from Keila 0.30.3 built from source!","html_body":null,"sender_id":"nms_weLJnLY5","template_id":null,"sent_at":null,"scheduled_for":null,"mjml_body":null,"json_body":null,"mjml_content":null,"text_content":null,"preview_text":null,"segment_id":null}}

触发投递(异步,经 Oban 队列发送):

curl -s -i -X POST -H "Authorization: Bearer lkp0Henf2NsXM8Vehf4R6zKdAE23eGzSRB-ftkwqTJQ" \
  -H "Content-Type: application/json" \
  http://192.168.146.131:8139/api/v1/campaigns/nmc_weLJnLY5/actions/send

实测回显(HTTP 头 + 响应体):

HTTP/1.1 202 Accepted
content-type: application/json; charset=utf-8

{"data":{"campaign_id":"nmc_weLJnLY5","delivery_queued":true}}

9.7 验证测试邮件送达

在收信端查询(本文实验环境为 Mailpit 的 HTTP API):

curl -s http://192.168.146.131:8138/api/v1/messages \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('total:', d['total']); [print(m['From']['Address'], '->', [a['Address'] for a in m['To']], '|', m['Subject']) for m in d['messages'][:5]]"

实测回显:

total: 2
newsletter@keila.local -> ['alice@example.com'] | Keila B1 验证邮件
newsletter@keila.local -> ['bob@example.com'] | Keila B1 验证邮件

两名联系人均收到由 newsletter@keila.local 发出的测试邮件——源码构建的 Keila 0.30.3 全链路(PostgreSQL 存储 → Oban 投递队列 → SMTP 发信)验证通过。

十、生产部署配置建议

实验通过后,生产化只需补齐三件事:

  1. 固定环境变量:把上述 export 集合写入 /etc/keila.env(chmod 600),由 systemd 加载。下面是一个可直接参考的 unit 示例(bin/keila daemon 自身已带守护与日志,systemd 只负责开机拉起与崩溃重启):
[Unit]
Description=Keila Newsletter
After=network.target postgresql.service

[Service]
Type=forking
User=keila
EnvironmentFile=/etc/keila.env
WorkingDirectory=/opt/keila-0.30.3/_build/prod/rel/keila
ExecStart=/opt/keila-0.30.3/_build/prod/rel/keila/bin/keila daemon
ExecStop=/opt/keila-0.30.3/_build/prod/rel/keila/bin/keila stop
PIDFile=/opt/keila-0.30.3/_build/prod/rel/keila/keila.pid
Restart=on-failure

[Install]
WantedBy=multi-user.target
  1. HTTPS 入口:URL_SCHEMA=https 并配合 Nginx/Caddy 反代到 127.0.0.1:4000,否则浏览器会话 Cookie 与邮件链接都会以 http 生成;
  2. 真实 SMTP:将 MAILER_SMTP_* 指向生产中继(默认 AUTH_METHOD=auto + TLS_MODE=auto 适配 587 STARTTLS),同时建议配置 CAPTCHA_* 环境变量替换注册页默认的 hCaptcha 测试配置(不配置时 Keila 使用 hCaptcha staging 密钥,注册验证码形同虚设,详见配置详解篇)。

十一、常见问题

11.1 mix deps.compile 阶段 fast_html 报 Error 127

cmake: not found。apt-get install -y cmake 后重跑即可,无需清理已编译依赖。

11.2 mix esbuild 报 @codemirror/state 无法解析

报错形如 import { EditorState } from "@codemirror/state" ... You can mark the path "@codemirror/state" as external,共 39 个错误。根因是 assets/node_modules 不存在或残缺——npm ci 失败后继续执行就会这样。先 npm install --prefix assets 再 mix assets.deploy。

11.3 npm ci 报 package.json 与 lock 不同步

Debian 12 的 npm 9.2.0 对 Keila 0.30.3 的 lock 文件(lockfileVersion 2)执行严格一致性校验会失败。实测 npm install --prefix assets 可正常完成安装。

11.4 首次启动后不知道管理员密码

KEILA_USER/KEILA_PASSWORD 未设置时,种子创建 root@localhost 并随机生成密码(启动日志会打印一行随机密码)。若已错过,最简单的办法是清空数据库重来:dropdb + createdb 后设置好环境变量再启动。

11.5 注册/创建用户报密码错误

Keila 强制密码 ≥ 10 字符,q1w2e3r4 这类 8 位口令不通过。注册页(Web 界面自助注册)同样受此限制,且默认挂着 hCaptcha 验证码(staging 配置下任意 response 可通过,生产必须配置 CAPTCHA_*)。

11.6 API 返回 400 "Bad Request"

请求体必须用 {"data": {...}} 包裹业务字段,直接平铺会 400。另外确认带了 Authorization: Bearer <api_key> 与 Content-Type: application/json。

11.7 bin/keila eval 报 Repo not started

eval 启动的裸节点不会自动启动应用,需在表达式开头加 Application.ensure_all_started(:keila),且执行期间 daemon 必须处于停止状态(否则 Endpoint 抢占 4000 端口导致应用启动失败)。

11.8 启动日志出现 latin1 编码警告

warning: the VM is running with native name encoding of latin1 which may cause Elixir to malfunction。设置 export LANG=C.UTF-8 LC_ALL=C.UTF-8 后启动即可消除。

十二、卸载清理

# 停止服务(systemd 场景)
systemctl stop keila && systemctl disable keila

# 删除 release 产物与源码
rm -rf /opt/keila-0.30.3 /opt/downloads

# 删除数据库(先备份再操作)
su postgres -c "psql -c \"DROP DATABASE keila;\""
su postgres -c "psql -c \"DROP USER keila;\""

# 如需彻底移除 OTP 与 Elixir
rm -rf /usr/local/otp-27 /usr/local/elixir-1.18.4
rm -f /usr/local/bin/{erl,escript,elixir,mix}

十三、参考链接