7302 字
37 分钟
Project Zomboid Dedicated Server Docker 一键端搭建指南

1. 环境一览#

依赖版本描述
Ubuntujammy运行环境
ddns-go6.14.1动态ip与域名绑定方案
pz-web-backend1.0.4(LATEST)Web配置面板,管理僵毁服务器的配置文件
docker-composev2.29.7-desktop.1docker-compose
docker-engine27.3.1, build ce12230docker-engine
FileBrowser2.54.0文件管理工具,当Web配置面板不满足要求时,自行调整配置文件
SteamCMDLATESTSteamCMD
Supervisord4.2.1多任务进程管理工具
Project Zomboid Dedicated Server42.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.list
fi
EOF

因为这个系统镜像是极简系统,我们需要安装一下可能需要的依赖:

# 启用 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-8
ENV LANG=en_US.UTF-8
ENV LANGUAGE=en_US:en
ENV LC_ALL=en_US.UTF-8

3.2 FileBrowser安装#

既然涉及到了github,要考虑到大陆访问的问题,这里要设置一个GITHUB_PROXY_URLgithub相关资源加速。

只要在.env文件中设置了变量,就可以直接在DockerFile中通过$变量的形式访问,无需显式声明ENV

显式声明ENV是为了让其它文件(比如说后面可能会涉及到的僵毁服务器启动之类的代码)用到。

ARG则是在当前DockerFile中可以使用,明确告诉看文件的人这不是魔法变量。

