Files
squaddocker/README.md
gongzhiyongandClaude Opus 5 22c9581ef4 docs: 补充 RCON 未启动的实测失败模式
实机部署验证时发现 21114/tcp 不监听,根因不是端口冲突,而是
Rcon.cfg 的 Password= 为空导致 RCON 服务不启动。补充:
- 服务端的确切告警日志与设置成功后的日志
- Rcon.cfg 为 CRLF 行尾,sed 修改需保留 \r
- 建议在服务器本机生成随机密码,不经过剪贴板或聊天记录
- 排障表新增该条目

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 21:08:36 +08:00

16 KiB
Raw Permalink Blame History

Squad(战术小队)专用服务器 — Docker 部署

基于 cm2network/squad 镜像的 Squad 专用服务器部署方案,支持单实例与多实例。

标注 [实测] 的内容来自 2026-07-27 对目标环境和镜像的实际验证,非估算或推测。 标注 [未验证] 的内容尚未在本环境证实,请勿直接用于生产。


1. 这套方案解决什么

Squad 服务端官方只提供 SteamCMD 裸装方式(配 screen 托管)。本方案用 Docker 替代:

  • 进程守护与自动重启交给 Docker,不依赖 screen 会话
  • 游戏文件、配置、日志都在挂载卷里,容器可随意重建
  • 多实例通过环境变量声明端口,不用改启动脚本

不解决的问题:游戏本体下载量和硬件需求 —— 见下节。


2. 硬件要求 — 先读这一节

2.1 镜像不含游戏文件

[实测] cm2network/squad 镜像只有 134.3 MB(压缩后,6 层),内容是 Debian 基础层 + SteamCMD + entry.sh。

游戏服务端本体(约 80–100 GB)是容器每次启动时由 SteamCMD 拉取的,落在挂载卷里。

两个直接后果:

  • 把镜像搬到私有注册表不能省掉这次下载
  • 每个独立安装的实例都要下一份,所以多实例基本上必须共享游戏文件(§7.2)

2.2 最低配置

资源 单实例 双实例 说明
物理核 1 2 看物理核,不是 vCPU。Squad 主 tick 重度单线程,一核跑两实例会双双掉 tick
内存 8 GB 16 GB 无 swap 时更吃紧,务必设 mem_limit
磁盘(持久) 120 GB 160 GB(共享游戏文件)
200+ GB(独立安装)
另需留出 SteamCMD 更新的临时空间

⚠️ Azure 用户注意:/mnt 挂的资源盘是临时盘,VM deallocate 后清空,不能放游戏文件。 df -hT 看不出来,要查 /etc/fstab 是否有 azure_resource 和 comment=cloudconfig 字样。

2.3 首次下载耗时会远超预期

[实测] 目标环境到 Steam CDN 的速度为 0.8–1.6 MB/s(约 7–13 Mbit/s)。

按 80 GB 计算,单次完整下载需要 14–28 小时。

部署前确认:

  • 下载完成后磁盘仍有余量(建议留 20 GB 以上)
  • 下载期间尽量不重启容器 —— SteamCMD 支持续传,但会重新校验,很耗时

3. 前置条件

3.1 目标机器

  • Linux x86_64,GLIBC ≥ 2.17
  • Docker Engine + docker compose 插件
  • 当前用户在 docker 组,或有 sudo

Ubuntu 上不要用 apt install docker.io。[实测] 目标环境 apt-cache policy docker.io 显示 Candidate: (none)。请走 Docker 官方源(download.docker.com)。

3.2 Docker Hub 不可达时(中国大陆常见)

[实测] 目标环境 registry-1.docker.io / index.docker.io / auth.docker.io 全部超时 (DNS 可解析,属网络层拦截)。

两种应对:

A. 镜像搬运到私有注册表(本方案采用) — 见 §5

B. 配置 registry mirror — /etc/docker/daemon.json:

{ "registry-mirrors": ["https://docker.m.daocloud.io"] }

[实测] 该 mirror 返回 401 正常鉴权挑战,可用。但依赖第三方站点可用性。

3.3 端口放行

主机防火墙之外,云平台安全组也要放行。Azure NSG、阿里云安全组这类配置在机器外部, 机内 iptables / ufw 查不到,是最容易漏的一环 —— 表现为服务端一切正常但玩家搜不到、连不上。

