1. 环境一览
| 依赖 | 版本 | 描述 |
|---|---|---|
| Ubuntu | jammy | 运行环境 |
| ddns-go | 6.14.1 | 动态ip与域名绑定方案 |
| pz-web-backend | 1.0.4(LATEST) | Web配置面板,管理僵毁服务器的配置文件 |
| docker-compose | v2.29.7-desktop.1 | docker-compose |
| docker-engine | 27.3.1, build ce12230 | docker-engine |
| FileBrowser | 2.54.0 | 文件管理工具,当Web配置面板不满足要求时,自行调整配置文件 |
| SteamCMD | LATEST | SteamCMD |
| Supervisord | 4.2.1 | 多任务进程管理工具 |
| Project Zomboid Dedicated Server | 42.13.1 | 僵毁的独立服务器 |
2. 思路
其实正常搭建一个全自动的僵毁独立服务器,无非需要考虑以下内容:
-
运行环境:因为我不太熟悉
Debian,所以选择了Ubuntu:jammy。 -
家庭宽带IP变动:
ddns-go现成项目解决域名与宽带IP绑定的问题。 -
steam登录以及游戏下载:SteamCMD。 -
游戏配置:主要是改动
Server.ini以及SandboxVar.lua文件。 -
游戏文件管理:
Filebrowser登录直接修改上述的两个文件。 -
游戏文件可视化管理:自己写一个
Web应用来做管理,从功能上来说无非就是以下九个功能:- 读取文件
- 写入文件
- 重启服务器
- 执行服务器更新
- 查找以及应用模组
- 应用自更新
- 配置可视化(UI)
- 游戏日志
- 多语言翻译
从功能拆分角度考虑,这个面板应该是一个独立项目拆出去,因为服务端的基础设施其实是比较固定的,但是面板会迭代的比较快,也可能会支持比较多的功能,
Docker容器的发布本身就很重,不太适合放一块。 -
多任务管理:
supervisord -
HTTPS:acme.sh脚本配合CF_TOKEN以及CF_Account_ID进行自动化证书申请,当然,如果直接提供cert.pem以及key.pem也支持。满足我使用HTTPS访问面板的需求。这里仅做
Cloudflare申请证书支持,毕竟是个免费的互联网基石,其它的做来吃力不讨好,本质上还是我自用的东西。 -
Nginx: 做游戏文件可视化管理以及游戏文件管理的反代,将它们暴露的端口收敛至一个位置,用URL作为区分,另外Nginx也支持基本的Auth功能,这样就能避免我单独为Web配置面板做权限管理功能,一举两得。
3. 实现
3.1 构建一个运行环境
首先,我们使用ubuntu:jammy镜像:
# syntax=docker/dockerfile:1# 启用高级构建特性,这玩意儿是定义解析器指令,使用后可以在Run指令中使用--mount等参数,使用HEREDOC 语法,像写普通的 shell 脚本一样编写多行命令。
# 指定基础镜像FROM ubuntu:jammy**特别注意:**HEREDOC的特性比较严格,一个是不允许在\内外有其它字符,所以也就不支持在里面写注释,第二个是变量声明还不能有空格做格式上的美观,这里踩了一段时间的坑。
如果有使用过Linux系统的经验,在中国大陆你会很自然而然地想到换国内的源。
但项目未必需要源,所以我们要给个开关:
# 设置一个启动时传输参数,是否使用功能国内源(默认为 true)ARG USE_CN_MIRROR=true# 当然,你也可以声明为ENV环境变量,这样在别的文件中也可以使用.# ENV USE_CN_MIRROR=true
# <<EOF ... EOF这种用法就像是function() {...} 这样的函数区块一样RUN <<EOF# 更换国内源(USE_CN_MIRROR=true 时启用)if [ "$USE_CN_MIRROR" = "true" ]; then echo "Switching to CN mirror..." # 使用 ; 作为分隔符,或者直接换行,两种 sed 命令可以合成一个 # sed 是 "Stream Editor"(流编辑器)的缩写,主要用于文本处理。 # sed 's/要查找的内容/要替换成的内容/g' 文件名 # -i 不输出结果打印到屏幕上,且直接应用 # -e 表达式/脚本,在一个sed命令中执行多次编辑规则,无需重复调用sed # 下面表达式的主要功能,指的是将文件内的archive.ubuntu.com或者security.ubuntu.com改为mirrors.aliyun.com sed -i \ -e 's/archive.ubuntu.com/mirrors.aliyun.com/g' \ -e 's/security.ubuntu.com/mirrors.aliyun.com/g' \ /etc/apt/sources.listfiEOF因为这个系统镜像是极简系统,我们需要安装一下可能需要的依赖:
# 启用 32 位架构(SteamCMD 必须)并安装依赖# dpkg --add-architecture i386 启用 32 位架构(SteamCMD 必须)# --mount 这是BuildKit高级功能,持久化缓存下面这些依赖,免得你DockerFile后又要跑去下一遍。# target 就是缓存的目录,这里分两个,一个是apt用来下载.deb安装包的地方,另一个是apt-get update下载的软件包list信息。# sharing缓存锁,多个构建任务同时运行时,缓存不会出错。# apt-get就不用介绍了吧?安装依赖用的。RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ <<EOF# 脚本出错时立即退出set -e# --- 准备基础环境 ---echo "Adding 32-bit architecture for SteamCMD..."dpkg --add-architecture i386# --- 更新软件包列表 (会使用高速缓存) ---echo "Updating package lists..."apt-get update
# --- 安装所有依赖 ---echo "Installing dependencies..."RAW_DEPS_LIST=" # software-properties-common: 提供 add-apt-repository 等管理软件源的工具。 software-properties-common # lib32gcc-s1: SteamCMD 和游戏运行必需的 32 位基础库。 lib32gcc-s1 # apache2-utils: 提供 htpasswd 命令,用于 Nginx Basic Auth 认证。 apache2-utils # ca-certificates: 保证 HTTPS 连接的安全性,是 curl, wget 等命令能正常工作的基础。 ca-certificates # curl, wget, git: 下载和版本控制工具。 curl wget git # cron: 任务计划程序,用于定期备份等。 cron # locales: 本地化与字符集支持,防止控制台乱码。 locales # gosu: 一个比 sudo 更安全、更适合在容器中切换用户的工具。 gosu # supervisor: 进程管理器,我们用它来同时运行游戏、Nginx 等。 supervisor # nginx: 高性能的反向代理服务器。 nginx # socat: 强大的网络工具。 socat"# 捕获上面字符串后面的#并全部删除,HEREDOC的特性比较严格,一个是不允许在\内外有其它自负,所以也就不支持在里面写注释,第二个是变量声明还不能有空格做格式上的美观,这里踩了一段时间的坑。CLEAN_DEPS=$(echo "$RAW_DEPS_LIST" | sed '/^\s*#/d;/^\s*$/d')apt-get install -y $CLEAN_DEPS
# --- 清理 ---echo "Cleaning up apt lists to reduce image size..."rm -rf /var/lib/apt/lists/*EOF
# 配置语言环境(防止僵毁控制台乱码)# 需要在locales下载后才能使用。RUN locale-gen en_US.UTF-8ENV LANG=en_US.UTF-8ENV LANGUAGE=en_US:enENV LC_ALL=en_US.UTF-83.2 FileBrowser安装
既然涉及到了github,要考虑到大陆访问的问题,这里要设置一个GITHUB_PROXY_URL给github相关资源加速。
只要在.env文件中设置了变量,就可以直接在DockerFile中通过$变量的形式访问,无需显式声明ENV
显式声明ENV是为了让其它文件(比如说后面可能会涉及到的僵毁服务器启动之类的代码)用到。
ARG则是在当前DockerFile中可以使用,明确告诉看文件的人这不是魔法变量。
# 获取FileBrowserRUN <<EOF# 脚本出错时立即退出set -e
# --- 检测 CPU 架构 ---# dpkg打印当前处理器架构,然后去判断是amd64(x86)还是arm64(arm)架构,因为Filebrowser不支持arm结构的处理器。ARCH=$(dpkg --print-architecture)echo "Detected architecture: $ARCH"
case "$ARCH" in "amd64") FB_ARCH="linux-amd64" ;; "arm64") FB_ARCH="linux-arm64" ;; *) echo "Unsupported architecture for FileBrowser: $ARCH" exit 1 ;;esac
# --- 构造下载 URL ---# 定义基础 URLFB_URL="https://github.com/filebrowser/filebrowser/releases/latest/download/${FB_ARCH}-filebrowser.tar.gz"
# 根据构建参数决定是否添加代理前缀# -n的意思是"non-zero length" 或者 "not empty",当然,你也可以写成"$GITHUB_PROXY_URL"!=""# echo其实就是控制台输出这么一串字符,相当于打印logif [ -n "$GITHUB_PROXY_URL" ]; then echo "Using GitHub proxy: $GITHUB_PROXY_URL" DOWNLOAD_URL="${GITHUB_PROXY_URL}/${FB_URL}"else DOWNLOAD_URL="${FB_URL}"fi# --- 下载并安装 ---echo "Downloading FileBrowser from: $DOWNLOAD_URL"# curl# -f -fail返回错误会直接失败退出# -s -silent 静默工作# -S -show-error安静,但出错了还是要输出log# -L -location 跟着重定向走# tar 用于处理.tar文件(解压缩之类的也有)# -x --extract 提取文件# -z --gzip 告诉tar这个数据是gzip算法压缩,需要用gunzip解压才能处理# -C --directory /usr/local/bin 解压后放入/usr/local/bin# filebrowser 指定只要这个文件curl -fsSL "$DOWNLOAD_URL" | tar -xz -C /usr/local/bin filebrowser
# --- 赋予执行权限 ---chmod +x /usr/local/bin/filebrowserecho "FileBrowser installed successfully."EOFTips:关于权限与chmod,在Linux系统里头使用ls -l你会看到详细的信息,譬如rwxr-xr-x这样的部分,先给一份列表:
| 用户类别 | 符号 | 描述 |
|---|---|---|
| User (所有者) | u | 创建该文件或目录的用户。 |
| Group (所属组) | g | 文件所属的用户组。一个组可以包含多个用户。 |
| Others (其他人) | o | 系统中既不是所有者也不是所属组成员的所有其他用户。 |
| All(所有人) | a | 系统中的所有用户 |
| 权限类型 | 符号 | 数字值 | 对 文件 的意义 | 对 目录 的意义 |
|---|---|---|---|---|
| Read (读) | r | 4 | 可以读取文件的内容 (cat, less)。 | 可以列出目录中的文件和子目录列表 (ls)。 |
| Write (写) | w | 2 | 可以修改文件的内容 (vim, echo >)。 | 可以在目录中创建、删除、重命名文件和子目录。 |
| Execute (执行) | x | 1 | 可以作为程序来运行 (./myscript.sh)。 | 可以进入 (cd) 该目录。 |
| 操作符 | 描述 |
|---|---|
| + | 添加权限 |
| - | 移除权限 |
| = | 设置为指定的权限(覆盖原有权限) |
chmod有两种使用模式:
- 符号模式
- chmod u+x dir or file:给所有者添加执行权限
- chmod g-w dir or file:移除用户组写权限
- ……
- 数字模式(一般都是直接用数字)
- 7 = 4+2+1 (rwx)
- 6 = 4+2(rw-)
- 5 = 4+1(r-x)
- 4 = 4 (r—)
- 0 = 0 (---)
综合上面所说,rwxr-xr-x指的就是-> 所有者rwx(读写执行,即任何操作),组织与其他人不能写,但可以执行和读取。
chmod 0777中的0仅仅指的是传递8进制数据,777就是所有人可以对文件进行任何操作。
请仔细辨别读取权限与chmod操作时的区别,读取到的权限rwxr-xr-x中的-跟chmod符号模式下u+x/u-x中符号的含义是不一样的,读取到的权限-这个符号代表的是省略,也就是没有的意思,仅仅作为占位符存在,而chmod里面的符号是以操作符号的形式存在。
3.3 安装SteamCMD
# 手动安装 SteamCMD# 相当于cd /home...WORKDIR /home/steam/steamcmd#切换用户为'steam'这个用户USER steamRUN <<EOF# 脚本出错时立即退出set -e
# --- 定义下载地址 ---OFFICIAL_URL="https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz"CN_URL="https://media.st.dl.eccdnx.com/client/installer/steamcmd_linux.tar.gz"DOWNLOAD_URL=""
# --- 根据构建参数选择 URL ---if [ "$USE_CN_MIRROR" = "true" ]; then echo "--> Using Steam China CDN..." DOWNLOAD_URL="$CN_URL"else echo "--> Using International CDN..." DOWNLOAD_URL="$OFFICIAL_URL"fi
# --- 下载并解压 ---echo "--> Downloading SteamCMD from: $DOWNLOAD_URL"curl -fsSL "$DOWNLOAD_URL" | tar -zxvf -
echo "--> SteamCMD installed successfully."EOF3.4 拉取Web配置面板仓库
# .env文件内定义:# PZ_SETTING_WEB_REPO: Web配置面板的仓库 user/repo模式# PZ_WEB_BACKEND_FILENAME: LATEST Release的文件名,为了跟前缀代理适配,只能无奈给一个固定的文件名# $GITHUB_PROXY_URL: 前面提到的GITHUB加速代理
RUN <<EOF# 脚本出错时立即退出set -e
# --- 构造指向最新版的链接 ---echo "Constructing GitHub download URL..."LATEST_URL="https://github.com/${PZ_SETTING_WEB_REPO}/releases/latest/download/${PZ_WEB_BACKEND_FILENAME}"
# --- 检查并应用代理 ---if [ -n "$GITHUB_PROXY_URL" ]; then echo "--> Using GitHub proxy: $GITHUB_PROXY_URL" DOWNLOAD_URL="${GITHUB_PROXY_URL}${LATEST_URL}"else DOWNLOAD_URL="${LATEST_URL}"fi
# --- 下载并安装 ---echo "--> Downloading latest version from: $DOWNLOAD_URL"# -L 参数是必须的,用于跟随 GitHub 的 latest release 重定向# pz-web-backend-default 放在这里头是为了不覆盖真正的pz-web-backend目录,后续还要判断一下是否要把下载的东西挪过去,既然我们把面板拆成了两个项目,如果pz-web-backend里面有东西,且没有启用self-update更新,那我们就不覆盖。curl -fSL "$DOWNLOAD_URL" -o /usr/local/share/pz-web-backend-default
# --- 赋予执行权限 ---chmod +x /usr/local/share/pz-web-backend-defaultecho "--> pz-web-backend downloaded and installed successfully."EOF3.5 备菜准备完毕,做一些预埋工作。
前面提到HTTPS证书的神情,我们肯定不在构建的时候做,得在构建完成后的容器初始化的时候做,这个时候就要预埋一些东西。
HTTPS申请证书的ENV变量- 各种关于
游戏服务器配置的变量 目录创建以及权限设置(涉及到root账户以及普通账户的转换)- 涉及到入口文件(初始化脚本)、服务器执行文件(启动脚本)、
Supervisord配置这三个额外文件(或许还有更多)需要处理。 - 设置端口暴露
另外注意区分
chmod与chown,前者是权限操作,后者也是权限操作,但后者是change owner,有些容器创建的,或者是root用户下创建的容器,最后如果默认的用户要使用的话,就要赋予权限。这里我们创建一个叫做
steam的用户
其实有点画个靶子自己射箭的意思,这些目录放以前需要自己运行一下或者去看看文档才能了解,不过现在有了LLM,直接问就行。
# --- 构建参数 ---# 是否使用功能国内源(默认为 true)ARG USE_CN_MIRROR=true# 代理,构建时通过 --build-arg 传入ARG http_proxyARG https_proxy
# 设置环境变量,防止 apt 安装时弹出交互界面ENV DEBIAN_FRONTEND=noninteractiveENV http_proxy=$http_proxyENV https_proxy=$https_proxy
# --- 僵毁相关环境变量 ---# 存档目录ENV PZ_DATA_DIR="/home/steam/Zomboid"# 安装目录ENV PZ_INSTALL_DIR="/opt/pzserver"# 端口定义ENV PORT_GAME_UDP=16261ENV PORT_GAME_UDP_HANDSHAKE=16262
# --- FileBrowser 相关环境变量 ---# Web 端口ENV PORT_FILEBROWSER=35088
# --- 游戏配置页面相关环境变量 ---ENV PORT_GAME_SETTING_WEB=10888
# --- HTTPS 预留配置 ---# 模式: off (默认), custom (自带文件), cloudflare (自动申请)ENV SSL_MODE="off"# 你的域名ENV DOMAIN_NAME="localhost"# 你的邮箱 (SSL_MODE=cloudflare 时启用)ENV EMAIL=""# Cloudflare API信息(SSL_MODE=cloudflare 时启用)ENV CF_Token=""ENV CF_Account_ID=""# 自定义证书路径 (SSL_MODE=custom 时启用)ENV SSL_PATH="/certs"ENV SSL_KEY="key.pem"ENV SSL_CERT="cert.pem"
# 设置僵毁默认分支为 publicENV PZ_BRANCH="public"# 设置游戏设置Web服务相关内容ENV PZ_SETTING_WEB_REPO="Asteroid77/pz-web-backend"# 下载游戏配置页面后台二进制文件ENV PZ_WEB_BACKEND_FILENAME="pz-web-backend-linux-amd64"
# --- 用户与目录 ---RUN <<EOF# 脚本出错时立即退出set -e
# --- 创建 steam 用户 ---echo "Creating user 'steam'..."useradd -m -d /home/steam -s /bin/bash steam
# --- 用户与目录 ---RUN <<EOF# 脚本出错时立即退出set -e
# --- 创建 steam 用户 ---echo "Creating user 'steam'..."useradd -m -d /home/steam -s /bin/bash steam
# --- 创建所有必需的目录 ---# 创建steamcmd目录echo "Creating application directories..."DIRECTORIES_TO_CREATE=" # 创建SteamCMD目录 /home/steam/steamcmd # 创建Steam目录 /home/steam/Steam # 创建僵毁服务器目录 ${PZ_INSTALL_DIR} # 创建 supervisor日志目录 /var/log/supervisor # 创建证书存放目录 /certs # 创建 FileBrowser 数据库专用目录 /opt/filebrowser # 创建游戏配置Web服务目录 /opt/pz-web-backend # 创建 Nginx 配置目录 /etc/nginx/conf.d"CLEAN_DIRECTORIES_TO_CREATE=$(echo "$DIRECTORIES_TO_CREATE" | sed '/^\s*#/d;/^\s*$/d')mkdir -p $CLEAN_DIRECTORIES_TO_CREATE
# --- 赋予 steam 用户对关键目录的所有权 ---echo "Setting permissions for 'steam' user..."CHOWN_LIST=" /home/steam ${PZ_INSTALL_DIR} /certs /opt/filebrowser /opt/pz-web-backend"chown -R steam:steam $CHOWN_LIST
EOF
# 配置文件注入# 切换用户USER root# 复制 Supervisord 配置COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf# 复制游戏启动脚本COPY start-pz.sh /home/steam/start-pz.shRUN chmod +x /home/steam/start-pz.sh && chown steam:steam /home/steam/start-pz.sh# 复制入口脚本COPY entrypoint.sh /entrypoint.shRUN chmod +x /entrypoint.sh
# 暴露端口:游戏UDP + Nginx实际反代端口EXPOSE ${PORT_GAME_UDP}/udp ${PORT_GAME_UDP_HANDSHAKE}/udp ${PORT_GAME_SETTING_WEB}
#设置一个入口文件,上面提到的入口脚本,游戏启动脚本,Supervisord配置文件都由这个入口激活。ENTRYPOINT ["/entrypoint.sh"]3.6 构建文件建立完毕,DockerFile文件总览:
请查阅链接直接查看,就不占用过多篇幅。
4. 编写入口脚本
入口入口,我们要做什么事情呢?
- 关键目录的权限再确认
HTTPS证书的自动申请- 初始化
Web配置面板 - 初始化
Filebrowser - 初始化
nginx并动态构筑其config - 创建日志文件避免后续
Supervisord管理多进程输出日志时报错 - 启动
Supervisord做多进程管理
首先,我们肯定要设置一个在DockerFile里面执行脚本时一直有关注的东西:
# 出错就退出set -e4.1 关键目录权限再确认
因为一个个赋予权限非常细碎,小文件操作很浪费时间,应该先看权限有没有问题,有问题的再去改。
echo "--- 权限检测 ---"smart_chown() { local path="$1" local owner="$2"
# 检查目录是否存在 if [ ! -d "$path" ]; then echo "目录不存在,跳过权限检查: $path" return fi
# 获取当前所有者 local current_owner=$(stat -c '%U' "$path")
# 如果所有者不匹配,则执行修复 if [ "$current_owner" != "$owner" ]; then echo "权限不匹配: $path (当前: $current_owner, 期望: $owner)。正在修复..." # -R 是必须的,因为 Docker 可能创建了多层 root 权限的目录 chown -R "$owner:$owner" "$path" else echo "权限正确: $path (所有者: $owner)" fi}# steam用户的目录权限处理echo "--- 正在给予默认的steam用户目录权限... ---"smart_chown /home/steam/Zomboid "steam"smart_chown /home/steam/Steam "steam"smart_chown /opt/pzserver "steam"smart_chown /opt/filebrowser "steam"smart_chown /opt/pz-web-backend "steam"4.2 初始化Web配置面板
就像我们在构建期间说的那样,如果用户自己有传入Web配置面板文件,那我们就不覆盖而是直接使用,这也是前面预埋的逻辑。
# --- 初始化 Web 配置面板 ---WEB_DIR="/opt/pz-web-backend"WEB_BIN="$WEB_DIR/pz-web-backend"WEB_DEFAULT="/usr/local/share/pz-web-backend-default"
echo "--- 初始化 Web 配置面板 ---"# 确保目录存在if [ ! -d "$WEB_DIR" ]; then mkdir -p "$WEB_DIR"fi
# 如果挂载目录里没有二进制文件(第一次运行),则从镜像备份里复制一个if [ ! -f "$WEB_BIN" ]; then echo "检测到面板程序缺失,复制初始版本..." cp "$WEB_DEFAULT" "$WEB_BIN"else echo "检测到现有面板程序,跳过复制 (保留持久化版本)。"fi
# 确保 steam 用户有权限执行和写入 (为了在线更新能覆盖)chown -R steam:steam "$WEB_DIR"chmod +x "$WEB_BIN"4.3 初始化Filebrowser
echo "--- 初始化 文件浏览器(FileBrowser)变量 ---"FB_DIR="/opt/filebrowser"FB_DB="/opt/filebrowser/database.db"# 用户在网页上看到的“根目录”实际上是这里FB_ROOT="/home/steam/Zomboid"# 确保目标目录存在,否则 FileBrowser 会报错mkdir -p "$FB_ROOT" "$FB_DIR"chown steam:steam "$FB_ROOT" "$FB_DIR"
# 如果数据库不存在,或者大小为0 (上次初始化失败),则重新初始化if [ ! -s "$FB_DB" ]; then echo "--- 初始化 FileBrowser 数据库 ---"
# 安全起见,先删掉旧的 rm -f "$FB_DB"
# 初始化空库 filebrowser config init -d "$FB_DB"
# 设置全局配置 # 注意:root 路径必须存在且有权限 filebrowser config set -d "$FB_DB" --address 0.0.0.0 --port 35088 --root "$FB_ROOT" --baseurl "/filebrowser/"
# 创建管理员 (密码长度必须 > 12) # 默认账号: admin # 默认密码: admin12345678 echo "设置管理员密码为: $FILEBROWSER_ADMIN_PASSWORD" filebrowser users add "$FILEBROWSER_ADMIN_USERNAME" "$FILEBROWSER_ADMIN_PASSWORD" --perm.admin -d "$FB_DB" echo "--- 给予 FileBrowser 数据库权限 ---" chown -R steam:steam "$FB_DIR" chmod 644 "$FB_DB" 2>/dev/null || trueelse echo "--- FileBrowser 数据库已存在,跳过初始化 ---"fi4.4 配置Nginx
setup_nginx_auth() { # 环境变量中定义的用户名和密码 local user="$PZ_WEB_ACCOUNT" local pass="$PZ_WEB_PASSWORD" local auth_file="/etc/nginx/.htpasswd"
if [ -n "$pass" ]; then echo "--- [Security] 正在为 Web 面板配置 Basic Auth ---" echo " User: $user" echo " Pass: (已隐藏)"
# 使用 htpasswd 生成密码文件 (-b 表示命令行输入密码, -c 表示创建新文件) htpasswd -bc "$auth_file" "$user" "$pass" return 0 else echo "⚠️ 警告: 未设置 ADMIN_PASSWORD,Web 面板将没有任何密码保护!" # 如果存在旧文件,删掉,防止意外锁定 rm -f "$auth_file" return 1 fi}# Nginx 配置生成函数 (优化版:增加 HTTP->HTTPS 跳转)generate_nginx_config() { local ssl_on=$1 local cert=$2 local key=$3 NGINX_CONF="/etc/nginx/conf.d/default.conf"
echo "# 自动生成 Nginx 配置" > "$NGINX_CONF"
if [ "$ssl_on" = "on" ]; then # --- HTTPS Server --- echo "server {" >> "$NGINX_CONF" echo " listen 443 ssl;" >> "$NGINX_CONF" echo " server_name $DOMAIN_NAME;" >> "$NGINX_CONF" echo " ssl_certificate $cert;" >> "$NGINX_CONF" echo " ssl_certificate_key $key;" >> "$NGINX_CONF" echo " ssl_protocols TLSv1.2 TLSv1.3;" >> "$NGINX_CONF"
# 插入反向代理逻辑 append_proxy_locations "$NGINX_CONF"
echo "}" >> "$NGINX_CONF"
# --- HTTP 跳转 HTTPS --- echo "server {" >> "$NGINX_CONF" echo " listen 80;" >> "$NGINX_CONF" echo " server_name $DOMAIN_NAME;" >> "$NGINX_CONF" echo " return 301 https://\$host\$request_uri;" >> "$NGINX_CONF" echo "}" >> "$NGINX_CONF" else # --- 纯 HTTP 模式 --- echo "server {" >> "$NGINX_CONF" echo " listen 80;" >> "$NGINX_CONF" append_proxy_locations "$NGINX_CONF" echo "}" >> "$NGINX_CONF" fi}
# 抽取公共的 location 配置append_proxy_locations() { local conf_file=$1 local auth_file="/etc/nginx/.htpasswd"
# 检查密码文件是否存在 local auth_config="" if [ -f "$auth_file" ]; then auth_config="auth_basic \"Restricted Area\"; auth_basic_user_file $auth_file;" fi
# --- Go Web Backend (需要密码保护) --- echo " location / {" >> "$conf_file"
# 注入认证配置 if [ -n "$auth_config" ]; then echo " $auth_config" >> "$conf_file" fi
echo " proxy_pass http://127.0.0.1:10888;" >> "$conf_file" echo " proxy_set_header Host \$host;" >> "$conf_file" echo " proxy_set_header X-Real-IP \$remote_addr;" >> "$conf_file" echo " proxy_http_version 1.1;" >> "$conf_file" echo " proxy_set_header Upgrade \$http_upgrade;" >> "$conf_file" echo " proxy_set_header Connection \"upgrade\";" >> "$conf_file" echo " }" >> "$conf_file"
# --- FileBrowser (自带登录,不需要 Nginx 再拦截一次) --- echo " location /filebrowser/ {" >> "$conf_file" echo " proxy_pass http://127.0.0.1:35088/filebrowser/;" >> "$conf_file" echo " proxy_set_header Host \$host;" >> "$conf_file" echo " proxy_set_header X-Real-IP \$remote_addr;" >> "$conf_file" echo " }" >> "$conf_file"}
echo "--- 清理 Nginx 默认配置 ---"rm -f /etc/nginx/conf.d/default.conf /etc/nginx/sites-enabled/default# 生成Nginx密码文件setup_nginx_auth4.5 初始化HTTPS环境,自动化申请证书,应用Nginx
# 证书目录权限处理if [ -d "/certs" ]; then # 尝试修改权限,但如果失败(例如只读挂载),不要退出脚本 chmod -R 755 /certs 2>/dev/null || echo "提示: /certs 目录是只读的,跳过权限修改。"fiecho "模式: SSL_MODE=$SSL_MODE"echo "域名: DOMAIN_NAME=$DOMAIN_NAME"# 证书目录权限处理if [ -d "/certs" ]; then # 尝试修改权限,但如果失败(例如只读挂载),不要退出脚本 chmod -R 755 /certs 2>/dev/null || echo "提示: /certs 目录是只读的,跳过权限修改。"fi# 打印HTTPS预备信息if [ "$SSL_MODE" = "cloudflare" ]; then if [ -z "$CF_Token" ]; then echo "警告: SSL模式为 Cloudflare 但未检测到 CF_Token" else echo "Cloudflare Token 已加载 (掩码处理: ${CF_Token:0:5}******)" fifi
setup_ssl() { echo "--- [HTTPS] 初始化 SSL 配置 (当前模式: $SSL_MODE) ---"
# 约定好最终使用的证书文件名 FINAL_CERT="$SSL_PATH/$SSL_CERT" FINAL_KEY="$SSL_PATH/$SSL_KEY"
# 是否准备好 HTTPS SSL_READY="false"
# ============================================ # 检查是否已有证书 # ============================================ if [ -s "$FINAL_CERT" ] && [ -s "$FINAL_KEY" ]; then echo "✅ 检测到 /certs 目录下已存在证书文件,跳过申请步骤。" echo " -> 直接使用现有证书。" SSL_READY="true" else echo "ℹ️ /certs 目录下未找到完整证书,进入申请/生成流程..."
# ============================================ # 根据模式处理 # ============================================
# --- 模式 A: Cloudflare 自动申请 --- if [ "$SSL_MODE" = "cloudflare" ]; then echo "--- 正在使用 Cloudflare API 申请证书 ---"
# 校验参数 if [ -z "$CF_Token" ] || [ -z "$DOMAIN_NAME" ] || [ -z "$CF_Account_ID" ]; then echo "❌ 错误: 缺少 CF_Token/DOMAIN_NAME/CF_Account_ID,无法申请。回退到 HTTP 模式。" else # 导入环境变量 export CF_Token="$CF_Token" export CF_Account_ID="$CF_Account_ID" # 申请证书 (如果失败不要退出脚本,而是回退 HTTP) if /root/.acme.sh/acme.sh --issue --server letsencrypt --dns dns_cf -d "$DOMAIN_NAME"; then # 安装证书到 /certs echo "--- 申请成功,正在安装证书到 /certs ---" /root/.acme.sh/acme.sh --install-cert -d "$DOMAIN_NAME" \ --key-file "$FINAL_KEY" \ --fullchain-file "$FINAL_CERT" \ --reloadcmd "nginx -s reload"
if [ -s "$FINAL_CERT" ]; then echo "✅ 证书已保存到挂载目录。" SSL_READY="true" fi else echo "❌ 证书申请失败,请检查 Cloudflare Token 或网络。" fi fi
# --- 模式 B: Custom 自定义 --- elif [ "$SSL_MODE" = "custom" ]; then # 用户选择了 custom 但没把文件放对位置 echo "❌ 模式为 custom 但 $SSL_PATH 下没找到 $SSL_CERT 以及 $SSL_KEY。" echo " 请将证书文件重命名并放入当前挂载Docker-Compose下的 ./certs 目录。" fi fi
# ============================================ # 生成 Nginx 配置 # ============================================ if [ "$SSL_READY" = "true" ]; then echo "🚀 启用 HTTPS (443) + HTTP 跳转" generate_nginx_config "on" "$FINAL_CERT" "$FINAL_KEY" else echo "⚠️ 未满足 HTTPS 条件,仅启用 HTTP (80)" generate_nginx_config "off" "" "" fi}# 执行Https证书创建以及Nginx初始化setup_ssl4.6 创建日志文件
# 创建日志文件,防止启动时报错touch /home/steam/pz-stdout.logchown steam:steam /home/steam/pz-stdout.log4.7 启动Supervisord
# 启动 Supervisorecho "--- 启动进程管理器 ---"exec supervisord -c /etc/supervisor/conf.d/supervisord.conf5. Supervisord配置
Project Zomboid Dedicated Server本身是没有restart这个功能的,但是其在Linux下有个停止信号SIGTERM可以被服务器的java进程捕抓到。
这样就可以实现Windows下用Rcon传递quit()才能实现的退出保存而非直接终止(会回档)。
基于上述逻辑Supervisord配置autorestart=true才有了用武之地。
[supervisord]nodaemon=trueuser=rootlogfile=/var/log/supervisord.logpidfile=/var/run/supervisord.pid
# 定义 RPC 接口,供 supervisorctl 连接[unix_http_server]file=/var/run/supervisor.sockchmod=0777
# 让 supervisorctl 能通过 RPC 控制进程[rpcinterface:supervisor]supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
# 定义 supervisorctl 客户端配置[supervisorctl]serverurl=unix:///var/run/supervisor.sock
[program:logstream]# 实时读取日志文件,并输出到标准输出command=tail -f /home/steam/pz-stdout.loguser=steamautostart=trueautorestart=true
# stdout 指向 /dev/stdout,才能被 docker logs 捕获stdout_logfile=/dev/stdoutstdout_logfile_maxbytes=0stderr_logfile=/dev/stderrstderr_logfile_maxbytes=0
# --- FileBrowser ---[program:filebrowser]command=/usr/local/bin/filebrowser -d /opt/filebrowser/database.dbuser=steam# 覆盖掉默认的 /rootenvironment=HOME="/home/steam",USER="steam"autostart=trueautorestart=truestdout_logfile=/dev/stdoutstdout_logfile_maxbytes=0stderr_logfile=/dev/stderrstderr_logfile_maxbytes=0
# --- PZ Web 后台管理 ---[program:webconfig]command=/opt/pz-web-backend/pz-web-backenddirectory=/opt/pz-web-backend# 确保目录存在 (Dockerfile 里 mkdir -p /opt/pz-web-backend)user=rootautostart=trueautorestart=truestdout_logfile=/dev/stdoutstdout_logfile_maxbytes=0stderr_logfile=/dev/stderrstderr_logfile_maxbytes=0
# --- Project Zomboid ---[program:pzserver]# 僵毁服务器启动脚本入口command=/home/steam/start-pz.shuser=steam# 覆盖掉默认的 /rootenvironment=HOME="/home/steam",USER="steam"autostart=trueautorestart=true# 将日志输出到具体文件stdout_logfile=/home/steam/pz-stdout.log# 设置日志大小上限 ,防止把磁盘写满stdout_logfile_maxbytes=10MB# 保留最近 5 个日志备份stdout_logfile_backups=5# 错误日志,方便查看stderr_logfile=/home/steam/pz-stdout.log# 一样,设置大小上限 ,防止把磁盘写满stderr_logfile_maxbytes=10MBstopasgroup=truekillasgroup=true
[program:nginx]# 让 Nginx 在前台运行,这样 Supervisor 才能管理它command=/usr/sbin/nginx -g "daemon off;"user=rootautostart=trueautorestart=truestdout_logfile=/dev/stdoutstdout_logfile_maxbytes=0stderr_logfile=/dev/stderrstderr_logfile_maxbytes=06. 僵毁服务器启动脚本
注意最后的exec执行“保存并退出”逻辑,别的都很常规,就普通的启动的时候查看更新,校验完整性,给SteamCMD挂上指定节点这样的操作。
#!/bin/bash# 位于 /home/steam/start-pz.sh
PZ_INSTALL_DIR="/opt/pzserver"STEAMCMD_DIR="/home/steam/steamcmd"
echo "--- Supervisor 正在启动僵毁服务端 ---"# 确认分支# 如果 PZ_BRANCH 为空,默认为 publicBRANCH=${PZ_BRANCH:-public}BETA_ARGS=""
if [ "$BRANCH" = "public" ] || [ "$BRANCH" = "latest" ]; then echo "--- [Game] 目标分支: 稳定版 (public) ---"else echo "--- [Game] 目标分支: 测试版 ($BRANCH) ---" BETA_ARGS="-beta $BRANCH"fi
# 处理国内源STEAM_CLIENT_ARGS=""
# 检查环境变量 STEAMCMD_CN_MIRROR_ID 是否存在且不为空if [ -n "$STEAMCMD_CN_MIRROR_ID" ]; then echo "--- [Game] ⚡ 检测到国内源配置,强制指定下载节点 ID: $STEAMCMD_CN_MIRROR_ID ---" # 将 -cellid 参数拼接到启动参数中 STEAM_CLIENT_ARGS="$STEAM_CLIENT_ARGS -cellid $STEAMCMD_CN_MIRROR_ID"fiecho "--- [Game] 开始检查更新... ---"echo "--- [Game] SteamCMD 参数: $STEAM_CLIENT_ARGS"
# timeout 600s: 给 SteamCMD 10分钟时间。如果超时或失败,尝试继续启动旧版本。timeout 600s $STEAMCMD_DIR/steamcmd.sh $STEAM_CLIENT_ARGS \ +force_install_dir $PZ_INSTALL_DIR \ +login anonymous \ +app_update 380870 $BETA_ARGS validate \ +quit || echo "--- [Game] ⚠️ 更新过程遇到错误或超时,尝试直接启动服务器... ---"
# --- 启动服务器 ---cd $PZ_INSTALL_DIRif [ ! -f "./start-server.sh" ]; then echo "错误: 找不到 start-server.sh,可能是游戏下载完全失败。" exit 1fi
# 使用 exec 替换当前 shell 进程# 这样 supervisord 的停止信号 (SIGTERM) 能直接传给 java 进程# 保证游戏能有机会执行“保存并退出”逻辑exec ./start-server.sh -adminpassword admin -cachedir=/home/steam/Zomboid7. Web配置面板
技术选型为:Go+Alpine.js(Daisyui.css+Tailwindcss)
时间很晚了,有机会再补充吧,可能这个周末会写。
主要讲逻辑以及自动构建。
尤其是多语言的处理,僵毁本身有一个Translate目录,将里面的目录读取下来给这些已经支持的i18n做支持。
Web页面倒是没什么好说的,撒把米让LLM去玩就行,我自己作为一个前端入门的都懒得写这种东西。
8. Docker-compose文件
version: '3.8'
services: pz-server: container_name: ${CONTAINER_NAME}
# --- DNS 配置 --- # 强制容器使用国内DNS,防止默认 DNS 污染导致 Steam 无法解析 dns: - ${DNS_SERVER_1} - ${DNS_SERVER_2}
# --- 构建配置 --- build: context: . dockerfile: Dockerfile # 构建时使用宿主机网络 network: host args: # 传递 .env 里的代理和源设置 http_proxy: ${PROXY_URL} https_proxy: ${PROXY_URL} USE_CN_MIRROR: ${USE_CN_MIRROR}
# --- 端口映射 --- ports: # 游戏端口 - "${PORT_GAME_UDP}:16261/udp" - "${PORT_GAME_HANDSHAKE}:16262/udp" # Nginx转发端口 # Web面板访问 10888,没有开启https时使用 - "${PORT_GAME_SETTING_EXT}:80" # 开启https后,需要转发给容器内的 443,同时注释掉上面的80 # - "${PORT_GAME_SETTING_EXT}:443"
# --- 环境变量 --- environment: - TZ=Asia/Shanghai # 代理配置 # - http_proxy=${PROXY_URL} # - https_proxy=${PROXY_URL} # HTTPS / SSL 配置 - SSL_MODE=${SSL_MODE} - DOMAIN_NAME=${DOMAIN_NAME} - CF_Token=${CF_TOKEN} - CF_Account_ID=${CF_ACCOUNT_ID} - SSL_CERT=${SSL_CERT} - SSL_KEY=${SSL_KEY} # 游戏配置 - PZ_BRANCH=${PZ_BRANCH} # PZ Web 面板账号配置 - PZ_WEB_ACCOUNT=${PZ_WEB_ACCOUNT} - PZ_WEB_PASSWORD=${PZ_WEB_PASSWORD} # 游戏更新相关 - STEAMCMD_CN_MIRROR_ID=${STEAMCMD_CN_MIRROR_ID} - PZ_BRANCH=${PZ_BRANCH} # FileBrowser 账号配置 - FILEBROWSER_ADMIN_USERNAME=${FILEBROWSER_ADMIN_USERNAME} - FILEBROWSER_ADMIN_PASSWORD=${FILEBROWSER_ADMIN_PASSWORD}
# --- 卷挂载 --- volumes: # 存档与配置 # 宿主机 ./data/zomboid -> 容器 /home/steam/Zomboid - ./data/zomboid:/home/steam/Zomboid
# 游戏本体 # 宿主机 ./data/game -> 容器 /opt/pzserver - ./data/game:/opt/pzserver
# FileBrowser 数据库 # 宿主机 ./data/filebrowser -> 容器 /opt/filebrowser - ./data/filebrowser:/opt/filebrowser
# Web 后端的数据目录 - ./data/web-backend:/opt/pz-web-backend
# Steam持久化,主要是登录缓存的持久化,这样只需要忍受第一次登录流程,后续直接拿着config/config.vdf握手 - ./data/steam:/home/steam/Steam
# 映射宿主机的 ./certs 目录到容器内的 /certs # 注意,该目录下应包含 cert.pem 和 key.pem 这样的证书文件。 # 如果你使用域名申请了证书的话,申请后的证书最终会放在这里供Nginx使用,这里有证书会跳过HTTPS申请步骤 # 如果你直接提供证书放在下面,并且正确设置好SSL_KEY和SSL_CERT变量,则会直接使用这些证书作为HTTPS证书 - ./certs:/certs
# --- 重启策略 --- restart: unless-stopped ddns-go: # --- DDNS-GO 服务 --- # 作用:自动更新域名解析,将域名指向家庭网络的动态公网IP。 container_name: ddns-go # command: "-l :9876 -f 300" # 每 300 秒(5分钟)检查一次 command: -f ${FREQUENCY_DDNS_GO} image: ghcr.io/jeessy2/ddns-go:latest
# --- 重启策略 --- restart: unless-stopped
# --- 端口映射 --- # 暴露 ddns-go 的 Web UI 配置界面 ports: - "${PORT_DDNS_GO}:9876"
# --- 卷挂载 --- # 将容器内的配置目录持久化到宿主机,防止容器重启后配置丢失 volumes: - ./data/ddns-go:/root
# --- 环境变量 --- environment: - TZ=Asia/Shanghai9. 总结
嗯,没什么好总结的,就是糊糊工程,只不过比起Windows上面的那坨糊糊,显得没那么糊糊。