From c71ecfd2bf7976a04e0980ac0b5cf2e8c2e3a5ed Mon Sep 17 00:00:00 2001
From: okxlin <61420215+okxlin@users.noreply.github.com>
Date: Thu, 16 Jul 2026 03:21:02 +0800
Subject: [PATCH 1/2] Add Keila app
Package official Keila 0.30.2 and latest images with the verified 1Panel PostgreSQL Runtime selector. Fresh install, administrator workflow, SMTP delivery, persistence, restart, uninstall, and native upgrade were validated on moelin/1panel:v2.
---
apps/keila/0.30.2/.env.sample | 23 ++
apps/keila/0.30.2/data.yml | 401 +++++++++++++++++++++++++
apps/keila/0.30.2/docker-compose.yml | 44 +++
apps/keila/0.30.2/scripts/init.sh | 74 +++++
apps/keila/0.30.2/scripts/uninstall.sh | 8 +
apps/keila/0.30.2/scripts/upgrade.sh | 5 +
apps/keila/README.md | 74 +++++
apps/keila/data.yml | 30 ++
apps/keila/latest/.env.sample | 23 ++
apps/keila/latest/data.yml | 401 +++++++++++++++++++++++++
apps/keila/latest/docker-compose.yml | 44 +++
apps/keila/latest/scripts/init.sh | 74 +++++
apps/keila/latest/scripts/uninstall.sh | 8 +
apps/keila/latest/scripts/upgrade.sh | 5 +
apps/keila/logo.png | Bin 0 -> 1908 bytes
15 files changed, 1214 insertions(+)
create mode 100644 apps/keila/0.30.2/.env.sample
create mode 100644 apps/keila/0.30.2/data.yml
create mode 100644 apps/keila/0.30.2/docker-compose.yml
create mode 100755 apps/keila/0.30.2/scripts/init.sh
create mode 100755 apps/keila/0.30.2/scripts/uninstall.sh
create mode 100755 apps/keila/0.30.2/scripts/upgrade.sh
create mode 100644 apps/keila/README.md
create mode 100644 apps/keila/data.yml
create mode 100644 apps/keila/latest/.env.sample
create mode 100644 apps/keila/latest/data.yml
create mode 100644 apps/keila/latest/docker-compose.yml
create mode 100755 apps/keila/latest/scripts/init.sh
create mode 100755 apps/keila/latest/scripts/uninstall.sh
create mode 100755 apps/keila/latest/scripts/upgrade.sh
create mode 100644 apps/keila/logo.png
diff --git a/apps/keila/0.30.2/.env.sample b/apps/keila/0.30.2/.env.sample
new file mode 100644
index 000000000..4a20393b2
--- /dev/null
+++ b/apps/keila/0.30.2/.env.sample
@@ -0,0 +1,23 @@
+PANEL_APP_PORT_HTTP=4000
+PANEL_DB_HOST=
+PANEL_DB_PORT=5432
+PANEL_DB_NAME=keila
+PANEL_DB_USER=keila
+PANEL_DB_USER_PASSWORD=
+KEILA_USER=root@localhost
+KEILA_PASSWORD=
+SECRET_KEY_BASE=
+URL_HOST=localhost
+URL_SCHEMA=http
+URL_PORT=4000
+APP_DATA_DIR=./data
+MAILER_SMTP_HOST=
+MAILER_SMTP_PORT=587
+MAILER_SMTP_FROM_EMAIL=
+MAILER_SMTP_USER=
+MAILER_SMTP_PASSWORD=
+MAILER_ENABLE_SSL=false
+MAILER_ENABLE_STARTTLS=false
+LOG_LEVEL=info
+TZ=Asia/Shanghai
+CONTAINER_NAME=
diff --git a/apps/keila/0.30.2/data.yml b/apps/keila/0.30.2/data.yml
new file mode 100644
index 000000000..c50c52713
--- /dev/null
+++ b/apps/keila/0.30.2/data.yml
@@ -0,0 +1,401 @@
+additionalProperties:
+ formFields:
+ - default: 4000
+ edit: true
+ envKey: PANEL_APP_PORT_HTTP
+ labelEn: HTTP Port
+ labelZh: HTTP 端口
+ label:
+ en: HTTP Port
+ zh: HTTP 端口
+ zh-Hant: HTTP 連接埠
+ ja: HTTP ポート
+ ko: HTTP 포트
+ ru: HTTP-порт
+ ms: Port HTTP
+ pt-br: Porta HTTP
+ required: true
+ rule: paramPort
+ type: number
+ - default: postgresql
+ envKey: PANEL_DB_TYPE
+ labelEn: Database Runtime
+ labelZh: 数据库 Runtime
+ label:
+ en: Database Runtime
+ zh: 数据库 Runtime
+ zh-Hant: 資料庫 Runtime
+ ja: データベース Runtime
+ ko: 데이터베이스 Runtime
+ ru: Среда базы данных
+ ms: Runtime Pangkalan Data
+ pt-br: Runtime de banco de dados
+ required: true
+ type: apps
+ values:
+ - label: PostgreSQL
+ value: postgresql
+ child:
+ default: ''
+ envKey: PANEL_DB_HOST
+ labelEn: PostgreSQL Service
+ labelZh: PostgreSQL 服务
+ label:
+ en: PostgreSQL Service
+ zh: PostgreSQL 服务
+ zh-Hant: PostgreSQL 服務
+ ja: PostgreSQL サービス
+ ko: PostgreSQL 서비스
+ ru: Сервис PostgreSQL
+ ms: Perkhidmatan PostgreSQL
+ pt-br: Serviço PostgreSQL
+ required: true
+ type: service
+ - default: 5432
+ edit: true
+ envKey: PANEL_DB_PORT
+ labelEn: Database Port
+ labelZh: 数据库端口
+ label:
+ en: Database Port
+ zh: 数据库端口
+ zh-Hant: 資料庫連接埠
+ ja: データベースポート
+ ko: 데이터베이스 포트
+ ru: Порт базы данных
+ ms: Port Pangkalan Data
+ pt-br: Porta do banco de dados
+ required: true
+ rule: paramPort
+ type: number
+ - default: keila
+ edit: true
+ envKey: PANEL_DB_NAME
+ labelEn: Database Name
+ labelZh: 数据库名
+ label:
+ en: Database Name
+ zh: 数据库名
+ zh-Hant: 資料庫名稱
+ ja: データベース名
+ ko: 데이터베이스 이름
+ ru: Имя базы данных
+ ms: Nama Pangkalan Data
+ pt-br: Nome do banco de dados
+ random: true
+ required: true
+ rule: paramCommon
+ type: text
+ - default: keila
+ edit: true
+ envKey: PANEL_DB_USER
+ labelEn: Database User
+ labelZh: 数据库用户
+ label:
+ en: Database User
+ zh: 数据库用户
+ zh-Hant: 資料庫使用者
+ ja: データベースユーザー
+ ko: 데이터베이스 사용자
+ ru: Пользователь базы данных
+ ms: Pengguna Pangkalan Data
+ pt-br: Usuário do banco de dados
+ random: true
+ required: true
+ rule: paramCommon
+ type: text
+ - default: ''
+ edit: true
+ envKey: PANEL_DB_USER_PASSWORD
+ labelEn: Database Password
+ labelZh: 数据库密码
+ label:
+ en: Database Password
+ zh: 数据库密码
+ zh-Hant: 資料庫密碼
+ ja: データベースパスワード
+ ko: 데이터베이스 비밀번호
+ ru: Пароль базы данных
+ ms: Kata Laluan Pangkalan Data
+ pt-br: Senha do banco de dados
+ required: true
+ rule: paramComplexity
+ type: password
+ - default: root@localhost
+ edit: true
+ envKey: KEILA_USER
+ labelEn: Initial Administrator Email
+ labelZh: 初始管理员邮箱
+ label:
+ en: Initial Administrator Email
+ zh: 初始管理员邮箱
+ zh-Hant: 初始管理員電子郵件
+ ja: 初期管理者メールアドレス
+ ko: 초기 관리자 이메일
+ ru: Email начального администратора
+ ms: E-mel Pentadbir Awal
+ pt-br: E-mail do administrador inicial
+ required: true
+ type: text
+ - default: ''
+ edit: true
+ envKey: KEILA_PASSWORD
+ labelEn: Initial Administrator Password (10+ characters)
+ labelZh: 初始管理员密码(至少 10 个字符)
+ label:
+ en: Initial Administrator Password (10+ characters)
+ zh: 初始管理员密码(至少 10 个字符)
+ zh-Hant: 初始管理員密碼(至少 10 個字元)
+ ja: 初期管理者パスワード(10 文字以上)
+ ko: 초기 관리자 비밀번호(10자 이상)
+ ru: Пароль начального администратора (не менее 10 символов)
+ ms: Kata Laluan Pentadbir Awal (10+ aksara)
+ pt-br: Senha do administrador inicial (10+ caracteres)
+ required: true
+ type: password
+ - default: ''
+ edit: true
+ envKey: SECRET_KEY_BASE
+ labelEn: Session Secret (64+ characters)
+ labelZh: 会话密钥(至少 64 个字符)
+ label:
+ en: Session Secret (64+ characters)
+ zh: 会话密钥(至少 64 个字符)
+ zh-Hant: 工作階段密鑰(至少 64 個字元)
+ ja: セッションシークレット(64 文字以上)
+ ko: 세션 비밀 키(64자 이상)
+ ru: Секрет сеанса (не менее 64 символов)
+ ms: Rahsia Sesi (64+ aksara)
+ pt-br: Segredo da sessão (64+ caracteres)
+ required: true
+ type: password
+ - default: ''
+ edit: true
+ envKey: URL_HOST
+ labelEn: Public Host (domain or IP)
+ labelZh: 公开访问主机(域名或 IP)
+ label:
+ en: Public Host (domain or IP)
+ zh: 公开访问主机(域名或 IP)
+ zh-Hant: 公開存取主機(網域或 IP)
+ ja: 公開ホスト(ドメインまたは IP)
+ ko: 공개 호스트(도메인 또는 IP)
+ ru: Публичный хост (домен или IP)
+ ms: Hos Awam (domain atau IP)
+ pt-br: Host público (domínio ou IP)
+ required: true
+ type: text
+ - default: http
+ edit: true
+ envKey: URL_SCHEMA
+ labelEn: Public URL Scheme
+ labelZh: 公开 URL 协议
+ label:
+ en: Public URL Scheme
+ zh: 公开 URL 协议
+ zh-Hant: 公開 URL 通訊協定
+ ja: 公開 URL スキーム
+ ko: 공개 URL 스키마
+ ru: Схема публичного URL
+ ms: Skema URL Awam
+ pt-br: Esquema da URL pública
+ required: true
+ type: select
+ values:
+ - label: http
+ value: http
+ - label: https
+ value: https
+ - default: 4000
+ edit: true
+ envKey: URL_PORT
+ labelEn: Public URL Port
+ labelZh: 公开 URL 端口
+ label:
+ en: Public URL Port
+ zh: 公开 URL 端口
+ zh-Hant: 公開 URL 連接埠
+ ja: 公開 URL ポート
+ ko: 공개 URL 포트
+ ru: Порт публичного URL
+ ms: Port URL Awam
+ pt-br: Porta da URL pública
+ required: true
+ type: number
+ - default: ./data
+ edit: true
+ envKey: APP_DATA_DIR
+ labelEn: Data Directory
+ labelZh: 数据目录
+ label:
+ en: Data Directory
+ zh: 数据目录
+ zh-Hant: 資料目錄
+ ja: データディレクトリ
+ ko: 데이터 디렉터리
+ ru: Каталог данных
+ ms: Direktori Data
+ pt-br: Diretório de dados
+ required: true
+ type: text
+ - default: ''
+ edit: true
+ envKey: MAILER_SMTP_HOST
+ labelEn: SMTP Host
+ labelZh: SMTP 主机
+ label:
+ en: SMTP Host
+ zh: SMTP 主机
+ zh-Hant: SMTP 主機
+ ja: SMTP ホスト
+ ko: SMTP 호스트
+ ru: SMTP-хост
+ ms: Hos SMTP
+ pt-br: Host SMTP
+ required: true
+ type: text
+ - default: 587
+ edit: true
+ envKey: MAILER_SMTP_PORT
+ labelEn: SMTP Port
+ labelZh: SMTP 端口
+ label:
+ en: SMTP Port
+ zh: SMTP 端口
+ zh-Hant: SMTP 連接埠
+ ja: SMTP ポート
+ ko: SMTP 포트
+ ru: SMTP-порт
+ ms: Port SMTP
+ pt-br: Porta SMTP
+ required: true
+ type: number
+ - default: ''
+ edit: true
+ envKey: MAILER_SMTP_FROM_EMAIL
+ labelEn: SMTP From Email
+ labelZh: SMTP 发件邮箱
+ label:
+ en: SMTP From Email
+ zh: SMTP 发件邮箱
+ zh-Hant: SMTP 寄件者電子郵件
+ ja: SMTP 送信元メールアドレス
+ ko: SMTP 발신 이메일
+ ru: Email отправителя SMTP
+ ms: E-mel Pengirim SMTP
+ pt-br: E-mail do remetente SMTP
+ required: true
+ type: text
+ - default: ''
+ edit: true
+ envKey: MAILER_SMTP_USER
+ labelEn: SMTP Username
+ labelZh: SMTP 用户名
+ label:
+ en: SMTP Username
+ zh: SMTP 用户名
+ zh-Hant: SMTP 使用者名稱
+ ja: SMTP ユーザー名
+ ko: SMTP 사용자 이름
+ ru: Имя пользователя SMTP
+ ms: Nama Pengguna SMTP
+ pt-br: Usuário SMTP
+ required: true
+ type: text
+ - default: ''
+ edit: true
+ envKey: MAILER_SMTP_PASSWORD
+ labelEn: SMTP Password
+ labelZh: SMTP 密码
+ label:
+ en: SMTP Password
+ zh: SMTP 密码
+ zh-Hant: SMTP 密碼
+ ja: SMTP パスワード
+ ko: SMTP 비밀번호
+ ru: Пароль SMTP
+ ms: Kata Laluan SMTP
+ pt-br: Senha SMTP
+ required: true
+ type: password
+ - default: 'false'
+ edit: true
+ envKey: MAILER_ENABLE_SSL
+ labelEn: Enable SMTP SSL/TLS
+ labelZh: 启用 SMTP SSL/TLS
+ label:
+ en: Enable SMTP SSL/TLS
+ zh: 启用 SMTP SSL/TLS
+ zh-Hant: 啟用 SMTP SSL/TLS
+ ja: SMTP SSL/TLS を有効化
+ ko: SMTP SSL/TLS 활성화
+ ru: Включить SMTP SSL/TLS
+ ms: Dayakan SMTP SSL/TLS
+ pt-br: Ativar SMTP SSL/TLS
+ required: true
+ type: select
+ values:
+ - label: 'false'
+ value: 'false'
+ - label: 'true'
+ value: 'true'
+ - default: 'false'
+ edit: true
+ envKey: MAILER_ENABLE_STARTTLS
+ labelEn: Enable SMTP STARTTLS
+ labelZh: 启用 SMTP STARTTLS
+ label:
+ en: Enable SMTP STARTTLS
+ zh: 启用 SMTP STARTTLS
+ zh-Hant: 啟用 SMTP STARTTLS
+ ja: SMTP STARTTLS を有効化
+ ko: SMTP STARTTLS 활성화
+ ru: Включить SMTP STARTTLS
+ ms: Dayakan SMTP STARTTLS
+ pt-br: Ativar SMTP STARTTLS
+ required: true
+ type: select
+ values:
+ - label: 'false'
+ value: 'false'
+ - label: 'true'
+ value: 'true'
+ - default: info
+ edit: true
+ envKey: LOG_LEVEL
+ labelEn: Log Level
+ labelZh: 日志级别
+ label:
+ en: Log Level
+ zh: 日志级别
+ zh-Hant: 日誌層級
+ ja: ログレベル
+ ko: 로그 수준
+ ru: Уровень журналирования
+ ms: Tahap Log
+ pt-br: Nível de log
+ required: true
+ type: select
+ values:
+ - label: debug
+ value: debug
+ - label: info
+ value: info
+ - label: error
+ value: error
+ - default: Asia/Shanghai
+ edit: true
+ envKey: TZ
+ labelEn: Timezone
+ labelZh: 时区
+ label:
+ en: Timezone
+ zh: 时区
+ zh-Hant: 時區
+ ja: タイムゾーン
+ ko: 시간대
+ ru: Часовой пояс
+ ms: Zon Waktu
+ pt-br: Fuso horário
+ required: true
+ type: text
diff --git a/apps/keila/0.30.2/docker-compose.yml b/apps/keila/0.30.2/docker-compose.yml
new file mode 100644
index 000000000..c77c6f9c6
--- /dev/null
+++ b/apps/keila/0.30.2/docker-compose.yml
@@ -0,0 +1,44 @@
+services:
+ keila:
+ image: "pentacent/keila:0.30.2"
+ container_name: ${CONTAINER_NAME}
+ restart: unless-stopped
+ networks:
+ - 1panel-network
+ ports:
+ - "${PANEL_APP_PORT_HTTP}:4000"
+ environment:
+ - DB_URL=postgres://${PANEL_DB_USER}:${PANEL_DB_USER_PASSWORD}@${PANEL_DB_HOST}:${PANEL_DB_PORT}/${PANEL_DB_NAME}
+ - DB_ENABLE_SSL=false
+ - KEILA_USER=${KEILA_USER}
+ - KEILA_PASSWORD=${KEILA_PASSWORD}
+ - SECRET_KEY_BASE=${SECRET_KEY_BASE}
+ - URL_HOST=${URL_HOST}
+ - URL_SCHEMA=${URL_SCHEMA}
+ - URL_PORT=${URL_PORT}
+ - USER_CONTENT_DIR=/opt/app/uploads
+ - MAILER_SMTP_HOST=${MAILER_SMTP_HOST}
+ - MAILER_SMTP_PORT=${MAILER_SMTP_PORT}
+ - MAILER_SMTP_FROM_EMAIL=${MAILER_SMTP_FROM_EMAIL}
+ - MAILER_SMTP_USER=${MAILER_SMTP_USER}
+ - MAILER_SMTP_PASSWORD=${MAILER_SMTP_PASSWORD}
+ - MAILER_ENABLE_SSL=${MAILER_ENABLE_SSL}
+ - MAILER_ENABLE_STARTTLS=${MAILER_ENABLE_STARTTLS}
+ - LOG_LEVEL=${LOG_LEVEL}
+ - TZ=${TZ}
+ volumes:
+ - "${APP_DATA_DIR}/uploads:/opt/app/uploads"
+ healthcheck:
+ test:
+ - CMD-SHELL
+ - wget --quiet --tries=1 --spider http://127.0.0.1:4000/auth/login || exit 1
+ interval: 30s
+ timeout: 10s
+ start_period: 90s
+ retries: 5
+ labels:
+ createdBy: "Apps"
+
+networks:
+ 1panel-network:
+ external: true
diff --git a/apps/keila/0.30.2/scripts/init.sh b/apps/keila/0.30.2/scripts/init.sh
new file mode 100755
index 000000000..80962de32
--- /dev/null
+++ b/apps/keila/0.30.2/scripts/init.sh
@@ -0,0 +1,74 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
+ENV_FILE="${ENV_FILE:-${ROOT_DIR}/.env}"
+
+fail() {
+ printf '%s\n' "$1" >&2
+ exit 1
+}
+
+strip_matching_quotes() {
+ local value="$1"
+
+ if [[ ${#value} -ge 2 ]]; then
+ if [[ "${value:0:1}" == '"' && "${value: -1}" == '"' ]]; then
+ value="${value:1:${#value}-2}"
+ elif [[ "${value:0:1}" == "'" && "${value: -1}" == "'" ]]; then
+ value="${value:1:${#value}-2}"
+ fi
+ fi
+ printf '%s\n' "$value"
+}
+
+read_env_value() {
+ local key="$1"
+ local value=""
+
+ if [[ -f "$ENV_FILE" ]]; then
+ value="$(grep -E "^${key}=" "$ENV_FILE" | tail -n 1 | cut -d '=' -f 2- || true)"
+ fi
+ strip_matching_quotes "$value"
+}
+
+path_is_dotenv_safe() {
+ local value="$1"
+
+ case "$value" in
+ *$'\n'* | *$'\r'* | *\\* | *'$'* | *'#'* | *'"'* | *"'"*) return 1 ;;
+ *) return 0 ;;
+ esac
+}
+
+if [[ ${APP_DATA_DIR+x} ]]; then
+ APP_DATA_DIR_RAW="$APP_DATA_DIR"
+else
+ APP_DATA_DIR_RAW="$(read_env_value APP_DATA_DIR)"
+fi
+APP_DATA_DIR_RAW="$(strip_matching_quotes "${APP_DATA_DIR_RAW:-./data}")"
+
+[[ -n "$APP_DATA_DIR_RAW" ]] || fail "APP_DATA_DIR must not be empty"
+path_is_dotenv_safe "$APP_DATA_DIR_RAW" || fail "APP_DATA_DIR contains unsupported dotenv characters"
+
+case "$APP_DATA_DIR_RAW" in
+ /*)
+ APP_DATA_DIR_ABS="$(realpath -m -- "$APP_DATA_DIR_RAW")"
+ ;;
+ *)
+ APP_DATA_DIR_ABS="$(realpath -m -- "${ROOT_DIR}/${APP_DATA_DIR_RAW#./}")"
+ case "$APP_DATA_DIR_ABS" in
+ "${ROOT_DIR}" | "${ROOT_DIR}"/*) ;;
+ *) fail "Relative APP_DATA_DIR must stay inside the application directory" ;;
+ esac
+ ;;
+esac
+
+[[ "$APP_DATA_DIR_ABS" != "/" ]] || fail "APP_DATA_DIR must not be the filesystem root"
+[[ ! -L "$APP_DATA_DIR_ABS" ]] || fail "APP_DATA_DIR must not be a symbolic link"
+if [[ -e "$APP_DATA_DIR_ABS" && ! -d "$APP_DATA_DIR_ABS" ]]; then
+ fail "APP_DATA_DIR must be a directory"
+fi
+
+mkdir -p -- "${APP_DATA_DIR_ABS}/uploads"
+chmod 750 -- "$APP_DATA_DIR_ABS" "${APP_DATA_DIR_ABS}/uploads"
diff --git a/apps/keila/0.30.2/scripts/uninstall.sh b/apps/keila/0.30.2/scripts/uninstall.sh
new file mode 100755
index 000000000..5a3f6bd6b
--- /dev/null
+++ b/apps/keila/0.30.2/scripts/uninstall.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+if command -v docker-compose >/dev/null 2>&1; then
+ docker-compose down --volumes --remove-orphans
+else
+ docker compose down --volumes --remove-orphans
+fi
diff --git a/apps/keila/0.30.2/scripts/upgrade.sh b/apps/keila/0.30.2/scripts/upgrade.sh
new file mode 100755
index 000000000..593b31667
--- /dev/null
+++ b/apps/keila/0.30.2/scripts/upgrade.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
+exec bash "${SCRIPT_DIR}/init.sh"
diff --git a/apps/keila/README.md b/apps/keila/README.md
new file mode 100644
index 000000000..589ba7d17
--- /dev/null
+++ b/apps/keila/README.md
@@ -0,0 +1,74 @@
+# Keila
+
+## 产品介绍
+
+Keila 是一款开源的自托管新闻简报与邮件营销平台,可用于管理联系人、订阅表单、邮件模板和营销活动,并支持 SMTP、AWS SES、SendGrid、Mailgun 与 Postmark 等发送渠道。
+
+## 主要功能
+
+- 管理联系人、分组、订阅表单、模板和新闻简报活动。
+- 提供可视化、Markdown 和纯文本邮件编辑能力。
+- 支持公开订阅页面、活动统计和 API。
+- 业务数据保存在 PostgreSQL,本地上传文件保存在独立持久化目录。
+
+## 部署拓扑
+
+- 本应用包只运行一个 Keila service,使用上游官方 `pentacent/keila` 镜像。
+- 安装时必须选择同一 1Panel 中已运行的 PostgreSQL Runtime;1Panel 会为 Keila 创建独立数据库和独立用户。
+- 上游示例 Compose 中的 PostgreSQL service 已替换为 1Panel Runtime 联动,不会在应用包中重复部署数据库。
+- SMTP 是生产启动的必填依赖,但属于用户提供的外部服务,不会作为 sidecar 安装。
+
+## 访问说明
+
+默认 Web 端口为 `4000`。安装后访问 `http://服务器地址:安装端口/auth/login`,使用安装表单中的初始管理员邮箱和密码登录。
+
+- `公开访问主机` 只填写域名或 IP,不包含 `http://`、`https://` 或路径。
+- 直接按端口访问时,公开 URL 端口通常与 Web 端口相同。
+- 通过反向代理发布 HTTPS 时,公开 URL 协议应设为 `https`,公开 URL 端口通常设为 `443`;反向代理必须正确转发 WebSocket。
+- 初始管理员只在空数据库第一次启动时创建。后续修改安装参数不会重置已有管理员密码,请在 Keila 中管理账号。
+- 上游要求管理员密码至少 10 个字符;会话密钥应为至少 64 个字符的强随机值。
+
+## SMTP
+
+Keila 在生产模式启动时必须配置系统 SMTP,用于注册、激活和密码重置等邮件。
+
+- SMTP 用户名在上游可回退为发件邮箱;本应用包要求显式填写,以兼容用户名与发件邮箱不同的服务。
+- 端口 `587` 通常配合 STARTTLS,端口 `465` 通常配合 SSL/TLS。具体组合以邮件服务商文档为准。
+- SMTP 密码、数据库密码、管理员密码和会话密钥都会写入 1Panel 生成的运行时 `.env`,不得提交到 Git 或复制到公开日志。
+
+## 数据、备份与升级
+
+- Keila 会在启动时自动执行 PostgreSQL schema migration,并在空数据库中创建初始 root 用户。
+- 联系人、活动、模板、账号和任务队列数据位于所选 PostgreSQL Runtime 的独立数据库中。
+- 用户上传文件位于 `${APP_DATA_DIR}/uploads`,容器内路径为 `/opt/app/uploads`。
+- 升级前必须同时备份 PostgreSQL 数据库和 `${APP_DATA_DIR}/uploads`。升级期间等待数据库 migration 完成后再停止或重启容器。
+- 固定版和 `latest` 使用相同的持久化边界;回滚时应恢复与目标版本匹配的升级前数据库及上传文件备份。
+- 卸载应用不会删除所选 PostgreSQL Runtime 本身;是否删除 Keila 的独立数据库和用户取决于 1Panel 卸载选项。卸载前请完成备份。
+
+## 安全与许可证
+
+- Compose 不启用 privileged、host network、额外 capability、设备映射或 Docker Socket。
+- 建议通过 1Panel 反向代理启用 HTTPS,并限制管理入口的公网暴露范围。
+- Keila 源代码采用 GNU Affero General Public License v3.0 或更高版本。部署、修改或向网络用户提供服务时,请遵守上游许可证义务。
+- 上游 README 说明 Keila 标志不包含在 AGPL 授权中;标志权利归其权利人,本应用包仅将其用于识别对应产品。
+
+## 参考资料
+
+- 官网:
+- 官方安装文档:
+- 官方配置文档:
+- 官方首次启动文档:
+- 源码与官方 Compose:
+
+## Introduction
+
+Keila is an open-source self-hosted newsletter and email marketing platform. This package runs one official Keila container, links it to a selectable PostgreSQL Runtime managed by 1Panel, and persists local uploads separately.
+
+Production startup requires SMTP configuration. Keila automatically runs database migrations and creates the initial root user on an empty database. Back up both PostgreSQL and `${APP_DATA_DIR}/uploads` before upgrades.
+
+## Features
+
+- Contact, segment, subscription-form, template, and newsletter campaign management.
+- Visual, Markdown, and plain-text email editing.
+- PostgreSQL-backed application data and a persistent local upload directory.
+- Explicit initial administrator, public URL, SMTP, and session-secret configuration.
diff --git a/apps/keila/data.yml b/apps/keila/data.yml
new file mode 100644
index 000000000..58b75a554
--- /dev/null
+++ b/apps/keila/data.yml
@@ -0,0 +1,30 @@
+name: Keila
+tags:
+ - Email
+title: 开源的自托管新闻简报与邮件营销平台
+description: 开源的自托管新闻简报与邮件营销平台
+additionalProperties:
+ key: keila
+ name: Keila
+ tags:
+ - Email
+ type: tool
+ website: https://www.keila.io/
+ document: https://www.keila.io/docs/installation
+ github: https://github.com/pentacent/keila
+ shortDescZh: 开源的自托管新闻简报与邮件营销平台
+ shortDescEn: An open-source self-hosted newsletter and email marketing platform
+ crossVersionUpdate: true
+ limit: 0
+ recommend: 0
+ architectures:
+ - amd64
+ description:
+ en: An open-source self-hosted newsletter and email marketing platform
+ zh: 开源的自托管新闻简报与邮件营销平台
+ zh-Hant: 開源的自託管電子報與電子郵件行銷平台
+ ja: オープンソースのセルフホスト型ニュースレター・メールマーケティングプラットフォーム
+ ko: 오픈 소스 자체 호스팅 뉴스레터 및 이메일 마케팅 플랫폼
+ ru: Самостоятельно размещаемая платформа с открытым исходным кодом для рассылок и email-маркетинга
+ ms: Platform sumber terbuka yang dihos sendiri untuk surat berita dan pemasaran e-mel
+ pt-br: Plataforma de código aberto auto-hospedada para newsletters e marketing por e-mail
diff --git a/apps/keila/latest/.env.sample b/apps/keila/latest/.env.sample
new file mode 100644
index 000000000..4a20393b2
--- /dev/null
+++ b/apps/keila/latest/.env.sample
@@ -0,0 +1,23 @@
+PANEL_APP_PORT_HTTP=4000
+PANEL_DB_HOST=
+PANEL_DB_PORT=5432
+PANEL_DB_NAME=keila
+PANEL_DB_USER=keila
+PANEL_DB_USER_PASSWORD=
+KEILA_USER=root@localhost
+KEILA_PASSWORD=
+SECRET_KEY_BASE=
+URL_HOST=localhost
+URL_SCHEMA=http
+URL_PORT=4000
+APP_DATA_DIR=./data
+MAILER_SMTP_HOST=
+MAILER_SMTP_PORT=587
+MAILER_SMTP_FROM_EMAIL=
+MAILER_SMTP_USER=
+MAILER_SMTP_PASSWORD=
+MAILER_ENABLE_SSL=false
+MAILER_ENABLE_STARTTLS=false
+LOG_LEVEL=info
+TZ=Asia/Shanghai
+CONTAINER_NAME=
diff --git a/apps/keila/latest/data.yml b/apps/keila/latest/data.yml
new file mode 100644
index 000000000..c50c52713
--- /dev/null
+++ b/apps/keila/latest/data.yml
@@ -0,0 +1,401 @@
+additionalProperties:
+ formFields:
+ - default: 4000
+ edit: true
+ envKey: PANEL_APP_PORT_HTTP
+ labelEn: HTTP Port
+ labelZh: HTTP 端口
+ label:
+ en: HTTP Port
+ zh: HTTP 端口
+ zh-Hant: HTTP 連接埠
+ ja: HTTP ポート
+ ko: HTTP 포트
+ ru: HTTP-порт
+ ms: Port HTTP
+ pt-br: Porta HTTP
+ required: true
+ rule: paramPort
+ type: number
+ - default: postgresql
+ envKey: PANEL_DB_TYPE
+ labelEn: Database Runtime
+ labelZh: 数据库 Runtime
+ label:
+ en: Database Runtime
+ zh: 数据库 Runtime
+ zh-Hant: 資料庫 Runtime
+ ja: データベース Runtime
+ ko: 데이터베이스 Runtime
+ ru: Среда базы данных
+ ms: Runtime Pangkalan Data
+ pt-br: Runtime de banco de dados
+ required: true
+ type: apps
+ values:
+ - label: PostgreSQL
+ value: postgresql
+ child:
+ default: ''
+ envKey: PANEL_DB_HOST
+ labelEn: PostgreSQL Service
+ labelZh: PostgreSQL 服务
+ label:
+ en: PostgreSQL Service
+ zh: PostgreSQL 服务
+ zh-Hant: PostgreSQL 服務
+ ja: PostgreSQL サービス
+ ko: PostgreSQL 서비스
+ ru: Сервис PostgreSQL
+ ms: Perkhidmatan PostgreSQL
+ pt-br: Serviço PostgreSQL
+ required: true
+ type: service
+ - default: 5432
+ edit: true
+ envKey: PANEL_DB_PORT
+ labelEn: Database Port
+ labelZh: 数据库端口
+ label:
+ en: Database Port
+ zh: 数据库端口
+ zh-Hant: 資料庫連接埠
+ ja: データベースポート
+ ko: 데이터베이스 포트
+ ru: Порт базы данных
+ ms: Port Pangkalan Data
+ pt-br: Porta do banco de dados
+ required: true
+ rule: paramPort
+ type: number
+ - default: keila
+ edit: true
+ envKey: PANEL_DB_NAME
+ labelEn: Database Name
+ labelZh: 数据库名
+ label:
+ en: Database Name
+ zh: 数据库名
+ zh-Hant: 資料庫名稱
+ ja: データベース名
+ ko: 데이터베이스 이름
+ ru: Имя базы данных
+ ms: Nama Pangkalan Data
+ pt-br: Nome do banco de dados
+ random: true
+ required: true
+ rule: paramCommon
+ type: text
+ - default: keila
+ edit: true
+ envKey: PANEL_DB_USER
+ labelEn: Database User
+ labelZh: 数据库用户
+ label:
+ en: Database User
+ zh: 数据库用户
+ zh-Hant: 資料庫使用者
+ ja: データベースユーザー
+ ko: 데이터베이스 사용자
+ ru: Пользователь базы данных
+ ms: Pengguna Pangkalan Data
+ pt-br: Usuário do banco de dados
+ random: true
+ required: true
+ rule: paramCommon
+ type: text
+ - default: ''
+ edit: true
+ envKey: PANEL_DB_USER_PASSWORD
+ labelEn: Database Password
+ labelZh: 数据库密码
+ label:
+ en: Database Password
+ zh: 数据库密码
+ zh-Hant: 資料庫密碼
+ ja: データベースパスワード
+ ko: 데이터베이스 비밀번호
+ ru: Пароль базы данных
+ ms: Kata Laluan Pangkalan Data
+ pt-br: Senha do banco de dados
+ required: true
+ rule: paramComplexity
+ type: password
+ - default: root@localhost
+ edit: true
+ envKey: KEILA_USER
+ labelEn: Initial Administrator Email
+ labelZh: 初始管理员邮箱
+ label:
+ en: Initial Administrator Email
+ zh: 初始管理员邮箱
+ zh-Hant: 初始管理員電子郵件
+ ja: 初期管理者メールアドレス
+ ko: 초기 관리자 이메일
+ ru: Email начального администратора
+ ms: E-mel Pentadbir Awal
+ pt-br: E-mail do administrador inicial
+ required: true
+ type: text
+ - default: ''
+ edit: true
+ envKey: KEILA_PASSWORD
+ labelEn: Initial Administrator Password (10+ characters)
+ labelZh: 初始管理员密码(至少 10 个字符)
+ label:
+ en: Initial Administrator Password (10+ characters)
+ zh: 初始管理员密码(至少 10 个字符)
+ zh-Hant: 初始管理員密碼(至少 10 個字元)
+ ja: 初期管理者パスワード(10 文字以上)
+ ko: 초기 관리자 비밀번호(10자 이상)
+ ru: Пароль начального администратора (не менее 10 символов)
+ ms: Kata Laluan Pentadbir Awal (10+ aksara)
+ pt-br: Senha do administrador inicial (10+ caracteres)
+ required: true
+ type: password
+ - default: ''
+ edit: true
+ envKey: SECRET_KEY_BASE
+ labelEn: Session Secret (64+ characters)
+ labelZh: 会话密钥(至少 64 个字符)
+ label:
+ en: Session Secret (64+ characters)
+ zh: 会话密钥(至少 64 个字符)
+ zh-Hant: 工作階段密鑰(至少 64 個字元)
+ ja: セッションシークレット(64 文字以上)
+ ko: 세션 비밀 키(64자 이상)
+ ru: Секрет сеанса (не менее 64 символов)
+ ms: Rahsia Sesi (64+ aksara)
+ pt-br: Segredo da sessão (64+ caracteres)
+ required: true
+ type: password
+ - default: ''
+ edit: true
+ envKey: URL_HOST
+ labelEn: Public Host (domain or IP)
+ labelZh: 公开访问主机(域名或 IP)
+ label:
+ en: Public Host (domain or IP)
+ zh: 公开访问主机(域名或 IP)
+ zh-Hant: 公開存取主機(網域或 IP)
+ ja: 公開ホスト(ドメインまたは IP)
+ ko: 공개 호스트(도메인 또는 IP)
+ ru: Публичный хост (домен или IP)
+ ms: Hos Awam (domain atau IP)
+ pt-br: Host público (domínio ou IP)
+ required: true
+ type: text
+ - default: http
+ edit: true
+ envKey: URL_SCHEMA
+ labelEn: Public URL Scheme
+ labelZh: 公开 URL 协议
+ label:
+ en: Public URL Scheme
+ zh: 公开 URL 协议
+ zh-Hant: 公開 URL 通訊協定
+ ja: 公開 URL スキーム
+ ko: 공개 URL 스키마
+ ru: Схема публичного URL
+ ms: Skema URL Awam
+ pt-br: Esquema da URL pública
+ required: true
+ type: select
+ values:
+ - label: http
+ value: http
+ - label: https
+ value: https
+ - default: 4000
+ edit: true
+ envKey: URL_PORT
+ labelEn: Public URL Port
+ labelZh: 公开 URL 端口
+ label:
+ en: Public URL Port
+ zh: 公开 URL 端口
+ zh-Hant: 公開 URL 連接埠
+ ja: 公開 URL ポート
+ ko: 공개 URL 포트
+ ru: Порт публичного URL
+ ms: Port URL Awam
+ pt-br: Porta da URL pública
+ required: true
+ type: number
+ - default: ./data
+ edit: true
+ envKey: APP_DATA_DIR
+ labelEn: Data Directory
+ labelZh: 数据目录
+ label:
+ en: Data Directory
+ zh: 数据目录
+ zh-Hant: 資料目錄
+ ja: データディレクトリ
+ ko: 데이터 디렉터리
+ ru: Каталог данных
+ ms: Direktori Data
+ pt-br: Diretório de dados
+ required: true
+ type: text
+ - default: ''
+ edit: true
+ envKey: MAILER_SMTP_HOST
+ labelEn: SMTP Host
+ labelZh: SMTP 主机
+ label:
+ en: SMTP Host
+ zh: SMTP 主机
+ zh-Hant: SMTP 主機
+ ja: SMTP ホスト
+ ko: SMTP 호스트
+ ru: SMTP-хост
+ ms: Hos SMTP
+ pt-br: Host SMTP
+ required: true
+ type: text
+ - default: 587
+ edit: true
+ envKey: MAILER_SMTP_PORT
+ labelEn: SMTP Port
+ labelZh: SMTP 端口
+ label:
+ en: SMTP Port
+ zh: SMTP 端口
+ zh-Hant: SMTP 連接埠
+ ja: SMTP ポート
+ ko: SMTP 포트
+ ru: SMTP-порт
+ ms: Port SMTP
+ pt-br: Porta SMTP
+ required: true
+ type: number
+ - default: ''
+ edit: true
+ envKey: MAILER_SMTP_FROM_EMAIL
+ labelEn: SMTP From Email
+ labelZh: SMTP 发件邮箱
+ label:
+ en: SMTP From Email
+ zh: SMTP 发件邮箱
+ zh-Hant: SMTP 寄件者電子郵件
+ ja: SMTP 送信元メールアドレス
+ ko: SMTP 발신 이메일
+ ru: Email отправителя SMTP
+ ms: E-mel Pengirim SMTP
+ pt-br: E-mail do remetente SMTP
+ required: true
+ type: text
+ - default: ''
+ edit: true
+ envKey: MAILER_SMTP_USER
+ labelEn: SMTP Username
+ labelZh: SMTP 用户名
+ label:
+ en: SMTP Username
+ zh: SMTP 用户名
+ zh-Hant: SMTP 使用者名稱
+ ja: SMTP ユーザー名
+ ko: SMTP 사용자 이름
+ ru: Имя пользователя SMTP
+ ms: Nama Pengguna SMTP
+ pt-br: Usuário SMTP
+ required: true
+ type: text
+ - default: ''
+ edit: true
+ envKey: MAILER_SMTP_PASSWORD
+ labelEn: SMTP Password
+ labelZh: SMTP 密码
+ label:
+ en: SMTP Password
+ zh: SMTP 密码
+ zh-Hant: SMTP 密碼
+ ja: SMTP パスワード
+ ko: SMTP 비밀번호
+ ru: Пароль SMTP
+ ms: Kata Laluan SMTP
+ pt-br: Senha SMTP
+ required: true
+ type: password
+ - default: 'false'
+ edit: true
+ envKey: MAILER_ENABLE_SSL
+ labelEn: Enable SMTP SSL/TLS
+ labelZh: 启用 SMTP SSL/TLS
+ label:
+ en: Enable SMTP SSL/TLS
+ zh: 启用 SMTP SSL/TLS
+ zh-Hant: 啟用 SMTP SSL/TLS
+ ja: SMTP SSL/TLS を有効化
+ ko: SMTP SSL/TLS 활성화
+ ru: Включить SMTP SSL/TLS
+ ms: Dayakan SMTP SSL/TLS
+ pt-br: Ativar SMTP SSL/TLS
+ required: true
+ type: select
+ values:
+ - label: 'false'
+ value: 'false'
+ - label: 'true'
+ value: 'true'
+ - default: 'false'
+ edit: true
+ envKey: MAILER_ENABLE_STARTTLS
+ labelEn: Enable SMTP STARTTLS
+ labelZh: 启用 SMTP STARTTLS
+ label:
+ en: Enable SMTP STARTTLS
+ zh: 启用 SMTP STARTTLS
+ zh-Hant: 啟用 SMTP STARTTLS
+ ja: SMTP STARTTLS を有効化
+ ko: SMTP STARTTLS 활성화
+ ru: Включить SMTP STARTTLS
+ ms: Dayakan SMTP STARTTLS
+ pt-br: Ativar SMTP STARTTLS
+ required: true
+ type: select
+ values:
+ - label: 'false'
+ value: 'false'
+ - label: 'true'
+ value: 'true'
+ - default: info
+ edit: true
+ envKey: LOG_LEVEL
+ labelEn: Log Level
+ labelZh: 日志级别
+ label:
+ en: Log Level
+ zh: 日志级别
+ zh-Hant: 日誌層級
+ ja: ログレベル
+ ko: 로그 수준
+ ru: Уровень журналирования
+ ms: Tahap Log
+ pt-br: Nível de log
+ required: true
+ type: select
+ values:
+ - label: debug
+ value: debug
+ - label: info
+ value: info
+ - label: error
+ value: error
+ - default: Asia/Shanghai
+ edit: true
+ envKey: TZ
+ labelEn: Timezone
+ labelZh: 时区
+ label:
+ en: Timezone
+ zh: 时区
+ zh-Hant: 時區
+ ja: タイムゾーン
+ ko: 시간대
+ ru: Часовой пояс
+ ms: Zon Waktu
+ pt-br: Fuso horário
+ required: true
+ type: text
diff --git a/apps/keila/latest/docker-compose.yml b/apps/keila/latest/docker-compose.yml
new file mode 100644
index 000000000..6da76e933
--- /dev/null
+++ b/apps/keila/latest/docker-compose.yml
@@ -0,0 +1,44 @@
+services:
+ keila:
+ image: "pentacent/keila:latest"
+ container_name: ${CONTAINER_NAME}
+ restart: unless-stopped
+ networks:
+ - 1panel-network
+ ports:
+ - "${PANEL_APP_PORT_HTTP}:4000"
+ environment:
+ - DB_URL=postgres://${PANEL_DB_USER}:${PANEL_DB_USER_PASSWORD}@${PANEL_DB_HOST}:${PANEL_DB_PORT}/${PANEL_DB_NAME}
+ - DB_ENABLE_SSL=false
+ - KEILA_USER=${KEILA_USER}
+ - KEILA_PASSWORD=${KEILA_PASSWORD}
+ - SECRET_KEY_BASE=${SECRET_KEY_BASE}
+ - URL_HOST=${URL_HOST}
+ - URL_SCHEMA=${URL_SCHEMA}
+ - URL_PORT=${URL_PORT}
+ - USER_CONTENT_DIR=/opt/app/uploads
+ - MAILER_SMTP_HOST=${MAILER_SMTP_HOST}
+ - MAILER_SMTP_PORT=${MAILER_SMTP_PORT}
+ - MAILER_SMTP_FROM_EMAIL=${MAILER_SMTP_FROM_EMAIL}
+ - MAILER_SMTP_USER=${MAILER_SMTP_USER}
+ - MAILER_SMTP_PASSWORD=${MAILER_SMTP_PASSWORD}
+ - MAILER_ENABLE_SSL=${MAILER_ENABLE_SSL}
+ - MAILER_ENABLE_STARTTLS=${MAILER_ENABLE_STARTTLS}
+ - LOG_LEVEL=${LOG_LEVEL}
+ - TZ=${TZ}
+ volumes:
+ - "${APP_DATA_DIR}/uploads:/opt/app/uploads"
+ healthcheck:
+ test:
+ - CMD-SHELL
+ - wget --quiet --tries=1 --spider http://127.0.0.1:4000/auth/login || exit 1
+ interval: 30s
+ timeout: 10s
+ start_period: 90s
+ retries: 5
+ labels:
+ createdBy: "Apps"
+
+networks:
+ 1panel-network:
+ external: true
diff --git a/apps/keila/latest/scripts/init.sh b/apps/keila/latest/scripts/init.sh
new file mode 100755
index 000000000..80962de32
--- /dev/null
+++ b/apps/keila/latest/scripts/init.sh
@@ -0,0 +1,74 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
+ENV_FILE="${ENV_FILE:-${ROOT_DIR}/.env}"
+
+fail() {
+ printf '%s\n' "$1" >&2
+ exit 1
+}
+
+strip_matching_quotes() {
+ local value="$1"
+
+ if [[ ${#value} -ge 2 ]]; then
+ if [[ "${value:0:1}" == '"' && "${value: -1}" == '"' ]]; then
+ value="${value:1:${#value}-2}"
+ elif [[ "${value:0:1}" == "'" && "${value: -1}" == "'" ]]; then
+ value="${value:1:${#value}-2}"
+ fi
+ fi
+ printf '%s\n' "$value"
+}
+
+read_env_value() {
+ local key="$1"
+ local value=""
+
+ if [[ -f "$ENV_FILE" ]]; then
+ value="$(grep -E "^${key}=" "$ENV_FILE" | tail -n 1 | cut -d '=' -f 2- || true)"
+ fi
+ strip_matching_quotes "$value"
+}
+
+path_is_dotenv_safe() {
+ local value="$1"
+
+ case "$value" in
+ *$'\n'* | *$'\r'* | *\\* | *'$'* | *'#'* | *'"'* | *"'"*) return 1 ;;
+ *) return 0 ;;
+ esac
+}
+
+if [[ ${APP_DATA_DIR+x} ]]; then
+ APP_DATA_DIR_RAW="$APP_DATA_DIR"
+else
+ APP_DATA_DIR_RAW="$(read_env_value APP_DATA_DIR)"
+fi
+APP_DATA_DIR_RAW="$(strip_matching_quotes "${APP_DATA_DIR_RAW:-./data}")"
+
+[[ -n "$APP_DATA_DIR_RAW" ]] || fail "APP_DATA_DIR must not be empty"
+path_is_dotenv_safe "$APP_DATA_DIR_RAW" || fail "APP_DATA_DIR contains unsupported dotenv characters"
+
+case "$APP_DATA_DIR_RAW" in
+ /*)
+ APP_DATA_DIR_ABS="$(realpath -m -- "$APP_DATA_DIR_RAW")"
+ ;;
+ *)
+ APP_DATA_DIR_ABS="$(realpath -m -- "${ROOT_DIR}/${APP_DATA_DIR_RAW#./}")"
+ case "$APP_DATA_DIR_ABS" in
+ "${ROOT_DIR}" | "${ROOT_DIR}"/*) ;;
+ *) fail "Relative APP_DATA_DIR must stay inside the application directory" ;;
+ esac
+ ;;
+esac
+
+[[ "$APP_DATA_DIR_ABS" != "/" ]] || fail "APP_DATA_DIR must not be the filesystem root"
+[[ ! -L "$APP_DATA_DIR_ABS" ]] || fail "APP_DATA_DIR must not be a symbolic link"
+if [[ -e "$APP_DATA_DIR_ABS" && ! -d "$APP_DATA_DIR_ABS" ]]; then
+ fail "APP_DATA_DIR must be a directory"
+fi
+
+mkdir -p -- "${APP_DATA_DIR_ABS}/uploads"
+chmod 750 -- "$APP_DATA_DIR_ABS" "${APP_DATA_DIR_ABS}/uploads"
diff --git a/apps/keila/latest/scripts/uninstall.sh b/apps/keila/latest/scripts/uninstall.sh
new file mode 100755
index 000000000..5a3f6bd6b
--- /dev/null
+++ b/apps/keila/latest/scripts/uninstall.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+if command -v docker-compose >/dev/null 2>&1; then
+ docker-compose down --volumes --remove-orphans
+else
+ docker compose down --volumes --remove-orphans
+fi
diff --git a/apps/keila/latest/scripts/upgrade.sh b/apps/keila/latest/scripts/upgrade.sh
new file mode 100755
index 000000000..593b31667
--- /dev/null
+++ b/apps/keila/latest/scripts/upgrade.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
+exec bash "${SCRIPT_DIR}/init.sh"
diff --git a/apps/keila/logo.png b/apps/keila/logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..a50106d4d1463f20c6ad1774c6872ccb93b17b68
GIT binary patch
literal 1908
zcmV-)2aEWLP)h<~k
zb%vQPNMf3(ww}e`xYXyYxWrp;_de=Mqr_>yP?M5*yr<~
z#o*KA@yOopw!zHU-{!>I>)Yt_hLxr^PG}i4QAb{ONMCjwI8;escc8e+Vv43pc$jdN
zuifT&G7E#_V{Vo#jfb#
z?fCmy)V~)xR8Z2qAf}Zcp^_FgQ5}ke8fI^my2v5Cq8CG1I@b=;kOxU6A6{rm?@{_4cFT+QHi0H5Jf@0000RbW%=J
z0X1WUveMn{_ZV~WW(!xh|00nSKL_t(|+U=eDR~lCk$FDr(
zxhjf?y-~PDvaQADp)oc-niiDevf-A-nA#?7V_)_~8`F2wv`ruXc7H$-*t>8S)@26w
z`y=e(e9oNN`OeH%Q{WQBdNK$dI%vr2c!Yviq^*W135r0|t@V0}qR>=}cT*&4YO@tT
z7HI!jf`lm=g&Tnm;DHV(_yXMswK9PTdZHfcF^WocHy_6ZJx&NAA&N{PY_}n$ltu|5
zK5WoQTt)h%*+bNJG&dj~E0MJ?X=jlZYccOGxI+tPgK&wlyioVFqNgv%1*~Vkh9%iPhs(M9C
zEgS1mB3bdWX=Wd^dgZDxL$1lO#E9g{!F6UQw7R?^`tG{qSpt!)bs2l1)f=m#1d*(K
zlF)O^W@uHtDfn{1R_J;Ap;e>Jur*tuUpohAfz70Bg-)Lbw9#hLwnDRWfi~I<255uL
zpnz_-8641UHiHDZ)n>3jx7Z9G=q8&%1bt>Rn4lXr0~C5$OnqWAK%tk!&?hzn6#ABG
zMtv17^om)i)#Vki(Ce$F)HhbaLa(piS~9sC)tf+}JIjT3pOw}hhSrX(3`zNZw?T8t5@hZ+(=)5oX*|u
zjrq+%Ru7g#Sf^bZccEQq7utn(p@jpqy7eW;Mn4WMD#+3r(2Ime;%+kGqVICPI?_a+
ze*q(>1%cM}5@U=ViKq`jRF5wO&|3X>z7jxx4WdqeBY>_kQ|fOaIAW1^fEH070)YO`
zkk7h)njVa?ir)TM@E8DeNdV1xt%h%F`W=%-Wt%*;&j2|#n<
zoO8X&Tnt8j4A92g(z{~#@-fhc=`7GjX8sF=a}TWpbcO36TT@Xmxz=d$bmtxd`p%D1
zz3DJApWow_HQj^pU~5fxVH_b?R8^k2`)Qt2<6rjVkHrq`f==}v>R+8dfV5$xQAv*Z
z*uJCQ1&oL4SsTW5$-KUJAou#h!GUPqUlhqP2OS+fm+6w%zde*e{-pBne`{8Uu_7FMJ4*<<0mA{Lqnx3yaWN{2bv!xBLGU<7?xS38
zfO3rlQDTUSw1RLoN)-1J8kFmPa4uFN$}|S%wiQ+=mW2i4!?2c(kQZ?Hcbh!0nLWd^
z)DiOf%s__?t0AD3Z#q6~D`8Sy`I@lT>k^9JN4Y%s!MGE0000
Date: Thu, 16 Jul 2026 09:30:42 +0800
Subject: [PATCH 2/2] fix(keila): enforce valid session secret
---
apps/keila/0.30.2/scripts/init.sh | 37 +++++++++++++++++++++++++++++++
apps/keila/latest/scripts/init.sh | 37 +++++++++++++++++++++++++++++++
2 files changed, 74 insertions(+)
diff --git a/apps/keila/0.30.2/scripts/init.sh b/apps/keila/0.30.2/scripts/init.sh
index 80962de32..116e04e94 100755
--- a/apps/keila/0.30.2/scripts/init.sh
+++ b/apps/keila/0.30.2/scripts/init.sh
@@ -32,6 +32,40 @@ read_env_value() {
strip_matching_quotes "$value"
}
+set_env_value() {
+ local key="$1"
+ local value="$2"
+ local temp_file
+
+ temp_file="$(mktemp "${ENV_FILE}.tmp.XXXXXX")"
+ awk -v key="$key" -v value="$value" '
+ BEGIN { updated = 0 }
+ $0 ~ ("^" key "=") {
+ if (!updated) {
+ print key "=" value
+ updated = 1
+ }
+ next
+ }
+ { print }
+ END {
+ if (!updated) print key "=" value
+ }
+ ' "$ENV_FILE" > "$temp_file"
+ chmod --reference="$ENV_FILE" "$temp_file"
+ mv -f -- "$temp_file" "$ENV_FILE"
+}
+
+ensure_secret_key_base() {
+ local secret_key_base
+
+ secret_key_base="$(read_env_value SECRET_KEY_BASE)"
+ if [[ ${#secret_key_base} -lt 64 ]]; then
+ secret_key_base="$(od -An -N64 -tx1 /dev/urandom | tr -d ' \n')"
+ set_env_value SECRET_KEY_BASE "$secret_key_base"
+ fi
+}
+
path_is_dotenv_safe() {
local value="$1"
@@ -41,6 +75,9 @@ path_is_dotenv_safe() {
esac
}
+[[ -f "$ENV_FILE" ]] || fail "Environment file not found: ${ENV_FILE}"
+ensure_secret_key_base
+
if [[ ${APP_DATA_DIR+x} ]]; then
APP_DATA_DIR_RAW="$APP_DATA_DIR"
else
diff --git a/apps/keila/latest/scripts/init.sh b/apps/keila/latest/scripts/init.sh
index 80962de32..116e04e94 100755
--- a/apps/keila/latest/scripts/init.sh
+++ b/apps/keila/latest/scripts/init.sh
@@ -32,6 +32,40 @@ read_env_value() {
strip_matching_quotes "$value"
}
+set_env_value() {
+ local key="$1"
+ local value="$2"
+ local temp_file
+
+ temp_file="$(mktemp "${ENV_FILE}.tmp.XXXXXX")"
+ awk -v key="$key" -v value="$value" '
+ BEGIN { updated = 0 }
+ $0 ~ ("^" key "=") {
+ if (!updated) {
+ print key "=" value
+ updated = 1
+ }
+ next
+ }
+ { print }
+ END {
+ if (!updated) print key "=" value
+ }
+ ' "$ENV_FILE" > "$temp_file"
+ chmod --reference="$ENV_FILE" "$temp_file"
+ mv -f -- "$temp_file" "$ENV_FILE"
+}
+
+ensure_secret_key_base() {
+ local secret_key_base
+
+ secret_key_base="$(read_env_value SECRET_KEY_BASE)"
+ if [[ ${#secret_key_base} -lt 64 ]]; then
+ secret_key_base="$(od -An -N64 -tx1 /dev/urandom | tr -d ' \n')"
+ set_env_value SECRET_KEY_BASE "$secret_key_base"
+ fi
+}
+
path_is_dotenv_safe() {
local value="$1"
@@ -41,6 +75,9 @@ path_is_dotenv_safe() {
esac
}
+[[ -f "$ENV_FILE" ]] || fail "Environment file not found: ${ENV_FILE}"
+ensure_secret_key_base
+
if [[ ${APP_DATA_DIR+x} ]]; then
APP_DATA_DIR_RAW="$APP_DATA_DIR"
else