规则见 §4.2。


4. 端口规划

4.1 一个容易漏的端口

[实测] 镜像声明了 4 个端口:

7787/udp    27165/udp    21114/tcp    15000/udp

最后一个 15000/udp(BEACONPORT)常被漏掉 —— 官方 wiki 服务器安装页完全没有提及。 在 network_mode: host 下两个实例都会去绑 15000,导致第二个实例起不来或行为异常, 而日志里不一定报得清楚。

4.2 多实例端口表

游戏端口与查询端口各占 N 和 N+1 两个,所以实例间至少间隔 10,不能只差 1。

用途 协议 实例 1 实例 2
游戏 UDP 7787, 7788 7797, 7798
Steam 查询 UDP 27165, 27166 27175, 27176
Beacon UDP 15000 15010
RCON TCP 21114 21124

安全组入站规则(实例 1):

UDP  7787-7788     允许
UDP  27165-27166   允许
UDP  15000         允许
TCP  21114         允许 —— 建议限制来源 IP

RCON 是远程管理通道,不应对全网开放,建议只放行管理者 IP。

4.3 network_mode: host 是必需的

Squad 依赖 Steam 查询协议,用 -p 端口映射经常导致服务器在游戏内浏览器中搜不到。

代价:Docker 不再隔离端口,多实例端口必须手工错开。

若每个实例有独立 IP,也可以用 MULTIHOME 绑定(§8.2),比纯靠端口错开更清晰。


5. 镜像搬运到私有注册表

在能访问 Docker Hub 的机器上执行(通常是本地开发机)。

5.1 拉取、打标、推送

# 该镜像只有 linux/amd64。在 Apple Silicon 上必须显式指定 --platform,
# 否则会报找不到匹配平台。搬运不需要运行,所以架构不匹配无妨。
docker pull --platform linux/amd64 cm2network/squad:latest

docker login <注册表地址> -u <用户名> --password-stdin   # 密码走 stdin,不要用 -p
docker tag cm2network/squad:latest <注册表地址>/squad-server:v1
docker push <注册表地址>/squad-server:v1

用 --password-stdin 而不是 -p <密码>:后者会把密码暴露在进程列表(ps)里。

5.2 核对 digest

推送后记录 digest,目标机拉取后比对,确认跑的是同一个镜像:

docker image inspect --format '{{index .RepoDigests 0}}' <注册表地址>/squad-server:v1

[实测] 本次搬运结果与 Docker Hub 上游 digest 完全一致:

sha256:8cba47f53df586c2767525a9f062c2996b36f9a34a1d771a35f67a1713c46893

若注册表无鉴权(任何人可写),这一步是必要的完整性检查,不是可选项。

5.3 HTTP 明文注册表的额外配置

如果私有注册表是 HTTP(非 TLS),Docker 默认拒绝,报 http: server gave HTTP response to HTTPS client。需要:

Linux — /etc/docker/daemon.json:

{ "insecure-registries": ["<地址:端口>"] }

改完 sudo systemctl restart docker。

macOS(Docker Desktop) — 写 ~/.docker/daemon.json 后必须完整退出并重启 Docker Desktop。

踩坑记录:docker desktop restart 可能异步返回,导致后续命令连到的还是旧守护进程, 配置看似没生效。另外用 pkill 强杀 Docker Desktop 会打断它的退出流程, 使随后的 open -a Docker 失效。稳妥做法:osascript -e 'quit app "Docker"' → 轮询等 socket 消失 → open -a Docker → 轮询等 docker info 成功 → 再验证:

docker info --format '{{json .RegistryConfig.IndexConfigs}}'

确认目标地址出现且 "Secure": false。

使用 ACR / Harbor 等有效 TLS 的注册表时,以上都不需要。

5.4 拉取凭据的权限收敛

目标机 docker login 后凭据会以 base64 存进 ~/.docker/config.json(不是加密)。

如果用的是注册表管理员凭据,那台机器一旦被入侵,攻击者就获得了推送权限, 可以覆盖你的镜像 tag。生产环境建议改用只读(pull-only)令牌:

  • Azure ACR:创建 scope map + token,只授 content/read
  • Harbor:创建 robot account,只勾 pull

6. 部署

6.1 准备数据目录

sudo mkdir -p /srv/squad/inst1
# 镜像内以 uid 1000 (steam) 运行,必须给写权限
sudo chown -R 1000:1000 /srv/squad

