caddy Caddy reverse proxy + HTTPS
To put an app on your VPS behind an HTTPS reverse proxy, run the command below as root. It installs caddy from the official GitHub release (SHA-512 verified), sets up the systemd service, and with --domain writes a reverse-proxy site so Caddy obtains a certificate automatically.
$ curl -fsSL xyz.sh/caddy | bash
13bfe32bc5f1160009e94bd8d9a1800758c7a4c89d29f0928edc24d7d9e7ad83
· Matches the script curl receives. You can also fetch /caddy.sh.sha256
Every script passes bash -n and ShellCheck and was tested with curl | bash as root in the official Docker images of Debian 12/11, Ubuntu 24.04/22.04/20.04/18.04, Rocky 9, AlmaLinux 9 and CentOS 7 (see “Container test results” on each page). Containers lack systemd, kernel modules and real public routes, so those parts still need a real VPS.
What it does
Downloads the official binary from Caddy's GitHub releases (default v2.11.9, SHA-512 pinned for amd64 / arm64 / armv7) into /usr/bin/caddy without adding a repository; creates the caddy system user, /etc/caddy/Caddyfile and /var/lib/caddy; installs and enables the official systemd unit from caddyserver/dist. With --domain example.com --to 127.0.0.1:8080 it writes an example.com { reverse_proxy 127.0.0.1:8080 } site block and Caddy obtains and renews a Let's Encrypt / ZeroSSL certificate automatically; --email sets the ACME email. Every change is checked with caddy validate and the backup is restored on failure. Point DNS to the server and open TCP 80/443 before using --domain.
Container test results
Results of curl | bash as root without a terminal in each distribution's official Docker image. partial means the container could not verify everything (for example no systemd, no kernel module, or the test protocol blocked by the test environment's proxy) and a real VPS is needed to confirm. (2026-11-11)
| OS | Result |
|---|---|
| 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 |
FAQ
- Is this script safe?
- xyz.sh adds no telemetry and performs no hidden actions. Read the full source on the page or download it with curl -fsSL xyz.sh/<name>.sh first; every script has a /<name>.sh.sha256 checksum. Piping a remote script to bash is still a risk — review the source first. The binary comes from https://github.com/caddyserver/caddy/releases; the default version's SHA-512 is pinned in the script and a mismatch aborts.
- Which operating systems are supported?
- Declared support: Debian, Ubuntu, CentOS, Rocky Linux, and AlmaLinux. The script detects the distribution and warns on an unknown one. It was tested in Docker containers of these systems (syntax, --dry-run, and whatever can run in a container; see tests/RESULTS.md); parts that need systemd, kernel modules, or the public internet still need a real VPS.
- Do I need root?
- Yes. Run it as root or via sudo bash; it exits with a message otherwise. --dry-run and --status show what it would do first.
- The certificate request failed — why?
- Make sure the domain's A/AAAA records point to this server, TCP 80 and 443 are open in the firewall and security group, and nothing else (such as Nginx) uses those ports. journalctl -u caddy shows details.
Full source
This is exactly what curl receives (common library + script body). Read it before running.
#!/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
Recommended VPS
These slots are placeholders. No vendor or affiliate link has been added, and nothing here is a purchase button. Real plans and compliant referral links will be filled in later.
Vendor TBD
Region, specs, and price will be filled in after onboarding. This is not a checkout link.
Vendor TBD
Region, specs, and price will be filled in after onboarding. This is not a checkout link.