# 获取FileBrowser
RUN <<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 ---
# 定义基础 URL
FB_URL="https://github.com/filebrowser/filebrowser/releases/latest/download/${FB_ARCH}-filebrowser.tar.gz"
# 根据构建参数决定是否添加代理前缀
# -n的意思是"non-zero length" 或者 "not empty",当然,你也可以写成"$GITHUB_PROXY_URL"!=""
# echo其实就是控制台输出这么一串字符,相当于打印log
if [ -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/filebrowser
echo "FileBrowser installed successfully."
EOF

Tips:关于权限与chmod,在Linux系统里头使用ls -l你会看到详细的信息,譬如rwxr-xr-x这样的部分,先给一份列表:

用户类别符号描述
User (所有者)u创建该文件或目录的用户。
Group (所属组)g文件所属的用户组。一个组可以包含多个用户。
Others (其他人)o系统中既不是所有者也不是所属组成员的所有其他用户。
All(所有人)a系统中的所有用户
权限类型符号数字值文件 的意义目录 的意义
Read (读)r4可以读取文件的内容 (cat, less)。可以列出目录中的文件和子目录列表 (ls)。
Write (写)w2可以修改文件的内容 (vim, echo >)。可以在目录中创建、删除、重命名文件和子目录
Execute (执行)x1可以作为程序来运行 (./myscript.sh)。可以进入 (cd) 该目录。
操作符描述
+添加权限
-移除权限
=设置为指定的权限(覆盖原有权限)

chmod有两种使用模式:

  1. 符号模式
    • chmod u+x dir or file:给所有者添加执行权限
    • chmod g-w dir or file:移除用户组写权限
    • ……
  2. 数字模式(一般都是直接用数字)
    • 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 steam
RUN <<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."
EOF

3.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-default
echo "--> pz-web-backend downloaded and installed successfully."
EOF

3.5 备菜准备完毕,做一些预埋工作。#

前面提到HTTPS证书的神情,我们肯定不在构建的时候做,得在构建完成后的容器初始化的时候做,这个时候就要预埋一些东西。

  • HTTPS申请证书的ENV变量
  • 各种关于游戏服务器配置的变量
  • 目录创建以及权限设置(涉及到root账户以及普通账户的转换)
  • 涉及到入口文件(初始化脚本)、服务器执行文件(启动脚本)、Supervisord配置这三个额外文件(或许还有更多)需要处理。
  • 设置端口暴露

另外注意区分chmodchown,前者是权限操作,后者也是权限操作,但后者是change owner,有些容器创建的,或者是root用户下创建的容器,最后如果默认的用户要使用的话,就要赋予权限。

这里我们创建一个叫做steam用户

其实有点画个靶子自己射箭的意思,这些目录放以前需要自己运行一下或者去看看文档才能了解,不过现在有了LLM,直接问就行。

# --- 构建参数 ---
# 是否使用功能国内源(默认为 true)
ARG USE_CN_MIRROR=true
# 代理,构建时通过 --build-arg 传入
ARG http_proxy
ARG https_proxy
# 设置环境变量,防止 apt 安装时弹出交互界面
ENV DEBIAN_FRONTEND=noninteractive
ENV http_proxy=$http_proxy
ENV https_proxy=$https_proxy
# --- 僵毁相关环境变量 ---
# 存档目录
ENV PZ_DATA_DIR="/home/steam/Zomboid"
# 安装目录
ENV PZ_INSTALL_DIR="/opt/pzserver"
# 端口定义
ENV PORT_GAME_UDP=16261
ENV 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"
# 设置僵毁默认分支为 public
ENV 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.sh
RUN chmod +x /home/steam/start-pz.sh && chown steam:steam /home/steam/start-pz.sh
# 复制入口脚本
COPY entrypoint.sh /entrypoint.sh
RUN 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 -e

4.1 关键目录权限再确认#

因为一个个赋予权限非常细碎,小文件操作很浪费时间,应该先看权限有没有问题,有问题的再去改。

Terminal window
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配置面板文件,那我们就不覆盖而是直接使用,这也是前面预埋的逻辑。

Terminal window
# --- 初始化 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#

Terminal window
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 || true
else
echo "--- FileBrowser 数据库已存在,跳过初始化 ---"
fi

4.4 配置Nginx#

Terminal window
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_auth

4.5 初始化HTTPS环境,自动化申请证书,应用Nginx#

Terminal window
# 证书目录权限处理
if [ -d "/certs" ]; then
# 尝试修改权限,但如果失败(例如只读挂载),不要退出脚本
chmod -R 755 /certs 2>/dev/null || echo "提示: /certs 目录是只读的,跳过权限修改。"
fi
echo "模式: 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}******)"
fi
fi
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_ssl

4.6 创建日志文件#

Terminal window
# 创建日志文件,防止启动时报错
touch /home/steam/pz-stdout.log
chown steam:steam /home/steam/pz-stdout.log

4.7 启动Supervisord#

Terminal window
# 启动 Supervisor
echo "--- 启动进程管理器 ---"
exec supervisord -c /etc/supervisor/conf.d/supervisord.conf

5. Supervisord配置#

Project Zomboid Dedicated Server本身是没有restart这个功能的,但是其在Linux下有个停止信号SIGTERM可以被服务器的java进程捕抓到。

这样就可以实现Windows下用Rcon传递quit()才能实现的退出保存而非直接终止(会回档)。

基于上述逻辑Supervisord配置autorestart=true才有了用武之地。

Terminal window
[supervisord]
nodaemon=true
user=root
logfile=/var/log/supervisord.log
pidfile=/var/run/supervisord.pid
# 定义 RPC 接口,供 supervisorctl 连接
[unix_http_server]
file=/var/run/supervisor.sock
chmod=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.log
user=steam
autostart=true
autorestart=true
# stdout 指向 /dev/stdout,才能被 docker logs 捕获
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
# --- FileBrowser ---
[program:filebrowser]
command=/usr/local/bin/filebrowser -d /opt/filebrowser/database.db
user=steam
# 覆盖掉默认的 /root
environment=HOME="/home/steam",USER="steam"
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
# --- PZ Web 后台管理 ---
[program:webconfig]
command=/opt/pz-web-backend/pz-web-backend
directory=/opt/pz-web-backend
# 确保目录存在 (Dockerfile 里 mkdir -p /opt/pz-web-backend)
user=root
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
# --- Project Zomboid ---
[program:pzserver]
# 僵毁服务器启动脚本入口
command=/home/steam/start-pz.sh
user=steam
# 覆盖掉默认的 /root
environment=HOME="/home/steam",USER="steam"
autostart=true
autorestart=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=10MB
stopasgroup=true
killasgroup=true
[program:nginx]
# 让 Nginx 在前台运行,这样 Supervisor 才能管理它
command=/usr/sbin/nginx -g "daemon off;"
user=root
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

6. 僵毁服务器启动脚本#

注意最后的exec执行“保存并退出”逻辑,别的都很常规,就普通的启动的时候查看更新,校验完整性,给SteamCMD挂上指定节点这样的操作。

#!/bin/bash
# 位于 /home/steam/start-pz.sh
PZ_INSTALL_DIR="/opt/pzserver"
STEAMCMD_DIR="/home/steam/steamcmd"
echo "--- Supervisor 正在启动僵毁服务端 ---"
# 确认分支
# 如果 PZ_BRANCH 为空,默认为 public
BRANCH=${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"
fi
echo "--- [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_DIR
if [ ! -f "./start-server.sh" ]; then
echo "错误: 找不到 start-server.sh,可能是游戏下载完全失败。"
exit 1
fi
# 使用 exec 替换当前 shell 进程
# 这样 supervisord 的停止信号 (SIGTERM) 能直接传给 java 进程
# 保证游戏能有机会执行“保存并退出”逻辑
exec ./start-server.sh -adminpassword admin -cachedir=/home/steam/Zomboid

7. 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/Shanghai

9. 总结#

项目文档

嗯,没什么好总结的,就是糊糊工程,只不过比起Windows上面的那坨糊糊,显得没那么糊糊。

Project Zomboid Dedicated Server Docker 一键端搭建指南
https://blog.astro777.cfd/posts/guide/pz-dedicated-server-docker-all-in-one/
作者
ASTRO
发布于
2026-01-12
许可协议
CC BY-NC-SA 4.0