6.2 配置

cp .env.example .env
$EDITOR .env

6.3 启动

docker compose up -d squad1
docker compose logs -f squad1

首次启动进入 SteamCMD 下载阶段(§2.3,14–28 小时)。 日志出现 Success! App '403240' fully installed. 表示下载完成。

6.4 编辑服务器配置

游戏文件下载完成后,配置文件出现在 /srv/squad/inst1/SquadGame/ServerConfig/:

文件 作用
Server.cfg 服务器名称、人数上限等主配置
Rcon.cfg RCON 端口与密码
Admins.cfg 管理员与权限组
MapRotation.cfg 地图轮换
MOTD.cfg 每日消息
ServerMessages.cfg 循环公告
Bans.cfg 封禁列表
License.cfg 服务器授权

RCON 密码必须在这里改,否则 RCON 根本不会启动。 镜像没有 RCONPASSWORD 环境变量, entry.sh 只重写端口、不管密码。

[实测] 默认 Rcon.cfg 的 Password= 是空的,此时服务端启动会输出:

LogRCONServer: Warning: Start(): RCONServer was not setup because the password
               was not specified on the command line or in the ini file

现象是 21114/tcp 不监听 —— 容易被误判为端口冲突,实际是密码没设。

设好密码后重启,日志应出现:

LogRCONServer: Start(): RCONServer setup on 0.0.0.0:21114

⚠️ Rcon.cfg 是 CRLF 行尾。 用 sed 改的时候要保留 \r,否则可能出现解析问题:

sudo sed -i -E "s/^Password=.*$/Password=<你的密码>\r/" Rcon.cfg

生成随机密码时建议直接在服务器上生成,不要经过聊天记录或剪贴板:

openssl rand -base64 24 | tr -d '/+=' | cut -c1-24

改完重启生效:

docker compose restart squad1

7. 多实例

7.1 RCON 端口:自动处理,但有个后续陷阱

[实测] entry.sh 会在启动时自动重写 Rcon.cfg 里的端口:

sed -i -e 's/Port=21114/'"Port=${RCONPORT}"'/g' "${STEAMAPPDIR}/SquadGame/ServerConfig/Rcon.cfg"

所以新建实例时不需要手动改 Rcon.cfg 端口 —— 很多教程(包括官方 wiki)说必须手动改, 对这个镜像而言是多余的。

但这个 sed 只匹配字面量 Port=21114。 因此:

⚠️ 首次启动之后再改 RCONPORT,Rcon.cfg 不会跟着变 —— 此时 Rcon.cfg 里已经是旧的自定义端口,不再包含 21114,sed 匹配不上,静默失败。 结果是启动参数与配置文件不一致,RCON 连不上。

改端口时必须同时手工改 Rcon.cfg,或者删掉 Rcon.cfg 让 SteamCMD 重新生成默认文件。

7.2 共享游戏文件 [未验证]

游戏本体约 80–100 GB,下载一次 14–28 小时,两个实例各下一份通常不现实。 思路是游戏本体只留一份共享,每实例独立持有 ServerConfig 和日志。

⚠️ 本方案尚未实测,存在明确的已知风险。

[实测] entry.sh 在每次容器启动时都会无条件执行 steamcmd +app_update (源码第 2–17 行,没有"文件已存在则跳过"的判断)。

两个容器共享同一游戏目录时,会并发对同一批文件跑 SteamCMD 更新,有文件损坏风险。

若要采用,稳妥流程是:停掉所有实例 → 单独跑一次更新 → 再一起启动; 并且需要改造 entry.sh 或覆盖启动命令来跳过第二个实例的更新步骤。

在验证完成前,请勿在生产环境使用。

7.3 启用实例 2

docker compose --profile instance2 up -d

启用前确认硬件满足 §2.2 的双实例要求。


8. 镜像中未见于文档的环境变量

以下变量不在镜像的默认 ENV 列表里,是从 entry.sh 源码中确认的 [实测], 但相当有用:

8.1 SERVER_NAME

设置后,entry.sh 会把它写入 Server.cfg 的 ServerName(源码第 22–25 行):

sed -i -e "s/^ServerName=.*/ServerName=\"${SERVER_NAME}\"/" .../Server.cfg

