PocketBase + Caddy 安装和快速添加项目的一键脚本
— dokbun
PocketBase 非常适合个人小项目,AI 编程场景下的后端用户注册登录、数据和文件存储必备。
这个脚本可以快速安装 PocketBase 和 Caddy,并实现快速添加多个 PocketBase 和绑定域名,自动申请 SSL 证书,以便用于不同的项目。提前解析域名之后,运行脚本即可开始使用。
创建脚本
vim pocketbase.sh#!/bin/bash
set -euo pipefail
# ==============================================================================
# 脚本名称: install_pocketbase.sh
# 描述: 在 Ubuntu VPS 上安装 PocketBase 和 Caddy,并管理多个项目实例。
# 作者: dokbun
# ==============================================================================
# 颜色定义
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
DEFAULT_PB_VERSION="0.38.2"
PB_REPO_URL="https://github.com/pocketbase/pocketbase"
PB_RELEASES_URL="https://github.com/pocketbase/pocketbase/releases"
PB_SYSTEM_USER="pocketbase"
PB_SYSTEM_GROUP="pocketbase"
PB_PROJECTS_DIR="/opt/pocketbase"
CADDYFILE="/etc/caddy/Caddyfile"
# 注意:直接以 root 运行脚本不是最佳实践。
# 脚本中已为需要 root 权限的命令添加了 sudo,因此请以普通用户运行,
# 它会在需要时提示您输入 sudo 密码。
error() {
echo -e "${RED}错误: $*${NC}" >&2
}
info() {
echo -e "${GREEN}$*${NC}"
}
warn() {
echo -e "${YELLOW}$*${NC}" >&2
}
validate_version() {
local version="$1"
[[ "$version" =~ ^[0-9]+(\.[0-9]+){1,3}$ ]]
}
validate_project_name() {
local project_name="$1"
[[ "$project_name" =~ ^[a-zA-Z0-9_-]+$ ]]
}
validate_port() {
local port="$1"
[[ "$port" =~ ^[0-9]+$ ]] && [ "$port" -ge 1 ] && [ "$port" -le 65535 ]
}
validate_domain() {
local domain="$1"
[[ "$domain" =~ ^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$ ]]
}
validate_email() {
local email="$1"
[[ "$email" =~ ^[^[:space:]@]+@[^[:space:]@]+\.[^[:space:]@]+$ ]]
}
detect_pocketbase_arch() {
case "$(uname -m)" in
x86_64|amd64)
echo "amd64"
;;
aarch64|arm64)
echo "arm64"
;;
armv7l|armv7)
echo "armv7"
;;
*)
error "不支持的 CPU 架构: $(uname -m)"
return 1
;;
esac
}
install_dependencies() {
warn "正在检查/安装必要依赖..."
sudo apt update
sudo apt install -y unzip wget curl gpg ca-certificates debian-keyring debian-archive-keyring apt-transport-https ufw
info "依赖安装完成。"
}
ensure_pocketbase_user() {
if ! getent group "$PB_SYSTEM_GROUP" >/dev/null 2>&1; then
warn "正在创建专用系统用户组 ${PB_SYSTEM_GROUP}..."
sudo groupadd --system "$PB_SYSTEM_GROUP"
fi
if id -u "$PB_SYSTEM_USER" >/dev/null 2>&1; then
info "系统用户 ${PB_SYSTEM_USER} 已存在。"
return 0
fi
warn "正在创建专用系统用户 ${PB_SYSTEM_USER}..."
sudo useradd --system --gid "$PB_SYSTEM_GROUP" --home-dir /var/lib/pocketbase --shell /usr/sbin/nologin "$PB_SYSTEM_USER"
info "系统用户 ${PB_SYSTEM_USER} 创建完成。"
}
ensure_projects_dir() {
warn "正在准备 PocketBase 实例目录: ${PB_PROJECTS_DIR}"
sudo install -d -o "$PB_SYSTEM_USER" -g "$PB_SYSTEM_GROUP" -m 0750 "$PB_PROJECTS_DIR"
}
prompt_pocketbase_version_and_download() {
echo
warn "PocketBase GitHub 仓库: ${PB_REPO_URL}"
warn "PocketBase GitHub Releases: ${PB_RELEASES_URL}"
echo
read -r -p "请输入 PocketBase 版本号 (回车默认 ${DEFAULT_PB_VERSION}): " PB_VERSION
PB_VERSION=${PB_VERSION:-"$DEFAULT_PB_VERSION"}
if ! validate_version "$PB_VERSION"; then
error "版本号格式不正确,例如: 0.35.0"
return 1
fi
PB_ARCH=$(detect_pocketbase_arch)
PB_ZIP_FILE="pocketbase_${PB_VERSION}_linux_${PB_ARCH}.zip"
PB_DOWNLOAD_URL="https://github.com/pocketbase/pocketbase/releases/download/v${PB_VERSION}/${PB_ZIP_FILE}"
PB_ZIP_PATH="$HOME/${PB_ZIP_FILE}"
if [ -s "$PB_ZIP_PATH" ]; then
info "已找到本地安装包,将复用: ${PB_ZIP_PATH}"
return 0
fi
warn "正在从 ${PB_DOWNLOAD_URL} 下载..."
if ! wget --https-only -O "${PB_ZIP_PATH}.tmp" "$PB_DOWNLOAD_URL"; then
rm -f "${PB_ZIP_PATH}.tmp"
error "下载失败! 请检查版本号、CPU 架构或网络连接。"
return 1
fi
if [ ! -s "${PB_ZIP_PATH}.tmp" ]; then
rm -f "${PB_ZIP_PATH}.tmp"
error "下载文件为空,请检查版本号或网络连接。"
return 1
fi
mv "${PB_ZIP_PATH}.tmp" "$PB_ZIP_PATH"
info "PocketBase v${PB_VERSION} (${PB_ARCH}) 下载完成,已保存至 ${PB_ZIP_PATH}"
}
install_caddy() {
if command -v caddy >/dev/null 2>&1 && sudo systemctl list-unit-files caddy.service --no-legend 2>/dev/null | awk '{print $1}' | grep -qx 'caddy.service'; then
info "检测到 Caddy 已安装,将保留现有安装。"
return 0
fi
warn "正在配置 Caddy 官方软件源并安装 Caddy..."
sudo install -d -m 0755 /usr/share/keyrings
curl -fsSL 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor --yes -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -fsSL 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list > /dev/null
sudo apt update
sudo apt install -y caddy
info "Caddy 安装完成。"
}
ensure_caddy_available() {
if command -v caddy >/dev/null 2>&1 && sudo systemctl list-unit-files caddy.service --no-legend 2>/dev/null | awk '{print $1}' | grep -qx 'caddy.service'; then
return 0
fi
warn "未检测到可用的 Caddy 命令和 systemd 服务,将先安装 Caddy。"
install_caddy
}
backup_caddyfile() {
local backup_path=""
if sudo test -f "$CADDYFILE"; then
backup_path="${CADDYFILE}.bak.$(date +%Y%m%d%H%M%S)"
sudo cp "$CADDYFILE" "$backup_path"
warn "已备份当前 Caddyfile: ${backup_path}"
fi
echo "$backup_path"
}
restore_caddyfile() {
local backup_path="$1"
if [ -n "$backup_path" ] && sudo test -f "$backup_path"; then
warn "正在恢复 Caddyfile 备份: ${backup_path}"
sudo cp "$backup_path" "$CADDYFILE"
fi
}
validate_caddyfile_or_restore() {
local backup_path="$1"
if sudo caddy validate --config "$CADDYFILE"; then
return 0
fi
error "Caddyfile 验证失败,已取消本次配置变更。"
restore_caddyfile "$backup_path"
return 1
}
format_caddyfile_or_restore() {
local backup_path="$1"
if sudo caddy fmt --overwrite "$CADDYFILE"; then
return 0
fi
error "Caddyfile 格式化失败,已取消本次配置变更。"
restore_caddyfile "$backup_path"
return 1
}
apply_caddy_or_restore() {
local backup_path="$1"
if sudo systemctl is-active --quiet caddy; then
if sudo systemctl reload caddy; then
return 0
fi
else
warn "Caddy 当前未运行,正在启用并启动 Caddy..."
if sudo systemctl enable caddy && sudo systemctl start caddy; then
return 0
fi
fi
error "Caddy 应用配置失败,正在回滚 Caddyfile。"
restore_caddyfile "$backup_path"
if sudo systemctl is-active --quiet caddy; then
sudo systemctl reload caddy || true
fi
return 1
}
ensure_caddy_email() {
local caddy_email="$1"
local backup_path
local tmp_file
backup_path=$(backup_caddyfile)
if ! sudo test -s "$CADDYFILE"; then
warn "正在创建新的 ${CADDYFILE}..."
sudo tee "$CADDYFILE" > /dev/null <<EOF
{
email $caddy_email
}
EOF
elif sudo grep -Eq '^[[:space:]]*email[[:space:]]+' "$CADDYFILE"; then
warn "Caddyfile 中已存在 email 配置,将保留现有配置。"
elif sudo awk 'NF && $1 !~ /^#/ { exit ($0 ~ /^[[:space:]]*\{[[:space:]]*$/ ? 0 : 1) }' "$CADDYFILE"; then
warn "检测到现有 Caddy 全局配置块,正在写入 email..."
tmp_file=$(mktemp)
sudo awk -v caddy_email="$caddy_email" '
!inserted && $0 ~ /^[[:space:]]*\{[[:space:]]*$/ {
print
print " email " caddy_email
inserted = 1
next
}
{ print }
' "$CADDYFILE" > "$tmp_file"
sudo cp "$tmp_file" "$CADDYFILE"
rm -f "$tmp_file"
else
warn "正在将 email 全局配置添加到现有 Caddyfile 顶部..."
tmp_file=$(mktemp)
{
printf '{\n email %s\n}\n\n' "$caddy_email"
sudo cat "$CADDYFILE"
} > "$tmp_file"
sudo cp "$tmp_file" "$CADDYFILE"
rm -f "$tmp_file"
fi
format_caddyfile_or_restore "$backup_path"
validate_caddyfile_or_restore "$backup_path"
apply_caddy_or_restore "$backup_path"
info "Caddyfile 配置完成。"
}
append_caddy_site() {
local domain_name="$1"
local pb_port="$2"
local backup_path
if caddy_site_exists "$domain_name"; then
error "Caddyfile 中已存在域名配置: ${domain_name}"
return 1
fi
backup_path=$(backup_caddyfile)
warn "正在将域名配置追加到 ${CADDYFILE}..."
sudo tee -a "$CADDYFILE" > /dev/null <<EOF
${domain_name} {
reverse_proxy 127.0.0.1:${pb_port}
}
EOF
format_caddyfile_or_restore "$backup_path"
validate_caddyfile_or_restore "$backup_path"
apply_caddy_or_restore "$backup_path"
info "Caddyfile 配置已验证并重载。"
}
remove_caddy_site() {
local domain_name="$1"
local backup_path
local tmp_file
if ! sudo grep -Fq "${domain_name} {" "$CADDYFILE" 2>/dev/null; then
warn "Caddyfile 中未找到域名配置: ${domain_name},跳过 Caddy 配置删除。"
return 0
fi
backup_path=$(backup_caddyfile)
tmp_file=$(mktemp)
warn "正在从 ${CADDYFILE} 删除域名配置: ${domain_name}"
sudo awk -v domain="$domain_name" '
function count_char(str, char, i, count) {
count = 0
for (i = 1; i <= length(str); i++) {
if (substr(str, i, 1) == char) {
count++
}
}
return count
}
$0 ~ "^[[:space:]]*" domain "[[:space:]]*\\{" {
skipping = 1
depth = count_char($0, "{") - count_char($0, "}")
if (depth <= 0) {
skipping = 0
}
next
}
skipping {
depth += count_char($0, "{") - count_char($0, "}")
if (depth <= 0) {
skipping = 0
}
next
}
{ print }
' "$CADDYFILE" > "$tmp_file"
sudo cp "$tmp_file" "$CADDYFILE"
rm -f "$tmp_file"
format_caddyfile_or_restore "$backup_path"
validate_caddyfile_or_restore "$backup_path"
apply_caddy_or_restore "$backup_path"
info "Caddy 域名配置已删除并应用。"
}
is_port_in_use() {
local port="$1"
if command -v ss >/dev/null 2>&1; then
ss -ltn | awk '{print $4}' | grep -Eq "(^|[:.])${port}$"
elif command -v lsof >/dev/null 2>&1; then
lsof -iTCP:"$port" -sTCP:LISTEN -Pn >/dev/null 2>&1
else
return 1
fi
}
find_available_port() {
local start_port="${1:-8090}"
local port="$start_port"
while [ "$port" -le 65535 ]; do
if ! is_port_in_use "$port"; then
echo "$port"
return 0
fi
port=$((port + 1))
done
return 1
}
prompt_available_port() {
local default_port="8090"
local suggested_port
if is_port_in_use "$default_port"; then
if suggested_port=$(find_available_port "$((default_port + 1))"); then
warn "默认端口 ${default_port} 已被占用,建议使用 ${suggested_port}。"
default_port="$suggested_port"
else
error "没有找到可用端口,请手动释放端口后重试。"
return 1
fi
fi
while true; do
read -r -p "请输入此项目实例的端口号 (留空则默认 ${default_port}): " PB_PORT
PB_PORT=${PB_PORT:-"$default_port"}
if ! validate_port "$PB_PORT"; then
error "端口号必须是 1-65535 之间的数字。"
continue
fi
if is_port_in_use "$PB_PORT"; then
error "端口 ${PB_PORT} 已被占用,请换一个端口。"
continue
fi
break
done
}
caddy_site_exists() {
local domain_name="$1"
sudo grep -Fq "${domain_name} {" "$CADDYFILE" 2>/dev/null
}
prompt_domain_name() {
while true; do
read -r -p "请输入您要绑定的域名 (例如: ${PROJECT_NAME}.example.com): " DOMAIN_NAME
if ! validate_domain "$DOMAIN_NAME"; then
error "域名格式不正确。"
continue
fi
if caddy_site_exists "$DOMAIN_NAME"; then
error "Caddyfile 中已存在域名配置: ${DOMAIN_NAME},请换一个域名或先删除旧配置。"
continue
fi
break
done
}
prompt_admin_credentials() {
while true; do
read -r -p "请输入新 PocketBase 实例的管理员邮箱: " ADMIN_EMAIL
if ! validate_email "$ADMIN_EMAIL"; then
error "管理员邮箱格式不正确,请重新输入。"
continue
fi
read -r -s -p "请输入新 PocketBase 实例的管理员密码: " ADMIN_PASS
echo # 换行
if [ -z "$ADMIN_PASS" ]; then
error "管理员密码不能为空,请重新输入。"
continue
fi
break
done
}
get_project_port() {
local project_name="$1"
local service_file_path="/etc/systemd/system/pocketbase-${project_name}.service"
if sudo test -f "$service_file_path"; then
sudo awk '/ExecStart=/ {
if (match($0, /127[.]0[.]0[.]1:[0-9]+/)) {
port = substr($0, RSTART, RLENGTH)
sub(/^127[.]0[.]0[.]1:/, "", port)
print port
exit
}
}' "$service_file_path"
fi
}
get_caddy_domains_by_port() {
local port="$1"
if [ -z "$port" ] || ! sudo test -f "$CADDYFILE"; then
return 0
fi
sudo awk -v port="$port" '
/^[[:space:]]*[^#[:space:]].*\{[[:space:]]*$/ {
site = $0
sub(/[[:space:]]*\{[[:space:]]*$/, "", site)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", site)
}
$0 ~ "reverse_proxy[[:space:]]+(127[.]0[.]0[.]1|localhost):" port "([[:space:]]|$)" {
if (site != "" && site != "{") {
print site
}
}
' "$CADDYFILE" | paste -sd ',' -
}
show_existing_projects() {
local found=0
local project_path
local project_name
local service_name
local active_state
local enabled_state
local port
local domains
echo -e "\n${GREEN}--- 当前检测到的 PocketBase 项目实例 ---${NC}"
if ! sudo test -d "$PB_PROJECTS_DIR"; then
warn "实例目录 ${PB_PROJECTS_DIR} 不存在,暂无可展示的项目。"
return 0
fi
printf '%-24s %-10s %-10s %-8s %s\n' "项目名" "运行状态" "开机自启" "端口" "Caddy 域名"
printf '%-24s %-10s %-10s %-8s %s\n' "------------------------" "----------" "----------" "--------" "----------------"
while IFS= read -r project_path; do
found=1
project_name=$(basename "$project_path")
service_name="pocketbase-${project_name}"
active_state=$(sudo systemctl is-active "$service_name" 2>/dev/null || true)
enabled_state=$(sudo systemctl is-enabled "$service_name" 2>/dev/null || true)
port=$(get_project_port "$project_name")
domains=$(get_caddy_domains_by_port "$port")
[ -n "$active_state" ] || active_state="unknown"
[ -n "$enabled_state" ] || enabled_state="unknown"
[ -n "$port" ] || port="-"
[ -n "$domains" ] || domains="-"
printf '%-24s %-10s %-10s %-8s %s\n' "$project_name" "$active_state" "$enabled_state" "$port" "$domains"
done < <(sudo find "$PB_PROJECTS_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort)
if [ "$found" -eq 0 ]; then
warn "未在 ${PB_PROJECTS_DIR} 下发现项目目录。"
fi
}
# ------------------------------------------------------------------------------
# 函数: 首次安装 PocketBase 和 Caddy
# ------------------------------------------------------------------------------
initial_setup() {
info "--- 1. 开始系统更新与清理 ---"
sudo apt update && sudo apt upgrade -y && sudo apt autoremove -y
info "系统更新完成。"
echo -e "\n${GREEN}--- 2. 安装必要依赖 ---${NC}"
install_dependencies
echo -e "\n${GREEN}--- 3. 下载 PocketBase ---${NC}"
prompt_pocketbase_version_and_download
echo -e "\n${GREEN}--- 4. 安装 Caddy ---${NC}"
install_caddy
echo -e "\n${GREEN}--- 5. 配置 PocketBase 专用用户 ---${NC}"
ensure_pocketbase_user
ensure_projects_dir
echo -e "\n${GREEN}--- 6. 配置 Caddy ---${NC}"
read -r -p "请输入用于 Caddy 自动申请 SSL 证书的邮箱: " CADDY_EMAIL
if ! validate_email "$CADDY_EMAIL"; then
error "邮箱格式不正确。"
return 1
fi
ensure_caddy_email "$CADDY_EMAIL"
echo -e "\n${GREEN}--- 7. 启动并设置 Caddy 开机自启 ---${NC}"
sudo systemctl enable caddy
sudo systemctl start caddy
sudo systemctl status caddy --no-pager || true
info "Caddy 已启动并设置开机自启。"
echo -e "\n${GREEN}--- 8. 配置防火墙 (ufw) ---${NC}"
warn "为 Caddy 开放 HTTP (80) 和 HTTPS (443) 端口..."
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
info "防火墙规则已添加。注意:脚本不会自动启用 ufw,避免影响现有 SSH 连接。"
echo -e "\n${GREEN}🎉 首次安装全部完成!${NC}"
warn "再次执行脚本可以开始添加 PocketBase 项目。"
}
# ------------------------------------------------------------------------------
# 函数: 添加新的 PocketBase 项目实例
# ------------------------------------------------------------------------------
add_project() {
local tmp_dir
info "--- 1. 准备项目环境 ---"
install_dependencies
ensure_caddy_available
ensure_pocketbase_user
ensure_projects_dir
read -r -p "请输入新项目的文件夹名称 (例如: project1): " PROJECT_NAME
if ! validate_project_name "$PROJECT_NAME"; then
error "项目名称只能包含字母、数字、下划线和短横线。"
return 1
fi
WORKING_DIR="${PB_PROJECTS_DIR}/${PROJECT_NAME}"
if sudo test -e "$WORKING_DIR"; then
error "实例目录 ${WORKING_DIR} 已存在。"
return 1
fi
SERVICE_NAME="pocketbase-$PROJECT_NAME"
SERVICE_FILE_PATH="/etc/systemd/system/${SERVICE_NAME}.service"
if sudo test -f "$SERVICE_FILE_PATH"; then
error "服务文件已存在: ${SERVICE_FILE_PATH}"
return 1
fi
echo -e "\n${GREEN}--- 2. 检查实例端口 ---${NC}"
prompt_available_port
warn "PocketBase 将仅监听 127.0.0.1:${PB_PORT},不会开放项目端口到公网。"
echo -e "\n${GREEN}--- 3. 检查 Caddy 域名 ---${NC}"
prompt_domain_name
echo -e "\n${GREEN}--- 4. 检查管理员账号 ---${NC}"
prompt_admin_credentials
prompt_pocketbase_version_and_download
warn "正在创建目录 ${WORKING_DIR} 并解压 PocketBase..."
tmp_dir=$(mktemp -d)
unzip -q "$PB_ZIP_PATH" -d "$tmp_dir"
sudo install -d -o "$PB_SYSTEM_USER" -g "$PB_SYSTEM_GROUP" -m 0750 "$WORKING_DIR"
sudo cp -a "${tmp_dir}/." "$WORKING_DIR/"
rm -rf "$tmp_dir"
sudo chmod +x "${WORKING_DIR}/pocketbase"
sudo chown -R "${PB_SYSTEM_USER}:${PB_SYSTEM_GROUP}" "$WORKING_DIR"
info "项目准备完成,工作目录: ${WORKING_DIR}"
echo -e "\n${GREEN}--- 5. 配置 Caddy 反向代理 ---${NC}"
append_caddy_site "$DOMAIN_NAME" "$PB_PORT"
echo -e "\n${GREEN}--- 6. 创建 systemd 服务 ---${NC}"
warn "正在创建服务文件: ${SERVICE_FILE_PATH}"
sudo tee "$SERVICE_FILE_PATH" > /dev/null <<EOF
[Unit]
Description=PocketBase ${PROJECT_NAME} service
After=network.target
[Service]
User=${PB_SYSTEM_USER}
Group=${PB_SYSTEM_GROUP}
Type=simple
WorkingDirectory=${WORKING_DIR}
ExecStart=${WORKING_DIR}/pocketbase serve --http="127.0.0.1:${PB_PORT}"
Restart=always
RestartSec=3
LimitNOFILE=4096
[Install]
WantedBy=multi-user.target
EOF
info "服务文件创建成功。"
echo -e "\n${GREEN}--- 7. 启动服务并创建 PocketBase 管理员 ---${NC}"
sudo systemctl daemon-reload
sudo systemctl enable "$SERVICE_NAME"
sudo systemctl start "$SERVICE_NAME"
warn "已启动服务,等待 3 秒让 PocketBase 初始化数据库..."
sleep 3
# 停止服务以安全地创建管理员 (防止 "database is locked")
sudo systemctl stop "$SERVICE_NAME"
warn "服务已临时停止,正在创建超级管理员..."
sudo -u "$PB_SYSTEM_USER" bash -c 'cd "$1" && ./pocketbase superuser upsert "$2" "$3"' _ "$WORKING_DIR" "$ADMIN_EMAIL" "$ADMIN_PASS"
warn "管理员创建成功,正在重启服务..."
sudo systemctl start "$SERVICE_NAME"
echo -e "\n${GREEN}服务状态:${NC}"
sudo systemctl status "$SERVICE_NAME" --no-pager || true
echo -e "\n${GREEN}--- 🎉 新项目添加完成! ---${NC}"
echo -e "项目名称: ${YELLOW}${PROJECT_NAME}${NC}"
echo -e "PocketBase 版本: ${YELLOW}${PB_VERSION}${NC}"
echo -e "后台地址: ${YELLOW}https://${DOMAIN_NAME}/_${NC}"
echo -e "管理员邮箱: ${YELLOW}${ADMIN_EMAIL}${NC}"
}
# ------------------------------------------------------------------------------
# 函数: 删除 PocketBase 项目实例
# ------------------------------------------------------------------------------
delete_project() {
local project_name
local working_dir
local service_name
local service_file_path
local domain_name
local detected_port
local detected_domains
local confirm
info "--- 删除 PocketBase 项目实例 ---"
show_existing_projects
echo
read -r -p "请输入要删除的项目名称: " project_name
if ! validate_project_name "$project_name"; then
error "项目名称只能包含字母、数字、下划线和短横线。"
return 1
fi
working_dir="${PB_PROJECTS_DIR}/${project_name}"
service_name="pocketbase-${project_name}"
service_file_path="/etc/systemd/system/${service_name}.service"
detected_port=$(get_project_port "$project_name")
detected_domains=$(get_caddy_domains_by_port "$detected_port")
echo -e "将删除以下资源:"
echo -e " systemd 服务: ${YELLOW}${service_name}${NC}"
echo -e " 服务文件: ${YELLOW}${service_file_path}${NC}"
echo -e " 实例目录: ${YELLOW}${working_dir}${NC}"
echo -e " 检测端口: ${YELLOW}${detected_port:-未检测到}${NC}"
echo -e " 检测域名: ${YELLOW}${detected_domains:-未检测到}${NC}"
echo
if [ -n "$detected_domains" ] && [[ "$detected_domains" != *,* ]]; then
read -r -p "请输入要删除的 Caddy 域名 (回车默认 ${detected_domains},输入 - 跳过): " domain_name
domain_name=${domain_name:-"$detected_domains"}
else
read -r -p "请输入该实例绑定的域名,用于删除 Caddy 配置 (留空则跳过 Caddy 配置删除): " domain_name
fi
if [ "$domain_name" = "-" ]; then
domain_name=""
fi
if [ -n "$domain_name" ] && ! validate_domain "$domain_name"; then
error "域名格式不正确。"
return 1
fi
echo
warn "此操作不可逆,请确认没有需要保留的 PocketBase 数据。"
read -r -p "确认删除项目 ${project_name}? 请输入 DELETE 确认: " confirm
if [ "$confirm" != "DELETE" ]; then
warn "未确认删除,操作已取消。"
return 0
fi
warn "正在停止并禁用 systemd 服务..."
sudo systemctl disable --now "$service_name" 2>/dev/null || true
if sudo test -f "$service_file_path"; then
warn "正在删除服务文件: ${service_file_path}"
sudo rm -f "$service_file_path"
else
warn "未找到服务文件: ${service_file_path}"
fi
sudo systemctl daemon-reload
sudo systemctl reset-failed "$service_name" 2>/dev/null || true
if sudo test -e "$working_dir"; then
warn "正在删除实例目录: ${working_dir}"
sudo rm -rf "$working_dir"
else
warn "未找到实例目录: ${working_dir}"
fi
if [ -n "$domain_name" ]; then
ensure_caddy_available
remove_caddy_site "$domain_name"
fi
echo -e "\n${GREEN}--- 删除完成 ---${NC}"
echo -e "项目名称: ${YELLOW}${project_name}${NC}"
if [ -n "$domain_name" ]; then
echo -e "已处理 Caddy 域名配置: ${YELLOW}${domain_name}${NC}"
else
echo -e "Caddy 配置: ${YELLOW}已跳过${NC}"
fi
}
# ------------------------------------------------------------------------------
# 主菜单
# ------------------------------------------------------------------------------
main_menu() {
while true; do
echo -e "\n${GREEN}=====================================${NC}"
echo -e "${GREEN} PocketBase & Caddy 多项目管理脚本 ${NC}"
echo -e "${GREEN}=====================================${NC}"
echo -e "请选择一个操作:"
echo -e " ${YELLOW}1)${NC} 首次安装 PocketBase 和 Caddy"
echo -e " ${YELLOW}2)${NC} 添加一个新的 PocketBase 项目实例"
echo -e " ${YELLOW}3)${NC} 删除一个 PocketBase 项目实例"
echo -e " ${YELLOW}4)${NC} 退出脚本"
read -r -p "请输入选项 [1-4]: " choice
case "$choice" in
1)
initial_setup
;;
2)
add_project
;;
3)
delete_project
;;
4)
info "再见!"
exit 0
;;
*)
error "无效选项,请输入 1, 2, 3 或 4。"
;;
esac
done
}
# 运行主菜单
main_menu
# 后续步骤:
# 授予执行权限:chmod +x pocketbase.sh
# 运行脚本:./pocketbase.sh授予执行权限
chmod +x pocketbase.sh运行脚本
./pocketbase.sh