caddy Caddy 反代 + HTTPS
要给 VPS 上的网站或应用加 HTTPS 反向代理,以 root 运行下面的命令。它会从 Caddy 官方 GitHub Release 安装 caddy(校验 SHA-512),配置 systemd 服务,并在加 --domain 时写入反向代理站点,Caddy 会自动申请证书。
$ curl -fsSL xyz.sh/caddy | bash
13bfe32bc5f1160009e94bd8d9a1800758c7a4c89d29f0928edc24d7d9e7ad83
· 与 curl 拿到的脚本一致,也可访问 /caddy.sh.sha256
所有脚本都通过 bash -n 与 ShellCheck 检查,并在 Debian 12/11、Ubuntu 24.04/22.04/20.04/18.04、Rocky 9、AlmaLinux 9、CentOS 7 的官方 Docker 镜像中以 root 运行 curl | bash 测试(结果见各页“容器测试结果”)。容器里没有 systemd、内核模块和真实公网线路,相关部分需要在真实 VPS 上确认。
它会做什么
从 Caddy 官方 GitHub Release 下载二进制(默认 v2.11.9,amd64 / arm64 / armv7 的 SHA-512 已固定校验)安装到 /usr/bin/caddy,不添加软件源;创建 caddy 系统用户、/etc/caddy/Caddyfile 和 /var/lib/caddy,安装官方 systemd 单元(来自 caddyserver/dist)并启用。加 --domain example.com --to 127.0.0.1:8080 会写入 example.com { reverse_proxy 127.0.0.1:8080 } 站点块,Caddy 会自动申请并续期 Let's Encrypt / ZeroSSL 证书;--email 设置 ACME 邮箱。每次修改都会先用 caddy validate 校验,失败则恢复备份。使用 --domain 前请先把域名解析到本机并放行 TCP 80/443。
容器测试结果
在各发行版官方 Docker 镜像中以 root、无终端方式运行 curl | bash 的结果。partial 表示容器内无法完整验证(例如没有 systemd、内核模块或测速协议被测试环境的代理拦截),需要在真实 VPS 上确认。 (2026-11-11)
| OS | 结果 |
|---|---|
| debian:12 | pass |
| debian:11 | pass |
| ubuntu:24.04 | pass |
| ubuntu:22.04 | pass |
| ubuntu:20.04 | pass |
| ubuntu:18.04 | pass |
| rockylinux:9 | pass |
| almalinux:9 | pass |
| centos:7 | pass |
常见问题
- 这个脚本安全吗?
- xyz.sh 不埋点、不偷偷执行未说明的操作,完整源码可在页面上查看,也可以用 curl -fsSL xyz.sh/<name>.sh 下载后先读再运行;每个脚本都有 /<name>.sh.sha256 校验值。把远程脚本管道给 bash 仍有风险,请先阅读源码。二进制来自 https://github.com/caddyserver/caddy/releases,默认版本的 SHA-512 已写在脚本里,下载后校验不通过会中止。
- 支持哪些系统?
- 声明支持 Debian、Ubuntu、CentOS、Rocky、AlmaLinux。脚本会检测发行版,无法识别时会提示。已在这些系统的 Docker 容器里做过语法、--dry-run 与可运行部分的测试(结果见 GitHub 上的 tests/RESULTS.md);依赖 systemd、内核模块或公网的部分需要在真实 VPS 上验证。
- 需要 root 吗?
- 需要。请用 root 或 sudo bash 运行;非 root 时脚本会退出并提示。--dry-run 和 --status 可以先看会做什么。
- 证书申请失败怎么办?
- 请确认域名 A/AAAA 记录已指向这台服务器,TCP 80 和 443 在防火墙与安全组都已放行,且没有其它程序(如 Nginx)占用这两个端口。用 journalctl -u caddy 查看详细原因。
完整源码
以下就是 curl 实际拿到的内容(公共库 + 脚本正文)。运行前请先读一遍。
#!/usr/bin/env bash
# Served by xyz.sh/caddy
# Human-readable page: https://xyz.sh/caddy
# This file is the common library followed by the script body.
# No telemetry is added by xyz.sh.
# =============================================================================
# xyz.sh common library
# Shared helpers for every script served by xyz.sh.
#
# Promises:
# - No telemetry, analytics, or background "phone-home" calls
# - No hidden installs or config changes beyond what the script announces
# - Official upstream URLs are printed before any remote code is executed
# - Destructive or system-changing steps ask for confirmation (or YES=1 / -y)
# =============================================================================
if [[ -n "${XYZ_COMMON_LOADED:-}" ]]; then
# When sourced a second time, return. When executed as a file, return fails.
# shellcheck disable=SC2317
return 0 2>/dev/null || true
fi
# shellcheck disable=SC2034
XYZ_COMMON_LOADED=1
set -euo pipefail
XYZ_VERSION="1.3.0"
XYZ_BASE="${XYZ_BASE:-https://xyz.sh}"
XYZ_NAME="${XYZ_NAME:-unknown}"
# --- language ---------------------------------------------------------------
# Override with XYZ_LANG=zh or XYZ_LANG=en.
if [[ -z "${XYZ_LANG:-}" ]]; then
case "${LC_ALL:-${LC_MESSAGES:-${LANG:-}}}" in
zh*|ZH*|cmn*|CMN*) XYZ_LANG="zh" ;;
*) XYZ_LANG="en" ;;
esac
fi
xyz_t() {
# xyz_t "English text" "中文文本"
if [[ "${XYZ_LANG}" == "zh" ]]; then
printf '%s' "${2:-$1}"
else
printf '%s' "$1"
fi
}
# --- colors (TTY only) ------------------------------------------------------
# XYZ_COLOR=always|never|auto (default auto: TTY and no NO_COLOR); --no-color.
_xyz_color=0
case "${XYZ_COLOR:-auto}" in
always) _xyz_color=1 ;;
never) _xyz_color=0 ;;
*) [[ -t 1 && -z "${NO_COLOR:-}" ]] && _xyz_color=1 ;;
esac
[[ "${XYZ_NO_COLOR:-0}" == "1" ]] && _xyz_color=0
if [[ "${_xyz_color}" == "1" ]]; then
XYZ_C_RESET=$'\033[0m'
XYZ_C_BOLD=$'\033[1m'
XYZ_C_DIM=$'\033[2m'
XYZ_C_RED=$'\033[31m'
XYZ_C_GREEN=$'\033[32m'
XYZ_C_YELLOW=$'\033[33m'
XYZ_C_CYAN=$'\033[36m'
else
XYZ_C_RESET=""
XYZ_C_BOLD=""
XYZ_C_DIM=""
XYZ_C_RED=""
XYZ_C_GREEN=""
XYZ_C_YELLOW=""
XYZ_C_CYAN=""
fi
xyz_info() { printf '%s[INFO]%s %s\n' "${XYZ_C_GREEN}" "${XYZ_C_RESET}" "$*"; }
xyz_warn() { printf '%s[WARN]%s %s\n' "${XYZ_C_YELLOW}" "${XYZ_C_RESET}" "$*" >&2; }
xyz_error() { printf '%s[ERROR]%s %s\n' "${XYZ_C_RED}" "${XYZ_C_RESET}" "$*" >&2; }
xyz_step() { printf '%s==>%s %s\n' "${XYZ_C_CYAN}" "${XYZ_C_RESET}" "$*"; }
# --- args / init ------------------------------------------------------------
XYZ_ARGS=()
XYZ_YES=0
XYZ_DRY_RUN=0
xyz_parse_global_args() {
local filtered=()
local arg
for arg in "$@"; do
case "${arg}" in
-y|--yes)
XYZ_YES=1
;;
--lang=zh|zh)
XYZ_LANG="zh"
;;
--lang=en|en)
XYZ_LANG="en"
;;
--dry-run)
XYZ_DRY_RUN=1
;;
--no-color)
XYZ_NO_COLOR=1
XYZ_C_RESET=""; XYZ_C_BOLD=""; XYZ_C_DIM=""; XYZ_C_RED=""
XYZ_C_GREEN=""; XYZ_C_YELLOW=""; XYZ_C_CYAN=""
;;
--help|-h)
XYZ_HELP=1
filtered+=("${arg}")
;;
*)
filtered+=("${arg}")
;;
esac
done
XYZ_ARGS=("${filtered[@]+"${filtered[@]}"}")
if [[ "${YES:-}" =~ ^(1|yes|true|YES)$ || "${XYZ_ASSUME_YES:-}" == "1" ]]; then
XYZ_YES=1
fi
# Exported so individual scripts can read flags set here.
export XYZ_ARGS XYZ_YES XYZ_LANG XYZ_HELP="${XYZ_HELP:-}" XYZ_DRY_RUN="${XYZ_DRY_RUN:-0}"
export XYZ_NO_COLOR="${XYZ_NO_COLOR:-0}"
if declare -F xyz_ui_init >/dev/null; then xyz_ui_init; fi
}
xyz_banner() {
local title="${XYZ_TITLE:-$XYZ_NAME}"
printf '\n%s%sxyz.sh/%s%s v%s\n' "${XYZ_C_BOLD}" "${XYZ_C_CYAN}" "${XYZ_NAME}" "${XYZ_C_RESET}" "${XYZ_VERSION}"
printf '%s\n' "${XYZ_C_DIM}----------------------------------------${XYZ_C_RESET}"
printf ' %s: %s/%s\n' "$(xyz_t "Page" "页面")" "${XYZ_BASE}" "${XYZ_NAME}"
printf ' %s: %s/%s.sh\n' "$(xyz_t "Raw" "源码")" "${XYZ_BASE}" "${XYZ_NAME}"
if [[ -n "${XYZ_UPSTREAM:-}" ]]; then
printf ' %s: %s\n' "$(xyz_t "Upstream" "上游来源")" "${XYZ_UPSTREAM}"
fi
printf ' %s\n' "$(xyz_t "No telemetry, no hidden actions." "无遥测、无隐藏操作。")"
printf '%s\n\n' "${XYZ_C_DIM}----------------------------------------${XYZ_C_RESET}"
if [[ -n "${title}" && "${title}" != "${XYZ_NAME}" ]]; then
xyz_step "${title}"
fi
}
xyz_init() {
xyz_parse_global_args "$@"
xyz_detect_os
xyz_banner
}
# --- root / OS --------------------------------------------------------------
xyz_is_root() {
[[ "${EUID:-$(id -u)}" -eq 0 ]]
}
xyz_require_root() {
if ! xyz_is_root; then
xyz_error "$(xyz_t \
"This script must be run as root. Try: sudo bash" \
"请使用 root 运行。可尝试:sudo bash")"
exit 1
fi
}
xyz_prefer_root() {
if ! xyz_is_root; then
xyz_warn "$(xyz_t \
"Not running as root; some information or tests may be limited." \
"当前不是 root,部分信息或测试可能不完整。")"
fi
}
XYZ_OS_ID=""
XYZ_OS_LIKE=""
XYZ_OS_VERSION=""
XYZ_OS_FAMILY="" # debian | rhel | unknown
XYZ_OS_PRETTY=""
xyz_detect_os() {
XYZ_OS_ID=""
XYZ_OS_LIKE=""
XYZ_OS_VERSION=""
XYZ_OS_FAMILY="unknown"
XYZ_OS_PRETTY="unknown"
if [[ -r /etc/os-release ]]; then
# shellcheck disable=SC1091
. /etc/os-release
XYZ_OS_ID="${ID:-}"
XYZ_OS_LIKE="${ID_LIKE:-}"
XYZ_OS_VERSION="${VERSION_ID:-}"
XYZ_OS_PRETTY="${PRETTY_NAME:-$XYZ_OS_ID}"
export XYZ_OS_ID XYZ_OS_LIKE XYZ_OS_VERSION XYZ_OS_FAMILY XYZ_OS_PRETTY
fi
case "${XYZ_OS_ID}" in
debian|ubuntu)
XYZ_OS_FAMILY="debian"
;;
centos|rhel|rocky|almalinux|alma|fedora)
XYZ_OS_FAMILY="rhel"
;;
*)
case " ${XYZ_OS_ID} ${XYZ_OS_LIKE} " in
*debian*|*ubuntu*) XYZ_OS_FAMILY="debian" ;;
*rhel*|*centos*|*fedora*|*rocky*|*alma*) XYZ_OS_FAMILY="rhel" ;;
*) : ;;
esac
;;
esac
}
xyz_os_supported() {
case "${XYZ_OS_ID}" in
debian|ubuntu|centos|rocky|almalinux) return 0 ;;
*) : ;;
esac
case " ${XYZ_OS_LIKE} " in
*debian*|*ubuntu*|*rhel*|*centos*|*rocky*|*alma*) return 0 ;;
*) : ;;
esac
return 1
}
xyz_require_supported_os() {
xyz_detect_os
if ! xyz_os_supported; then
xyz_warn "$(xyz_t \
"Unrecognized distribution: ${XYZ_OS_ID:-unknown} (${XYZ_OS_PRETTY}). Officially supported: Debian, Ubuntu, CentOS, Rocky, AlmaLinux." \
"未能识别的发行版:${XYZ_OS_ID:-unknown}(${XYZ_OS_PRETTY})。官方支持:Debian、Ubuntu、CentOS、Rocky、AlmaLinux。")"
if ! xyz_confirm "$(xyz_t "Continue anyway?" "仍然继续?")"; then
exit 1
fi
else
xyz_info "$(xyz_t "Detected OS:" "检测到系统:") ${XYZ_OS_PRETTY} (${XYZ_OS_FAMILY})"
fi
}
# --- packages / commands ----------------------------------------------------
xyz_need_cmd() {
command -v "$1" >/dev/null 2>&1
}
xyz_pkg_install() {
xyz_detect_os
if (( $# == 0 )); then
return 0
fi
case "${XYZ_OS_FAMILY}" in
debian)
export DEBIAN_FRONTEND=noninteractive
apt-get update -y || xyz_warn "$(xyz_t "apt-get update reported errors (a repository may be unreachable); trying to install anyway." "apt-get update 报错(可能有软件源无法访问),仍尝试安装。")"
apt-get install -y --no-install-recommends "$@"
;;
rhel)
if xyz_need_cmd dnf; then
dnf install -y "$@"
elif xyz_need_cmd yum; then
yum install -y "$@"
else
xyz_error "$(xyz_t "Neither dnf nor yum is available." "找不到 dnf 或 yum。")"
exit 1
fi
;;
*)
xyz_error "$(xyz_t "Cannot install packages on this OS automatically." "无法在此系统上自动安装软件包。")"
exit 1
;;
esac
}
xyz_ensure_cmds() {
local pkg
local missing=()
for pkg in "$@"; do
if ! xyz_need_cmd "${pkg}"; then
missing+=("${pkg}")
fi
done
if ((${#missing[@]} > 0)); then
xyz_info "$(xyz_t "Installing missing commands:" "正在安装缺失命令:") ${missing[*]}"
xyz_pkg_install "${missing[@]}"
fi
}
# --- confirmation (safe with curl | bash) -----------------------------------
# A readable /dev/tty node is not enough: in containers or curl|bash without a
# controlling terminal, open() fails with ENXIO ("No such device or address").
# [[ -r /dev/tty ]] is true in that case and must not be used as the check.
xyz_open_tty() {
# Opens /dev/tty into XYZ_TTY_FD if it is a real terminal. Caller must close.
local fd
XYZ_TTY_FD=""
# Brace group so a failed open cannot leak "No such device or address".
if { exec {fd}<>/dev/tty; } 2>/dev/null && [[ -t "${fd}" ]]; then
XYZ_TTY_FD="${fd}"
return 0
fi
if [[ -n "${fd:-}" ]]; then
exec {fd}>&- 2>/dev/null || true
fi
return 1
}
xyz_close_tty() {
if [[ -n "${XYZ_TTY_FD:-}" ]]; then
exec {XYZ_TTY_FD}>&- 2>/dev/null || true
XYZ_TTY_FD=""
fi
}
xyz_confirm() {
local prompt="$1"
if [[ "${XYZ_YES}" == "1" ]]; then
xyz_info "$(xyz_t "Auto-confirmed (-y / YES=1):" "已自动确认(-y / YES=1):") ${prompt}"
return 0
fi
local reply=""
if xyz_open_tty; then
printf '%s [y/N] ' "${prompt}" >&"${XYZ_TTY_FD}"
# shellcheck disable=SC2162
read -r reply <&"${XYZ_TTY_FD}" || true
xyz_close_tty
else
xyz_warn "$(xyz_t \
"No TTY available for confirmation. Re-run with -y or YES=1 if you accept." \
"没有可用的终端来确认。若你已了解并同意,请加 -y 或 YES=1 重试。")"
return 1
fi
case "${reply}" in
y|Y|yes|YES|Yes) return 0 ;;
*) return 1 ;;
esac
}
# --- download helpers -------------------------------------------------------
# Create the temp dir in the main shell. xyz_tmpdir is usually called inside
# $(...), a subshell, so creating it (and its EXIT trap) there would delete the
# directory as soon as the subshell returned.
xyz_init_tmpdir() {
if [[ -z "${XYZ_TMPDIR:-}" || ! -d "${XYZ_TMPDIR}" ]]; then
XYZ_TMPDIR="$(mktemp -d "${TMPDIR:-/tmp}/xyzsh.XXXXXX")"
# shellcheck disable=SC2064
trap "rm -rf '${XYZ_TMPDIR}'" EXIT
fi
}
xyz_tmpdir() {
if [[ -z "${XYZ_TMPDIR:-}" || ! -d "${XYZ_TMPDIR}" ]]; then
xyz_error "internal: temp dir not initialised"
exit 1
fi
printf '%s' "${XYZ_TMPDIR}"
}
xyz_fetch() {
# xyz_fetch URL DEST
local url="$1"
local dest="$2"
xyz_info "$(xyz_t "Downloading" "正在下载"): ${url}"
xyz_info "$(xyz_t "Saving to" "保存到"): ${dest}"
if xyz_need_cmd curl; then
curl -fsSL --connect-timeout 20 --max-time 180 --retry 2 --retry-delay 1 \
-o "${dest}" "${url}"
elif xyz_need_cmd wget; then
wget -q --timeout=60 -O "${dest}" "${url}"
else
xyz_error "$(xyz_t "Need curl or wget to download files." "需要 curl 或 wget 才能下载文件。")"
exit 1
fi
if [[ ! -s "${dest}" ]]; then
xyz_error "$(xyz_t "Download failed or file is empty:" "下载失败或文件为空:") ${url}"
exit 1
fi
if xyz_need_cmd sha256sum; then
xyz_info "SHA256: $(sha256sum "${dest}" | awk '{print $1}')"
elif xyz_need_cmd shasum; then
xyz_info "SHA256: $(shasum -a 256 "${dest}" | awk '{print $1}')"
fi
}
xyz_run_upstream() {
# xyz_run_upstream URL [args...]
# Downloads an official installer, shows the URL and a short preview, then
# runs it only after confirmation (unless -y / YES=1).
local url="$1"
shift
local dest
xyz_init_tmpdir
dest="$(xyz_tmpdir)/upstream.sh"
xyz_fetch "${url}" "${dest}"
chmod +x "${dest}"
xyz_info "$(xyz_t "Preview (first 20 lines):" "预览(前 20 行):")"
sed -n '1,20p' "${dest}" || true
printf '%s\n' "${XYZ_C_DIM}...${XYZ_C_RESET}"
# Default --dry-run: verify the download and preview, do not execute.
# Scripts that forward --dry-run to upstream (e.g. docker) set
# XYZ_FORWARD_DRY_RUN=1 so the official script still runs with that flag.
if [[ "${XYZ_DRY_RUN:-0}" == "1" && "${XYZ_FORWARD_DRY_RUN:-0}" != "1" ]]; then
xyz_info "$(xyz_t \
"Dry-run: official script downloaded and previewed; not executing it." \
"演练模式:已下载并预览官方脚本,不会执行。")"
return 0
fi
if ! xyz_confirm "$(xyz_t \
"Run this official upstream script?" \
"运行这份官方上游脚本?")"; then
xyz_error "$(xyz_t "Aborted by user." "已取消。")"
exit 1
fi
xyz_step "$(xyz_t "Executing upstream script" "正在执行上游脚本")"
# Never let the upstream script inherit our stdin: under `curl | bash` that
# is the rest of this very script. Give it the terminal if there is one,
# otherwise /dev/null. Interactive installers set XYZ_UPSTREAM_NEEDS_TTY=1.
if xyz_open_tty; then
local tty_fd="${XYZ_TTY_FD}"
XYZ_TTY_FD=""
bash "${dest}" "$@" <&"${tty_fd}"
local rc=$?
exec {tty_fd}>&- 2>/dev/null || true
return "${rc}"
fi
if [[ "${XYZ_UPSTREAM_NEEDS_TTY:-0}" == "1" ]]; then
xyz_error "$(xyz_t \
"This official installer is interactive and needs a terminal. Log in with SSH and run: bash <(curl -fsSL ${XYZ_BASE}/${XYZ_NAME})" \
"这个官方安装脚本需要交互,必须在终端里运行。请用 SSH 登录后执行:bash <(curl -fsSL ${XYZ_BASE}/${XYZ_NAME})")"
exit 1
fi
bash "${dest}" "$@" </dev/null
}
# --- small shared helpers ----------------------------------------------------
xyz_strip_ansi() {
sed -e 's/\x1b\[[0-9;?]*[A-Za-z]//g' -e 's/\r//g'
}
# Effective SSH port: sshd -T (what sshd really uses) > config files > env > 22.
xyz_effective_ssh_port() {
local port=""
if xyz_need_cmd sshd; then
port="$(sshd -T 2>/dev/null | awk '$1=="port" {print $2; exit}' || true)"
fi
if [[ -z "${port}" && -d /etc/ssh/sshd_config.d ]]; then
port="$(cat /etc/ssh/sshd_config.d/*.conf 2>/dev/null | awk 'tolower($1)=="port" {print $2; exit}' || true)"
fi
if [[ -z "${port}" && -f /etc/ssh/sshd_config ]]; then
port="$(awk 'tolower($1)=="port" {print $2; exit}' /etc/ssh/sshd_config || true)"
fi
if [[ -z "${port}" && -n "${SSH_CONNECTION:-}" ]]; then
port="$(printf '%s' "${SSH_CONNECTION}" | awk '{print $4}')"
fi
printf '%s' "${port:-22}"
}
xyz_has_systemd() {
[[ -d /run/systemd/system ]] && xyz_need_cmd systemctl
}
# Back up a file once per run as FILE.xyz-bak-YYYYmmddHHMMSS and print the path.
xyz_backup_file() {
local f="$1"
local bak
bak="${f}.xyz-bak-$(date +%Y%m%d%H%M%S)"
if [[ -e "${f}" ]]; then
cp -a "${f}" "${bak}"
printf '%s' "${bak}"
fi
}
# --- misc -------------------------------------------------------------------
xyz_usage_exit() {
if [[ "${1:-}" != "" ]]; then
printf '%s\n' "$1"
fi
exit 0
}
XYZ_NAME="caddy"
# xyz.sh/caddy — install Caddy and set up a reverse proxy with automatic HTTPS.
#
# Official project : https://caddyserver.com (https://github.com/caddyserver/caddy)
# Binary : official GitHub release tarball (default v2.11.9),
# SHA-512 pinned for amd64 / arm64 / armv7
#
# What it does:
# - installs /usr/bin/caddy from the official release (no repository added)
# - creates the `caddy` system user, /etc/caddy/Caddyfile, /var/lib/caddy
# - installs the official systemd unit (from caddyserver/dist) and enables it
# - with --domain / --to, adds a site block: example.com { reverse_proxy 127.0.0.1:8080 }
# Caddy then gets and renews a Let's Encrypt / ZeroSSL certificate itself.
# - validates the Caddyfile with `caddy validate` before reloading
#
# Before using --domain: point the domain's A/AAAA record to this server and
# open TCP 80 and 443 (firewall and cloud security group).
#
# Usage:
# curl -fsSL xyz.sh/caddy | bash -s -- -y
# curl -fsSL xyz.sh/caddy | bash -s -- --domain example.com --to 127.0.0.1:8080 -y
# curl -fsSL xyz.sh/caddy | bash -s -- --domain example.com --to 127.0.0.1:8080 --email me@example.com -y
# curl -fsSL xyz.sh/caddy | bash -s -- --status
# curl -fsSL xyz.sh/caddy | bash -s -- --uninstall -y
# curl -fsSL xyz.sh/caddy | bash -s -- --dry-run
#
# Supported: Debian, Ubuntu, CentOS, Rocky, AlmaLinux (x86_64, aarch64,
# armv7). Root required. No telemetry is added by this wrapper.
set -euo pipefail
if [[ -z "${XYZ_COMMON_LOADED:-}" ]]; then
_xyz_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ -f "${_xyz_dir}/lib/common.sh" ]]; then
# shellcheck source=lib/common.sh
source "${_xyz_dir}/lib/common.sh"
else
echo "xyz.sh: missing lib/common.sh" >&2
exit 1
fi
fi
XYZ_NAME="caddy"
XYZ_TITLE="$(xyz_t "Caddy reverse proxy with automatic HTTPS" "Caddy 反向代理 + 自动 HTTPS")"
XYZ_UPSTREAM="https://github.com/caddyserver/caddy/releases"
CADDY_VERSION="${CADDY_VERSION:-2.11.9}"
CADDYFILE="/etc/caddy/Caddyfile"
UNIT="/etc/systemd/system/caddy.service"
xyz_init "$@"
DOMAIN=""; UPSTREAM=""; EMAIL=""; DO_STATUS=0; DO_UNINSTALL=0; INSTALL_ONLY=0
i=0
while [[ $i -lt ${#XYZ_ARGS[@]} ]]; do
arg="${XYZ_ARGS[$i]}"
case "${arg}" in
--domain) i=$((i + 1)); DOMAIN="${XYZ_ARGS[$i]:-}" ;;
--to|--upstream) i=$((i + 1)); UPSTREAM="${XYZ_ARGS[$i]:-}" ;;
--email) i=$((i + 1)); EMAIL="${XYZ_ARGS[$i]:-}" ;;
--install-only) INSTALL_ONLY=1 ;;
--status) DO_STATUS=1 ;;
--uninstall|--remove) DO_UNINSTALL=1 ;;
--help|-h) XYZ_HELP=1 ;;
*)
xyz_error "$(xyz_t "Unknown argument:" "未知参数:") ${arg}"
exit 1
;;
esac
i=$((i + 1))
done
if [[ "${XYZ_HELP:-}" == "1" ]]; then
cat <<HINT
$(xyz_t "Usage:" "用法:")
curl -fsSL xyz.sh/caddy | bash -s -- -y
curl -fsSL xyz.sh/caddy | bash -s -- --domain example.com --to 127.0.0.1:8080 -y
curl -fsSL xyz.sh/caddy | bash -s -- --domain example.com --to 127.0.0.1:8080 --email me@example.com -y
curl -fsSL xyz.sh/caddy | bash -s -- --status
curl -fsSL xyz.sh/caddy | bash -s -- --uninstall -y
$(xyz_t "Point DNS to this server and open TCP 80/443 before using --domain." "使用 --domain 前,请先把域名解析到本机并放行 TCP 80/443。")
HINT
exit 0
fi
if [[ -n "${DOMAIN}" || -n "${UPSTREAM}" ]]; then
[[ -n "${DOMAIN}" && -n "${UPSTREAM}" ]] || { xyz_error "$(xyz_t "--domain and --to go together." "--domain 和 --to 需要同时提供。")"; exit 1; }
[[ "${DOMAIN}" =~ ^[A-Za-z0-9*]([A-Za-z0-9.-]*[A-Za-z0-9])?$ ]] || { xyz_error "--domain: invalid host name"; exit 1; }
UP_RE='^(https?://)?[][A-Za-z0-9.:-]+(/[^[:space:]]*)?$'
[[ "${UPSTREAM}" =~ ${UP_RE} ]] || { xyz_error "--to: like 127.0.0.1:8080"; exit 1; }
fi
[[ -z "${EMAIL}" || "${EMAIL}" =~ ^[^@[:space:]]+@[^@[:space:]]+\.[^@[:space:]]+$ ]] || { xyz_error "--email: invalid"; exit 1; }
if [[ "${DO_STATUS}" == "1" ]]; then
if xyz_need_cmd caddy; then caddy version; else xyz_info "$(xyz_t "Caddy is not installed." "未安装 Caddy。")"; fi
xyz_has_systemd && systemctl is-active caddy 2>/dev/null | sed 's/^/ caddy.service: /' || true
[[ -f "${CADDYFILE}" ]] && { xyz_info "${CADDYFILE}:"; sed 's/^/ /' "${CADDYFILE}"; }
exit 0
fi
xyz_require_root
xyz_require_supported_os
if [[ "${DO_UNINSTALL}" == "1" ]]; then
xyz_info "$(xyz_t "Will stop caddy and remove /usr/bin/caddy and ${UNIT}. /etc/caddy and /var/lib/caddy (certificates) are kept." "将停止 caddy 并删除 /usr/bin/caddy 与 ${UNIT}。保留 /etc/caddy 和 /var/lib/caddy(证书)。")"
if [[ "${XYZ_DRY_RUN:-0}" == "1" ]]; then xyz_info "$(xyz_t "Dry-run: nothing removed." "演练模式:未删除。")"; exit 0; fi
xyz_confirm "$(xyz_t "Uninstall Caddy?" "卸载 Caddy?")" || exit 1
if xyz_has_systemd; then systemctl disable --now caddy 2>/dev/null || true; fi
rm -f /usr/bin/caddy "${UNIT}"
xyz_has_systemd && systemctl daemon-reload || true
xyz_info "$(xyz_t "Removed." "已删除。")"
exit 0
fi
caddy_arch() {
case "$(uname -m)" in
x86_64|amd64) printf 'amd64' ;;
aarch64|arm64) printf 'arm64' ;;
armv7l|armv7*) printf 'armv7' ;;
*) xyz_error "$(xyz_t "Unsupported architecture:" "不支持的架构:") $(uname -m)"; exit 1 ;;
esac
}
caddy_sha512() {
[[ "${CADDY_VERSION}" == "2.11.9" ]] || return 1
case "$1" in
amd64) printf '0e85544340290a16ce4bfb76fe7d65e9de1bf3a6e67f5ccfff30b30d27ea4c73e078077b7bf3e58ae60b6b12e44327705287059c775bca5782e8ee85ea7edd75' ;;
arm64) printf '9e5e76d7888542924de2c25353ae87c5f77e2a22c5b1198633cae13688922282e05383d142599205c88d8d80eee5e967d291214d7abce4835329a191b20dd6e0' ;;
armv7) printf 'ea042518bfb5eec5070a3119430263ae920b2d1e878c733a80d3d0ced1168afe5816362f295de46915d651a67f31f4d8a67ac67326f507d90cda71274b8ee689' ;;
*) return 1 ;;
esac
}
ARCH="$(caddy_arch)"
TARBALL="caddy_${CADDY_VERSION}_linux_${ARCH}.tar.gz"
URL="https://github.com/caddyserver/caddy/releases/download/v${CADDY_VERSION}/${TARBALL}"
NEED_BIN=1
if xyz_need_cmd caddy && caddy version 2>/dev/null | grep -q "^v${CADDY_VERSION}"; then
NEED_BIN=0
fi
xyz_step "$(xyz_t "Planned changes" "计划变更")"
if [[ "${NEED_BIN}" == "1" ]]; then
printf ' %s: %s -> /usr/bin/caddy\n' "$(xyz_t "Install" "安装")" "${URL}"
else
printf ' %s: %s\n' "$(xyz_t "Already installed" "已安装")" "$(caddy version | head -1)"
fi
printf ' %s: caddy, %s, /var/lib/caddy, %s\n' "$(xyz_t "System user / files" "系统用户 / 文件")" "${CADDYFILE}" "${UNIT}"
if [[ -n "${DOMAIN}" ]]; then
printf ' %s: %s { reverse_proxy %s }\n' "$(xyz_t "Site" "站点")" "${DOMAIN}" "${UPSTREAM}"
resolved="$(getent ahostsv4 "${DOMAIN}" 2>/dev/null | awk '{print $1; exit}' || true)"
local_ip="$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src"){print $(i+1); exit}}' || true)"
printf ' DNS %s -> %s (%s %s)\n' "${DOMAIN}" "${resolved:-?}" "$(xyz_t "this server's route source:" "本机出口地址:")" "${local_ip:-?}"
if [[ -z "${resolved}" ]]; then
xyz_warn "$(xyz_t "${DOMAIN} does not resolve yet; HTTPS certificates will fail until it does." "${DOMAIN} 目前无法解析;在解析生效前证书申请会失败。")"
fi
fi
if xyz_need_cmd ss; then
busy="$(ss -ltnp 2>/dev/null | awk '$4 ~ /[:.](80|443)$/' | grep -v caddy || true)"
[[ -n "${busy}" ]] && xyz_warn "$(xyz_t "Ports 80/443 are used by another program:" "80/443 端口已被其它程序占用:") $(printf '%s' "${busy}" | awk '{print $4, $6}' | tr '\n' ' ')"
fi
if [[ "${XYZ_DRY_RUN:-0}" == "1" ]]; then
xyz_info "$(xyz_t "Dry-run: nothing installed or written." "演练模式:未安装、未写入任何内容。")"
exit 0
fi
xyz_confirm "$(xyz_t "Install / configure Caddy?" "安装 / 配置 Caddy?")" || { xyz_error "$(xyz_t "Aborted by user." "已取消。")"; exit 1; }
if [[ "${NEED_BIN}" == "1" ]]; then
xyz_need_cmd tar || xyz_ensure_cmds tar
xyz_init_tmpdir
T="$(xyz_tmpdir)"
xyz_fetch "${URL}" "${T}/${TARBALL}"
want="$(caddy_sha512 "${ARCH}" || true)"
if [[ -z "${want}" ]]; then
xyz_fetch "https://github.com/caddyserver/caddy/releases/download/v${CADDY_VERSION}/caddy_${CADDY_VERSION}_checksums.txt" "${T}/sums.txt"
want="$(awk -v f="${TARBALL}" '$2==f {print $1}' "${T}/sums.txt")"
fi
got="$(sha512sum "${T}/${TARBALL}" | awk '{print $1}')"
if [[ -z "${want}" || "${want}" != "${got}" ]]; then
xyz_error "$(xyz_t "SHA-512 mismatch for" "SHA-512 不匹配:") ${TARBALL}"
exit 1
fi
xyz_info "$(xyz_t "SHA-512 verified." "SHA-512 已校验。")"
tar -xzf "${T}/${TARBALL}" -C "${T}" caddy
install -m 0755 "${T}/caddy" /usr/bin/caddy
xyz_info "$(xyz_t "Installed" "已安装") $(/usr/bin/caddy version | head -1)"
fi
if ! getent group caddy >/dev/null; then groupadd --system caddy; fi
if ! id caddy >/dev/null 2>&1; then
useradd --system --gid caddy --create-home --home-dir /var/lib/caddy --shell /usr/sbin/nologin --comment "Caddy web server" caddy 2>/dev/null \
|| useradd --system -g caddy -m -d /var/lib/caddy -s /sbin/nologin caddy
fi
mkdir -p /etc/caddy /var/lib/caddy
chown -R caddy:caddy /var/lib/caddy
if [[ ! -f "${CADDYFILE}" ]]; then
{
printf '# Caddyfile written by xyz.sh/caddy %s. Docs: https://caddyserver.com/docs/caddyfile\n' "${XYZ_VERSION}"
if [[ -n "${EMAIL}" ]]; then printf '{\n\temail %s\n}\n\n' "${EMAIL}"; fi
if [[ -z "${DOMAIN}" ]]; then
printf ':80 {\n\trespond "Caddy is running (installed by xyz.sh/caddy)."\n}\n'
fi
} > "${CADDYFILE}"
fi
BAK=""
if [[ -n "${DOMAIN}" ]]; then
BAK="$(xyz_backup_file "${CADDYFILE}")"
# Replace an earlier xyz.sh block for the same domain.
sed -i "/^# BEGIN xyz.sh site ${DOMAIN//./\\.}\$/,/^# END xyz.sh site ${DOMAIN//./\\.}\$/d" "${CADDYFILE}"
# The placeholder :80 site would conflict with HTTP->HTTPS redirects; drop it.
sed -i '/^:80 {$/,/^}$/d' "${CADDYFILE}"
cat >> "${CADDYFILE}" <<SITE
# BEGIN xyz.sh site ${DOMAIN}
${DOMAIN} {
reverse_proxy ${UPSTREAM}
encode zstd gzip
}
# END xyz.sh site ${DOMAIN}
SITE
fi
if ! /usr/bin/caddy validate --config "${CADDYFILE}" --adapter caddyfile >/dev/null 2>&1; then
xyz_error "$(xyz_t "caddy validate rejected the Caddyfile:" "caddy validate 校验 Caddyfile 失败:")"
/usr/bin/caddy validate --config "${CADDYFILE}" --adapter caddyfile 2>&1 | tail -5 || true
[[ -n "${BAK}" ]] && cp -a "${BAK}" "${CADDYFILE}" && xyz_info "$(xyz_t "Restored" "已恢复") ${BAK}"
exit 1
fi
xyz_info "$(xyz_t "caddy validate: Caddyfile is valid." "caddy validate:Caddyfile 有效。")"
/usr/bin/caddy fmt --overwrite "${CADDYFILE}" >/dev/null 2>&1 || true
chown -R root:caddy /etc/caddy
mkdir -p "$(dirname "${UNIT}")"
cat > "${UNIT}" <<'UNITFILE'
# Official unit from https://github.com/caddyserver/dist (caddy.service), installed by xyz.sh/caddy
[Unit]
Description=Caddy
Documentation=https://caddyserver.com/docs/
After=network.target network-online.target
Requires=network-online.target
[Service]
Type=notify
User=caddy
Group=caddy
ExecStart=/usr/bin/caddy run --environ --config /etc/caddy/Caddyfile
ExecReload=/usr/bin/caddy reload --config /etc/caddy/Caddyfile --force
TimeoutStopSec=5s
LimitNOFILE=1048576
PrivateTmp=true
ProtectSystem=full
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
[Install]
WantedBy=multi-user.target
UNITFILE
if [[ "${INSTALL_ONLY}" == "1" ]]; then
xyz_info "$(xyz_t "--install-only: not starting the service." "--install-only:不启动服务。")"
exit 0
fi
if xyz_has_systemd; then
systemctl daemon-reload
if systemctl is-active --quiet caddy; then
systemctl reload caddy
else
systemctl enable --now caddy
fi
systemctl --no-pager --lines=0 status caddy || true
[[ -n "${DOMAIN}" ]] && xyz_info "$(xyz_t "Caddy will request a certificate for ${DOMAIN}. Check: journalctl -u caddy -f, then open https://${DOMAIN}" "Caddy 会为 ${DOMAIN} 申请证书。查看日志:journalctl -u caddy -f,然后访问 https://${DOMAIN}")"
xyz_info "$(xyz_t "Caddy is running." "Caddy 已运行。")"
else
xyz_warn "$(xyz_t "No systemd here (container?). Installed and validated; start manually: caddy start --config ${CADDYFILE}" "当前没有 systemd(可能是容器)。已安装并校验配置,请手动启动:caddy start --config ${CADDYFILE}")"
fi
推荐 VPS
以下为占位展示,尚未接入任何商家或联盟链接,也没有可点击的购买地址。上线推广前会再填入真实套餐与合规的返佣链接。
商家待定
机房、配置与价格将在接入后填写。当前不是购买入口。
商家待定
机房、配置与价格将在接入后填写。当前不是购买入口。