省去手工改配置文件。注意它每次启动都会覆盖 ServerName, 所以如果你在 Server.cfg 里手改了服务器名,又设了这个变量,手改的会被覆盖。

8.2 MULTIHOME

绑定到指定网卡 IP(源码第 47–51 行)。留空 / 0.0.0.0 / 127.0.0.1 时被忽略。

多实例若各有独立 IP,用它比纯靠端口错开更清晰,也能让每个实例都用标准端口。


9. 日常运维

更新服务端

entry.sh 每次启动都跑 SteamCMD 更新,所以游戏发新版本时:

docker compose restart squad1

更新镜像本身:

docker compose pull && docker compose up -d

游戏文件在卷里,不会丢。

查看日志

docker compose logs -f --tail=200 squad1

服务端自身日志在 /srv/squad/inst1/SquadGame/Saved/Logs/。

容器日志轮转已在 docker-compose.yml 里配置(50MB × 3)。也可以在 /etc/docker/daemon.json 里设为全局默认。

停止

docker compose stop squad1

[实测] entry.sh 最后一行是 bash SquadGameServer.sh ... 而非 exec, PID 1 是 bash 脚本。bash 在同步等待子进程时不转发 SIGTERM, 所以优雅停止未必真的优雅。docker-compose.yml 里设了 stop_grace_period: 120s 尽量留出落盘时间,但不保证。 重要操作前建议先在游戏内用 RCON 确认无人在线。

备份

只需备份配置,不含游戏本体:

tar czf squad-config-$(date +%F).tar.gz -C /srv/squad/inst1/SquadGame ServerConfig

10. 已知问题与限制

诚实声明,避免误判。

10.1 RANDOM 环境变量在此镜像中无效 [实测]

镜像默认设了 RANDOM=NONE,entry.sh 第 61 行把 ${RANDOM} 传给服务端。

但 RANDOM 是 bash 内置特殊变量,环境变量覆盖不了它 —— 每次读取都返回随机整数。 实测:

$ env RANDOM=NONE bash -c 'echo ${RANDOM}'
18575
$ env RANDOM=NONE bash -c 'echo ${RANDOM}'
2645

所以传给 SquadGameServer.sh 的实际上是随机数字,不是 NONE。 设置这个变量没有任何效果,不管设成什么。

地图轮换请改用 MapRotation.cfg 控制。

10.2 其他

  • §7.2 共享游戏文件方案未实测,且已知有并发更新风险,不要直接用于生产
  • 免费服人数上限未经确认。镜像默认 FIXEDMAXPLAYERS=80; 更高人数是否需要 Offworld 授权(License.cfg),请向官方确认,本文不做断言
  • 镜像只有 linux/amd64,无 arm64。Apple Silicon 上只能搬运,无法本地运行验证
  • MODS 变量格式是 bash 数组字面量(如 (1959152751 2016356569)), 不是逗号或空格分隔的普通字符串 —— 源码用 declare -a MODS="${MODS}" 解析
  • 宿主机 ulimit -n 若为默认 1024 偏低。已在 compose 中给容器单独调高,未改动宿主机

11. 排障

现象 原因 处理
http: server gave HTTP response to HTTPS client 未配 insecure-registries §5.3
服务器在游戏内搜不到 安全组未放行 UDP,或用了 -p 而非 host 网络 §3.3 / §4.3
第二实例起不来 BEACONPORT 或 RCON 端口冲突 §4.1
RCON 端口 21114 不监听 Rcon.cfg 的 Password= 为空,RCON 服务不启动
(不是端口冲突)
§6.4
改了 RCONPORT 后 RCON 连不上 Rcon.cfg 没跟着变(sed 只匹配 21114) §7.1
容器反复重启 内存不足被 OOM kill dmesg | grep -i oom 确认;降人数或调 mem_limit
下载卡住不动 Steam CDN 限速或中断 看日志;SteamCMD 会自动重试
配置改了不生效 未重启容器 docker compose restart
容器内无权限写 数据目录 owner 不是 uid 1000 sudo chown -R 1000:1000 <数据目录>
设了 RANDOM 没效果 镜像 bug,该变量无效 §10.1

12. 参考

官方 wiki 中文页用 6301 / 26301 作端口示例,那是部分托管商的习惯值, 不是镜像默认值。本文统一采用镜像默认的 Squad 官方标准端口 7787 / 27165。