From c1d6783d5564e5e4cc930aa2c54f4f24c98a2477 Mon Sep 17 00:00:00 2001
From: okxlin <61420215+okxlin@users.noreply.github.com>
Date: Wed, 15 Jul 2026 04:02:01 +0800
Subject: [PATCH] Add questdb app
Maintainer-workflow: appstore-pr-maintainer
---
apps/questdb/9.4.3/.env.sample | 9 ++
apps/questdb/9.4.3/data.yml | 139 +++++++++++++++++
apps/questdb/9.4.3/data/.gitkeep | 1 +
apps/questdb/9.4.3/docker-compose.yml | 34 ++++
apps/questdb/9.4.3/scripts/init.sh | 189 +++++++++++++++++++++++
apps/questdb/9.4.3/scripts/uninstall.sh | 8 +
apps/questdb/9.4.3/scripts/upgrade.sh | 5 +
apps/questdb/README.md | 57 +++++++
apps/questdb/data.yml | 31 ++++
apps/questdb/latest/.env.sample | 9 ++
apps/questdb/latest/data.yml | 139 +++++++++++++++++
apps/questdb/latest/data/.gitkeep | 1 +
apps/questdb/latest/docker-compose.yml | 34 ++++
apps/questdb/latest/scripts/init.sh | 189 +++++++++++++++++++++++
apps/questdb/latest/scripts/uninstall.sh | 8 +
apps/questdb/latest/scripts/upgrade.sh | 5 +
apps/questdb/logo.png | Bin 0 -> 4453 bytes
17 files changed, 858 insertions(+)
create mode 100644 apps/questdb/9.4.3/.env.sample
create mode 100644 apps/questdb/9.4.3/data.yml
create mode 100644 apps/questdb/9.4.3/data/.gitkeep
create mode 100644 apps/questdb/9.4.3/docker-compose.yml
create mode 100755 apps/questdb/9.4.3/scripts/init.sh
create mode 100755 apps/questdb/9.4.3/scripts/uninstall.sh
create mode 100755 apps/questdb/9.4.3/scripts/upgrade.sh
create mode 100644 apps/questdb/README.md
create mode 100644 apps/questdb/data.yml
create mode 100644 apps/questdb/latest/.env.sample
create mode 100644 apps/questdb/latest/data.yml
create mode 100644 apps/questdb/latest/data/.gitkeep
create mode 100644 apps/questdb/latest/docker-compose.yml
create mode 100755 apps/questdb/latest/scripts/init.sh
create mode 100755 apps/questdb/latest/scripts/uninstall.sh
create mode 100755 apps/questdb/latest/scripts/upgrade.sh
create mode 100644 apps/questdb/logo.png
diff --git a/apps/questdb/9.4.3/.env.sample b/apps/questdb/9.4.3/.env.sample
new file mode 100644
index 000000000..18379ca1f
--- /dev/null
+++ b/apps/questdb/9.4.3/.env.sample
@@ -0,0 +1,9 @@
+PANEL_APP_PORT_HTTP=9000
+PANEL_APP_PORT_PG=8812
+QUESTDB_HTTP_USER=admin
+QUESTDB_HTTP_PASSWORD=replace-with-at-least-32-safe-characters
+QUESTDB_PG_USER=admin
+QUESTDB_PG_PASSWORD=replace-with-another-32-safe-characters
+QUESTDB_TELEMETRY_ENABLED=false
+APP_DATA_DIR=./data
+CONTAINER_NAME=questdb
diff --git a/apps/questdb/9.4.3/data.yml b/apps/questdb/9.4.3/data.yml
new file mode 100644
index 000000000..22b8778d9
--- /dev/null
+++ b/apps/questdb/9.4.3/data.yml
@@ -0,0 +1,139 @@
+additionalProperties:
+ formFields:
+ - default: 9000
+ edit: true
+ envKey: PANEL_APP_PORT_HTTP
+ labelEn: Web and HTTP API Port
+ labelZh: Web 与 HTTP API 端口
+ label:
+ en: Web and HTTP API Port
+ zh: Web 与 HTTP API 端口
+ zh-Hant: Web 與 HTTP API 連接埠
+ ja: Web および HTTP API ポート
+ ko: Web 및 HTTP API 포트
+ ru: Порт Web и HTTP API
+ ms: Port Web dan API HTTP
+ pt-br: Porta Web e da API HTTP
+ required: true
+ rule: paramPort
+ type: number
+ - default: 8812
+ edit: true
+ envKey: PANEL_APP_PORT_PG
+ labelEn: PGWire Port
+ labelZh: PGWire 端口
+ label:
+ en: PGWire Port
+ zh: PGWire 端口
+ zh-Hant: PGWire 連接埠
+ ja: PGWire ポート
+ ko: PGWire 포트
+ ru: Порт PGWire
+ ms: Port PGWire
+ pt-br: Porta PGWire
+ required: true
+ rule: paramPort
+ type: number
+ - default: admin
+ edit: true
+ envKey: QUESTDB_HTTP_USER
+ labelEn: HTTP Username
+ labelZh: HTTP 用户名
+ label:
+ en: HTTP Username
+ zh: HTTP 用户名
+ zh-Hant: HTTP 使用者名稱
+ ja: HTTP ユーザー名
+ ko: HTTP 사용자 이름
+ ru: Имя пользователя HTTP
+ ms: Nama Pengguna HTTP
+ pt-br: Usuário HTTP
+ required: true
+ rule: paramCommon
+ type: text
+ - default: generate
+ edit: true
+ envKey: QUESTDB_HTTP_PASSWORD
+ labelEn: HTTP Password
+ labelZh: HTTP 密码
+ label:
+ en: HTTP Password
+ zh: HTTP 密码
+ zh-Hant: HTTP 密碼
+ ja: HTTP パスワード
+ ko: HTTP 비밀번호
+ ru: Пароль HTTP
+ ms: Kata Laluan HTTP
+ pt-br: Senha HTTP
+ required: true
+ type: password
+ - default: admin
+ edit: true
+ envKey: QUESTDB_PG_USER
+ labelEn: PGWire Username
+ labelZh: PGWire 用户名
+ label:
+ en: PGWire Username
+ zh: PGWire 用户名
+ zh-Hant: PGWire 使用者名稱
+ ja: PGWire ユーザー名
+ ko: PGWire 사용자 이름
+ ru: Имя пользователя PGWire
+ ms: Nama Pengguna PGWire
+ pt-br: Usuário PGWire
+ required: true
+ rule: paramCommon
+ type: text
+ - default: generate
+ edit: true
+ envKey: QUESTDB_PG_PASSWORD
+ labelEn: PGWire Password
+ labelZh: PGWire 密码
+ label:
+ en: PGWire Password
+ zh: PGWire 密码
+ zh-Hant: PGWire 密碼
+ ja: PGWire パスワード
+ ko: PGWire 비밀번호
+ ru: Пароль PGWire
+ ms: Kata Laluan PGWire
+ pt-br: Senha PGWire
+ required: true
+ type: password
+ - default: "false"
+ edit: true
+ envKey: QUESTDB_TELEMETRY_ENABLED
+ labelEn: Enable Anonymous Telemetry
+ labelZh: 启用匿名遥测
+ label:
+ en: Enable Anonymous Telemetry
+ zh: 启用匿名遥测
+ zh-Hant: 啟用匿名遙測
+ ja: 匿名テレメトリを有効化
+ ko: 익명 원격 측정 활성화
+ ru: Включить анонимную телеметрию
+ ms: Dayakan Telemetri Tanpa Nama
+ pt-br: Ativar telemetria anônima
+ required: true
+ type: select
+ values:
+ - label: "false"
+ value: "false"
+ - label: "true"
+ value: "true"
+ - 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
diff --git a/apps/questdb/9.4.3/data/.gitkeep b/apps/questdb/9.4.3/data/.gitkeep
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/apps/questdb/9.4.3/data/.gitkeep
@@ -0,0 +1 @@
+
diff --git a/apps/questdb/9.4.3/docker-compose.yml b/apps/questdb/9.4.3/docker-compose.yml
new file mode 100644
index 000000000..e5f0ab805
--- /dev/null
+++ b/apps/questdb/9.4.3/docker-compose.yml
@@ -0,0 +1,34 @@
+services:
+ questdb:
+ image: "questdb/questdb:9.4.3"
+ container_name: ${CONTAINER_NAME}
+ init: true
+ restart: unless-stopped
+ networks:
+ - 1panel-network
+ ports:
+ - "${PANEL_APP_PORT_HTTP}:9000"
+ - "${PANEL_APP_PORT_PG}:8812"
+ environment:
+ - QDB_HTTP_USER=${QUESTDB_HTTP_USER}
+ - QDB_HTTP_PASSWORD=${QUESTDB_HTTP_PASSWORD}
+ - QDB_PG_USER=${QUESTDB_PG_USER}
+ - QDB_PG_PASSWORD=${QUESTDB_PG_PASSWORD}
+ - QDB_TELEMETRY_ENABLED=${QUESTDB_TELEMETRY_ENABLED}
+ - QDB_LINE_TCP_ENABLED=false
+ - QDB_HTTP_PESSIMISTIC_HEALTH_CHECK_ENABLED=true
+ - QDB_HTTP_HEALTH_CHECK_AUTHENTICATION_REQUIRED=false
+ volumes:
+ - "${APP_DATA_DIR}:/var/lib/questdb"
+ healthcheck:
+ test: ["CMD-SHELL", "curl --fail --silent http://127.0.0.1:9003/status >/dev/null"]
+ interval: 30s
+ timeout: 5s
+ start_period: 60s
+ retries: 5
+ labels:
+ createdBy: "Apps"
+
+networks:
+ 1panel-network:
+ external: true
diff --git a/apps/questdb/9.4.3/scripts/init.sh b/apps/questdb/9.4.3/scripts/init.sh
new file mode 100755
index 000000000..ceed52f87
--- /dev/null
+++ b/apps/questdb/9.4.3/scripts/init.sh
@@ -0,0 +1,189 @@
+#!/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"
+}
+
+env_has_key() {
+ local key="$1"
+ [[ -f "$ENV_FILE" ]] && grep -qE "^${key}=" "$ENV_FILE"
+}
+
+read_env_value() {
+ local key="$1"
+ local value=""
+
+ if env_has_key "$key"; then
+ value="$(grep -E "^${key}=" "$ENV_FILE" | tail -n 1 | cut -d '=' -f 2-)"
+ fi
+ strip_matching_quotes "$value"
+}
+
+read_effective_value() {
+ local key="$1"
+
+ if [[ -v "$key" ]]; then
+ strip_matching_quotes "${!key}"
+ else
+ read_env_value "$key"
+ fi
+}
+
+username_is_safe() {
+ local value="$1"
+ [[ "$value" =~ ^[A-Za-z_][A-Za-z0-9_.-]{0,62}$ ]]
+}
+
+secret_is_safe() {
+ local value="$1"
+ [[ "$value" =~ ^[-A-Za-z0-9._~!@#%^\&*+=:,/?]{32,}$ ]]
+}
+
+generate_secret() {
+ local value=""
+
+ if command -v openssl >/dev/null 2>&1; then
+ value="$(openssl rand -hex 32)"
+ elif [[ -r /dev/urandom ]] && command -v od >/dev/null 2>&1; then
+ value="$(od -An -N32 -tx1 /dev/urandom | tr -d ' \n')"
+ fi
+ [[ "$value" =~ ^[0-9a-f]{64}$ ]] || fail "Unable to generate a secure QuestDB password"
+ printf '%s\n' "$value"
+}
+
+write_secret_cache() {
+ local cache_file="$1"
+ local value="$2"
+ local temp_file
+
+ umask 077
+ temp_file="$(mktemp "${cache_file}.tmp.XXXXXX")"
+ printf '%s\n' "$value" > "$temp_file"
+ chmod 600 "$temp_file"
+ mv -f -- "$temp_file" "$cache_file"
+}
+
+set_env_value() {
+ local key="$1"
+ local value="$2"
+ local env_dir
+ local temp_file
+
+ [[ -f "$ENV_FILE" ]] || fail "$ENV_FILE not found"
+ [[ ! -L "$ENV_FILE" ]] || fail "$ENV_FILE must not be a symbolic link"
+ env_dir="$(dirname "$ENV_FILE")"
+ temp_file="$(mktemp "${env_dir}/.questdb-env.tmp.XXXXXX")"
+
+ awk -v key="$key" -v value="$value" '
+ BEGIN { written = 0 }
+ $0 ~ "^" key "=" {
+ if (!written) {
+ print key "=" value
+ written = 1
+ }
+ next
+ }
+ { print }
+ END {
+ if (!written) {
+ print key "=" value
+ }
+ }
+ ' "$ENV_FILE" > "$temp_file"
+ chmod --reference="$ENV_FILE" "$temp_file"
+ mv -f -- "$temp_file" "$ENV_FILE"
+}
+
+resolve_secret() {
+ local env_key="$1"
+ local cache_file="$2"
+ local value
+ local cached_value=""
+
+ [[ ! -L "$cache_file" ]] || fail "QuestDB password cache must not be a symbolic link"
+ if [[ -e "$cache_file" && ! -f "$cache_file" ]]; then
+ fail "QuestDB password cache must be a regular file"
+ fi
+ if [[ -s "$cache_file" ]]; then
+ cached_value="$(sed -n '1p' "$cache_file")"
+ secret_is_safe "$cached_value" || fail "Persisted QuestDB password is invalid"
+ fi
+
+ value="$(read_effective_value "$env_key")"
+ if [[ -z "$value" || "$value" == "generate" ]]; then
+ if [[ -n "$cached_value" ]]; then
+ value="$cached_value"
+ else
+ value="$(generate_secret)"
+ fi
+ else
+ secret_is_safe "$value" || fail "${env_key} must contain at least 32 safe characters"
+ fi
+
+ write_secret_cache "$cache_file" "$value"
+ set_env_value "$env_key" "$value"
+}
+
+[[ -f "$ENV_FILE" ]] || fail "$ENV_FILE not found"
+[[ ! -L "$ENV_FILE" ]] || fail "$ENV_FILE must not be a symbolic link"
+
+app_data_dir_explicit=false
+if [[ ${APP_DATA_DIR+x} ]]; then
+ APP_DATA_DIR_RAW="$APP_DATA_DIR"
+ app_data_dir_explicit=true
+elif env_has_key APP_DATA_DIR; then
+ APP_DATA_DIR_RAW="$(read_env_value APP_DATA_DIR)"
+ app_data_dir_explicit=true
+else
+ APP_DATA_DIR_RAW="./data"
+fi
+APP_DATA_DIR_RAW="$(strip_matching_quotes "$APP_DATA_DIR_RAW")"
+
+if [[ "$app_data_dir_explicit" == "true" && -z "$APP_DATA_DIR_RAW" ]]; then
+ fail "APP_DATA_DIR must not be empty"
+fi
+
+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"
+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"
+
+http_user="$(read_effective_value QUESTDB_HTTP_USER)"
+pg_user="$(read_effective_value QUESTDB_PG_USER)"
+username_is_safe "$http_user" || fail "QUESTDB_HTTP_USER contains unsupported characters"
+username_is_safe "$pg_user" || fail "QUESTDB_PG_USER contains unsupported characters"
+
+resolve_secret QUESTDB_HTTP_PASSWORD "${APP_DATA_DIR_ABS}/.questdb_http_password"
+resolve_secret QUESTDB_PG_PASSWORD "${APP_DATA_DIR_ABS}/.questdb_pg_password"
diff --git a/apps/questdb/9.4.3/scripts/uninstall.sh b/apps/questdb/9.4.3/scripts/uninstall.sh
new file mode 100755
index 000000000..5a3f6bd6b
--- /dev/null
+++ b/apps/questdb/9.4.3/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/questdb/9.4.3/scripts/upgrade.sh b/apps/questdb/9.4.3/scripts/upgrade.sh
new file mode 100755
index 000000000..593b31667
--- /dev/null
+++ b/apps/questdb/9.4.3/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/questdb/README.md b/apps/questdb/README.md
new file mode 100644
index 000000000..2a28ec1bb
--- /dev/null
+++ b/apps/questdb/README.md
@@ -0,0 +1,57 @@
+# QuestDB
+
+## 产品介绍
+
+QuestDB 是一个面向实时分析的开源时序数据库,提供高吞吐写入、低延迟 SQL 查询、Web Console、REST API、InfluxDB Line Protocol 和 PostgreSQL Wire Protocol。
+
+## 主要功能
+
+- 面向时序数据的列式存储、WAL 和分区管理
+- Web Console、REST API 与 SQL 查询
+- 通过 HTTP 接收 InfluxDB Line Protocol 数据
+- 兼容 PostgreSQL Wire Protocol 的查询接口
+- 快照、检查点和持久化数据目录
+
+## 访问说明
+
+- Web Console、REST API 和 ILP-over-HTTP 共用默认端口 `9000`。
+- PostgreSQL Wire Protocol 默认使用端口 `8812`,数据库名为 `qdb`。
+- 本应用默认启用 HTTP Basic 认证和 PGWire 密码认证,两个接口使用独立用户名与密码。
+- QuestDB 的 ILP TCP 端口 `9009` 默认不提供简单的用户名/密码保护。本应用为减少未认证写入面,默认关闭且不发布该端口;常见写入场景可直接使用已受 HTTP Basic 保护的 ILP-over-HTTP。
+- 内部 `9003` 端口仅用于容器健康检查,不发布到宿主机。健康状态端点不要求业务凭据,其他 HTTP 接口仍受 Basic 认证保护。
+
+## 数据与安全
+
+- QuestDB 的完整数据根目录持久化到安装表单选择的数据目录,其中包含 `db`、`conf`、`public`、快照和检查点等运行数据。
+- 安装脚本会生成并持久化独立的 HTTP 与 PGWire 密码。数据目录中的 `.questdb_http_password` 和 `.questdb_pg_password` 用于防止 1Panel 升级回放安装参数时轮换密码,请与数据库一起备份。
+- 官方镜像入口以 root 启动,用于检查数据目录属主,然后以 UID/GID `10001:10001` 运行 QuestDB。首次挂载既有数据目录时,上游入口可能调整其已知数据子目录的属主;迁移前应先备份并核对权限。
+- Web Console 和协议端口本身不提供 HTTPS。对外访问时应使用 HTTPS 反向代理、限制端口来源,并按需配置网络层 TLS。
+- 默认关闭匿名遥测,可在安装表单中按需开启。
+
+## 升级说明
+
+- 升级前停止写入并完整备份数据目录及两个密码缓存文件。
+- 固定版与 `latest` 使用相同的数据路径和配置键,可通过 1Panel 执行跨版本升级。
+- 数据格式迁移可能在启动阶段完成。生产数据应先在副本环境验证目标版本,并在升级过程中等待健康检查通过后再恢复流量。
+
+## Introduction
+
+QuestDB is an open-source time-series database for high-throughput ingestion and low-latency SQL analytics. This package exposes the authenticated Web/REST/ILP-over-HTTP endpoint and PGWire endpoint, while disabling unauthenticated ILP TCP by default.
+
+Back up the full data directory, `.questdb_http_password` and `.questdb_pg_password` before upgrades. Public deployments should place the Web endpoint behind HTTPS and restrict direct access to the database ports.
+
+## Features
+
+- Column-oriented time-series storage, WAL and partition management
+- Web Console, REST API and SQL queries
+- Authenticated InfluxDB Line Protocol over HTTP
+- PostgreSQL Wire Protocol compatibility
+- Persistent checkpoints and snapshots
+
+## 参考资料
+
+- Docker 部署:
+- 配置选项:
+- 安全指南:
+- 源码仓库:
+- 官方镜像:
diff --git a/apps/questdb/data.yml b/apps/questdb/data.yml
new file mode 100644
index 000000000..9b77ed434
--- /dev/null
+++ b/apps/questdb/data.yml
@@ -0,0 +1,31 @@
+name: QuestDB
+tags:
+ - 数据库
+title: 面向实时分析的高性能开源时序数据库
+description: 面向实时分析的高性能开源时序数据库
+additionalProperties:
+ key: questdb
+ name: QuestDB
+ tags:
+ - Database
+ shortDescZh: 面向实时分析的高性能开源时序数据库
+ shortDescEn: A high-performance open-source time-series database for real-time analytics
+ description:
+ en: A high-performance open-source time-series database for real-time analytics
+ zh: 面向实时分析的高性能开源时序数据库
+ zh-Hant: 面向即時分析的高效能開源時序資料庫
+ ja: リアルタイム分析向けの高性能オープンソース時系列データベース
+ ko: 실시간 분석을 위한 고성능 오픈 소스 시계열 데이터베이스
+ ru: Высокопроизводительная база данных временных рядов с открытым исходным кодом для аналитики в реальном времени
+ ms: Pangkalan data siri masa sumber terbuka berprestasi tinggi untuk analitik masa nyata
+ pt-br: Banco de dados de séries temporais de alto desempenho e código aberto para análises em tempo real
+ type: website
+ crossVersionUpdate: true
+ limit: 0
+ recommend: 0
+ website: https://questdb.com/
+ github: https://github.com/questdb/questdb
+ document: https://questdb.com/docs/deployment/docker/
+ architectures:
+ - amd64
+ - arm64
diff --git a/apps/questdb/latest/.env.sample b/apps/questdb/latest/.env.sample
new file mode 100644
index 000000000..18379ca1f
--- /dev/null
+++ b/apps/questdb/latest/.env.sample
@@ -0,0 +1,9 @@
+PANEL_APP_PORT_HTTP=9000
+PANEL_APP_PORT_PG=8812
+QUESTDB_HTTP_USER=admin
+QUESTDB_HTTP_PASSWORD=replace-with-at-least-32-safe-characters
+QUESTDB_PG_USER=admin
+QUESTDB_PG_PASSWORD=replace-with-another-32-safe-characters
+QUESTDB_TELEMETRY_ENABLED=false
+APP_DATA_DIR=./data
+CONTAINER_NAME=questdb
diff --git a/apps/questdb/latest/data.yml b/apps/questdb/latest/data.yml
new file mode 100644
index 000000000..22b8778d9
--- /dev/null
+++ b/apps/questdb/latest/data.yml
@@ -0,0 +1,139 @@
+additionalProperties:
+ formFields:
+ - default: 9000
+ edit: true
+ envKey: PANEL_APP_PORT_HTTP
+ labelEn: Web and HTTP API Port
+ labelZh: Web 与 HTTP API 端口
+ label:
+ en: Web and HTTP API Port
+ zh: Web 与 HTTP API 端口
+ zh-Hant: Web 與 HTTP API 連接埠
+ ja: Web および HTTP API ポート
+ ko: Web 및 HTTP API 포트
+ ru: Порт Web и HTTP API
+ ms: Port Web dan API HTTP
+ pt-br: Porta Web e da API HTTP
+ required: true
+ rule: paramPort
+ type: number
+ - default: 8812
+ edit: true
+ envKey: PANEL_APP_PORT_PG
+ labelEn: PGWire Port
+ labelZh: PGWire 端口
+ label:
+ en: PGWire Port
+ zh: PGWire 端口
+ zh-Hant: PGWire 連接埠
+ ja: PGWire ポート
+ ko: PGWire 포트
+ ru: Порт PGWire
+ ms: Port PGWire
+ pt-br: Porta PGWire
+ required: true
+ rule: paramPort
+ type: number
+ - default: admin
+ edit: true
+ envKey: QUESTDB_HTTP_USER
+ labelEn: HTTP Username
+ labelZh: HTTP 用户名
+ label:
+ en: HTTP Username
+ zh: HTTP 用户名
+ zh-Hant: HTTP 使用者名稱
+ ja: HTTP ユーザー名
+ ko: HTTP 사용자 이름
+ ru: Имя пользователя HTTP
+ ms: Nama Pengguna HTTP
+ pt-br: Usuário HTTP
+ required: true
+ rule: paramCommon
+ type: text
+ - default: generate
+ edit: true
+ envKey: QUESTDB_HTTP_PASSWORD
+ labelEn: HTTP Password
+ labelZh: HTTP 密码
+ label:
+ en: HTTP Password
+ zh: HTTP 密码
+ zh-Hant: HTTP 密碼
+ ja: HTTP パスワード
+ ko: HTTP 비밀번호
+ ru: Пароль HTTP
+ ms: Kata Laluan HTTP
+ pt-br: Senha HTTP
+ required: true
+ type: password
+ - default: admin
+ edit: true
+ envKey: QUESTDB_PG_USER
+ labelEn: PGWire Username
+ labelZh: PGWire 用户名
+ label:
+ en: PGWire Username
+ zh: PGWire 用户名
+ zh-Hant: PGWire 使用者名稱
+ ja: PGWire ユーザー名
+ ko: PGWire 사용자 이름
+ ru: Имя пользователя PGWire
+ ms: Nama Pengguna PGWire
+ pt-br: Usuário PGWire
+ required: true
+ rule: paramCommon
+ type: text
+ - default: generate
+ edit: true
+ envKey: QUESTDB_PG_PASSWORD
+ labelEn: PGWire Password
+ labelZh: PGWire 密码
+ label:
+ en: PGWire Password
+ zh: PGWire 密码
+ zh-Hant: PGWire 密碼
+ ja: PGWire パスワード
+ ko: PGWire 비밀번호
+ ru: Пароль PGWire
+ ms: Kata Laluan PGWire
+ pt-br: Senha PGWire
+ required: true
+ type: password
+ - default: "false"
+ edit: true
+ envKey: QUESTDB_TELEMETRY_ENABLED
+ labelEn: Enable Anonymous Telemetry
+ labelZh: 启用匿名遥测
+ label:
+ en: Enable Anonymous Telemetry
+ zh: 启用匿名遥测
+ zh-Hant: 啟用匿名遙測
+ ja: 匿名テレメトリを有効化
+ ko: 익명 원격 측정 활성화
+ ru: Включить анонимную телеметрию
+ ms: Dayakan Telemetri Tanpa Nama
+ pt-br: Ativar telemetria anônima
+ required: true
+ type: select
+ values:
+ - label: "false"
+ value: "false"
+ - label: "true"
+ value: "true"
+ - 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
diff --git a/apps/questdb/latest/data/.gitkeep b/apps/questdb/latest/data/.gitkeep
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/apps/questdb/latest/data/.gitkeep
@@ -0,0 +1 @@
+
diff --git a/apps/questdb/latest/docker-compose.yml b/apps/questdb/latest/docker-compose.yml
new file mode 100644
index 000000000..0532b6a24
--- /dev/null
+++ b/apps/questdb/latest/docker-compose.yml
@@ -0,0 +1,34 @@
+services:
+ questdb:
+ image: "questdb/questdb:latest"
+ container_name: ${CONTAINER_NAME}
+ init: true
+ restart: unless-stopped
+ networks:
+ - 1panel-network
+ ports:
+ - "${PANEL_APP_PORT_HTTP}:9000"
+ - "${PANEL_APP_PORT_PG}:8812"
+ environment:
+ - QDB_HTTP_USER=${QUESTDB_HTTP_USER}
+ - QDB_HTTP_PASSWORD=${QUESTDB_HTTP_PASSWORD}
+ - QDB_PG_USER=${QUESTDB_PG_USER}
+ - QDB_PG_PASSWORD=${QUESTDB_PG_PASSWORD}
+ - QDB_TELEMETRY_ENABLED=${QUESTDB_TELEMETRY_ENABLED}
+ - QDB_LINE_TCP_ENABLED=false
+ - QDB_HTTP_PESSIMISTIC_HEALTH_CHECK_ENABLED=true
+ - QDB_HTTP_HEALTH_CHECK_AUTHENTICATION_REQUIRED=false
+ volumes:
+ - "${APP_DATA_DIR}:/var/lib/questdb"
+ healthcheck:
+ test: ["CMD-SHELL", "curl --fail --silent http://127.0.0.1:9003/status >/dev/null"]
+ interval: 30s
+ timeout: 5s
+ start_period: 60s
+ retries: 5
+ labels:
+ createdBy: "Apps"
+
+networks:
+ 1panel-network:
+ external: true
diff --git a/apps/questdb/latest/scripts/init.sh b/apps/questdb/latest/scripts/init.sh
new file mode 100755
index 000000000..ceed52f87
--- /dev/null
+++ b/apps/questdb/latest/scripts/init.sh
@@ -0,0 +1,189 @@
+#!/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"
+}
+
+env_has_key() {
+ local key="$1"
+ [[ -f "$ENV_FILE" ]] && grep -qE "^${key}=" "$ENV_FILE"
+}
+
+read_env_value() {
+ local key="$1"
+ local value=""
+
+ if env_has_key "$key"; then
+ value="$(grep -E "^${key}=" "$ENV_FILE" | tail -n 1 | cut -d '=' -f 2-)"
+ fi
+ strip_matching_quotes "$value"
+}
+
+read_effective_value() {
+ local key="$1"
+
+ if [[ -v "$key" ]]; then
+ strip_matching_quotes "${!key}"
+ else
+ read_env_value "$key"
+ fi
+}
+
+username_is_safe() {
+ local value="$1"
+ [[ "$value" =~ ^[A-Za-z_][A-Za-z0-9_.-]{0,62}$ ]]
+}
+
+secret_is_safe() {
+ local value="$1"
+ [[ "$value" =~ ^[-A-Za-z0-9._~!@#%^\&*+=:,/?]{32,}$ ]]
+}
+
+generate_secret() {
+ local value=""
+
+ if command -v openssl >/dev/null 2>&1; then
+ value="$(openssl rand -hex 32)"
+ elif [[ -r /dev/urandom ]] && command -v od >/dev/null 2>&1; then
+ value="$(od -An -N32 -tx1 /dev/urandom | tr -d ' \n')"
+ fi
+ [[ "$value" =~ ^[0-9a-f]{64}$ ]] || fail "Unable to generate a secure QuestDB password"
+ printf '%s\n' "$value"
+}
+
+write_secret_cache() {
+ local cache_file="$1"
+ local value="$2"
+ local temp_file
+
+ umask 077
+ temp_file="$(mktemp "${cache_file}.tmp.XXXXXX")"
+ printf '%s\n' "$value" > "$temp_file"
+ chmod 600 "$temp_file"
+ mv -f -- "$temp_file" "$cache_file"
+}
+
+set_env_value() {
+ local key="$1"
+ local value="$2"
+ local env_dir
+ local temp_file
+
+ [[ -f "$ENV_FILE" ]] || fail "$ENV_FILE not found"
+ [[ ! -L "$ENV_FILE" ]] || fail "$ENV_FILE must not be a symbolic link"
+ env_dir="$(dirname "$ENV_FILE")"
+ temp_file="$(mktemp "${env_dir}/.questdb-env.tmp.XXXXXX")"
+
+ awk -v key="$key" -v value="$value" '
+ BEGIN { written = 0 }
+ $0 ~ "^" key "=" {
+ if (!written) {
+ print key "=" value
+ written = 1
+ }
+ next
+ }
+ { print }
+ END {
+ if (!written) {
+ print key "=" value
+ }
+ }
+ ' "$ENV_FILE" > "$temp_file"
+ chmod --reference="$ENV_FILE" "$temp_file"
+ mv -f -- "$temp_file" "$ENV_FILE"
+}
+
+resolve_secret() {
+ local env_key="$1"
+ local cache_file="$2"
+ local value
+ local cached_value=""
+
+ [[ ! -L "$cache_file" ]] || fail "QuestDB password cache must not be a symbolic link"
+ if [[ -e "$cache_file" && ! -f "$cache_file" ]]; then
+ fail "QuestDB password cache must be a regular file"
+ fi
+ if [[ -s "$cache_file" ]]; then
+ cached_value="$(sed -n '1p' "$cache_file")"
+ secret_is_safe "$cached_value" || fail "Persisted QuestDB password is invalid"
+ fi
+
+ value="$(read_effective_value "$env_key")"
+ if [[ -z "$value" || "$value" == "generate" ]]; then
+ if [[ -n "$cached_value" ]]; then
+ value="$cached_value"
+ else
+ value="$(generate_secret)"
+ fi
+ else
+ secret_is_safe "$value" || fail "${env_key} must contain at least 32 safe characters"
+ fi
+
+ write_secret_cache "$cache_file" "$value"
+ set_env_value "$env_key" "$value"
+}
+
+[[ -f "$ENV_FILE" ]] || fail "$ENV_FILE not found"
+[[ ! -L "$ENV_FILE" ]] || fail "$ENV_FILE must not be a symbolic link"
+
+app_data_dir_explicit=false
+if [[ ${APP_DATA_DIR+x} ]]; then
+ APP_DATA_DIR_RAW="$APP_DATA_DIR"
+ app_data_dir_explicit=true
+elif env_has_key APP_DATA_DIR; then
+ APP_DATA_DIR_RAW="$(read_env_value APP_DATA_DIR)"
+ app_data_dir_explicit=true
+else
+ APP_DATA_DIR_RAW="./data"
+fi
+APP_DATA_DIR_RAW="$(strip_matching_quotes "$APP_DATA_DIR_RAW")"
+
+if [[ "$app_data_dir_explicit" == "true" && -z "$APP_DATA_DIR_RAW" ]]; then
+ fail "APP_DATA_DIR must not be empty"
+fi
+
+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"
+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"
+
+http_user="$(read_effective_value QUESTDB_HTTP_USER)"
+pg_user="$(read_effective_value QUESTDB_PG_USER)"
+username_is_safe "$http_user" || fail "QUESTDB_HTTP_USER contains unsupported characters"
+username_is_safe "$pg_user" || fail "QUESTDB_PG_USER contains unsupported characters"
+
+resolve_secret QUESTDB_HTTP_PASSWORD "${APP_DATA_DIR_ABS}/.questdb_http_password"
+resolve_secret QUESTDB_PG_PASSWORD "${APP_DATA_DIR_ABS}/.questdb_pg_password"
diff --git a/apps/questdb/latest/scripts/uninstall.sh b/apps/questdb/latest/scripts/uninstall.sh
new file mode 100755
index 000000000..5a3f6bd6b
--- /dev/null
+++ b/apps/questdb/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/questdb/latest/scripts/upgrade.sh b/apps/questdb/latest/scripts/upgrade.sh
new file mode 100755
index 000000000..593b31667
--- /dev/null
+++ b/apps/questdb/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/questdb/logo.png b/apps/questdb/logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..ca83aa19a31cd2a29496cb0521b54f01b7f3dc1e
GIT binary patch
literal 4453
zcmV-r5t{CaP)cw=oH1;^NOztyY`|53
zkS%J(W{$#JhK?<2u{myxENZbjZigyrkS%M7Dr$%;YRN)x%R_L*L2$!BZ_q_>yghEf
zKX1E2bHG1vy+3fQIBlafY^gSF%Sv^`NOsCic$+e8%Tj#COnAyxfWuLJ%3Fi8I&Q^U
zgv(%u#$=3^Fl&-7Ym6*vhbn4E&ptc=003ijQchC<0~RMlPGfz8l%=z~qh?t&Dhfl(
z+T`=@w!4(>Zs%gjL{bttuHD#<|MdMZqq{~oTa=sjEh!Au@WAX`Oj5l)Ig}=t&nd`ZC+HDpyfCxYcs5K}!!WbirF~-vB!u;#E
z$=ubg^){OsL4*K8z|z`)CfqWXH*(d?Yqj!R_I8^DWCm3kAOryU0jN1-EMtsiS*qlO
z%xhHnHXy_S(1<_-(;*m9YY=2vk|gQf($ecONk*W^Y+wK(L>4jt2m!;*7|W<5%QD5T
zLUZL&CzGWo
zQ=KJ=@EXvP^xk%wdAlgA8X~s??d-rN0wehqLKw@kfonlam7JKi%xwq*mmq|uGXMYp
zV?d38FqY+49gL1-+0HCU(ppMl^GhqCG6T~dM#8feAVL@+2~$T)uBhY-X(Wy8JMD09
z009(+@~~C0m)pNqr>^YnVx+4ckm$lI(zOEdoq4G)WwiBuVls5A)HQF#Cip
zuGsQiK-vJ%Eb0)#nApRR8_U?Mu)#d@Y|UR;wIK(kI-2Bw1#0GKS(0TLb$p@IV7`9K
zZfACpM3bGhbKABR8T(q9FfYwMJ6~P5*_oN@05Yn601yDkvIQxVWQ1MmIZfv5dOLzS
ztW5>jiVYK5mSy~k_}9Z+O_3Rfg;_S!RcM+TAURh3hk?NY2KsrZe7`%s(!k1tKAMS_-(GB`3_@D$~%l@vZG*sX3>vX
zQ5sVeBI0V>7VK65G;%W_A27z1N~Kb%P;6P2G4^3@h+c1~ilQjWe&V8UZlxWet|QQp
z*86KKrTkT4XEyir=XKJRTa77IJu
z`K?L>=9q-pOYej)LV(Er
z{r*b!txy+cs`<4*=1tXNRYml-{}k*
zt)2Mh2&(H40_awDMomwf&WfUF7WZL{Z;;7_7SITxm8BUy*cI+symqA(O}!uet6aay
z&Kt&o8@e{DzG2}|@ij-hwU}86$;!rFBh^~1$d#BhBrLOnx-|QNqsf6u$vuoCYV`O)dyk>M+aqhIP&lpBFEYnrM?
zl3KXQEH_Bz0J&MML5|~=KeTplH*fR=k}0uu1_(h2S7zF_7wRl%3a&p=+)%B&LF3VKP~AbG-Pk852tou`QXJl?9vB-S!F8#9
zC&<$N(1aUh(_`jp4YoOgO}zvt^syiH`(ldFT+>W;USwTo7fpZ5wjqSar`=N$4&;-Q
zYpKpHEu--bl#u7Q1ec6Me$r|Axk%QRTlP<36`KVm;<8PKP;FtFeM
zC{<`~muYBSRn;c@TnIu4kGnK%+{eeK$Ezt1{#6e&UtnEVRb79`tXno9gb;SU)TgJX
zr;90%{S{i8N{S`7HB?pC9~YUAv`%Oz_Q&ZmbG1Vsd+0_mYw9d;L)*C5ucQpkH$Bk0
zZfebJe=I;BXhMB<`bpYzikzVNE!=hKpI%b=|~gK?B+`KkoJW{eHjafc`jr=tH0UY<~QV
z0ZrTd0CxNRe!ti2^?DY#@H(NX!WO%(e;xpB`lH{qn0vj~0{u~Q*Bi~<-Db1d++Aiu
zlc@rD+$TZB?)AFe*IhLPBaL+-_2G_J1QZGPcTrS0^
zXV<#l`!hm4UEX&H9RRI8n%NbAtR*zUXZ^knyWjtNgeRBGzMVM;TC4BQNNWL8X6%%j
z#M200p}enzINEG}9egz#>YWX1o*3hk{`t8b`io*z>Yz1f^>Yt)XVS3}SLv-bfDpo{
zdw$-xVSoB{q(;#`6Iyc}1|2kKL7gc9006*%jK-th`EY1Ke-o|4RZ&`M{nm9ck2Pn&hF^64iO}P09lM!>d3Mr$tQhF>Mx6lsRIs?^(nQUTO+p~F~+jg
zk!4wu?bJWSI1DH@>r6kZuI}h`)U_PIGE34c+XXR``p2hnz8Z0e77ndXr?p^QTyoyT>+~q~Cxxk~Ron5m^Dsz1EKKJQ
z*?X(4^M|n(zl}}p9Q1Le4&5Ywo|u|n#8hVYbt`I-AD{9usoi_Gx7AP8ew>(kiQEqJ
zxgquwq5k0?@l?3~C@9;}hA#OlF?FUSF)ke&7WuT0nA%IM+S|GM^Q40g+kW0~i2dc)
zNvYMKqo8T1%Oy#EB{8D&^KdvE{t~Zg!3!!lVd*SMk~HZ2IUN4D8I!u&
z@-qiLu6I&DPDVYqB?Ukaq@RYvf5xQdsx8%Xgshtfo3qe58^A0{gF)x}*wlRWf&WhE
zHAq70a>-xg!C)|u27|%TQRn-AJ&9SERrmb2Z=Xo!knMMUnWcfjel+O(oGZko_MX%;
zVlT;6)l7q-^FXqCxt-EZQfgiGH-@Ek66$Kj>t)HwJQxh(zS75}4kk`YUEK8K3@0y*
zdty@RAnwsN$thXAw)hOWNb2Wn0<7Cug$$(}H?K;e=w;B(By;^M
z_L|SkOz=F176qCsKlGE@CoV2xQ`3Cyf@TaF7@gfH&|LZ9$hCRTc2l^@!+ZeN)%E)r9V_o9qc+1vinpt*A1Z$t5ePC%XcweC|O1)L>Egyw3W
zjV)_1pc8A&3%A4f?gY>l^_U^Gp>z^zPJH+q9mpI+m_dmQ&DA~|GgIBzB2GwM+x{?`
zpW{JuwO_^omUhI;1cOiFem&Y$RV-+(_RHAM&==&7Jti+di%(q?8Yw^%YCo0TQco_D
zP;=!wABK-q#fA2zzVL;fOp;LZ;&-i?Lo=w|mcF={OeT{jNro19vDpfmPf`;=yQvAV
zKlJ2DqJwl^{O&em2!%9|;zK*B389;LLYR|Mb9~``qi%o4-PF)rt@+Zs3wmXqOeT}b
zzmpCv)T*1$eBGg{s-_+$NKFQHDD!0ULE=#hz98&Bt9xbyRaI3JC~c$mWp+SMLCtY|
zp}N^NH3qXpX=8f(bpD%#xabSB3oZy;h65
zBOx@$-+kdhp7dSQ#-AOCK}V>6m>
r>VKVf8JBSxmvI@FaT%BK=EVO1?z;0|*$-yG00000NkvXXu0mjfhN6>_
literal 0
HcmV?d00001