Files
squaddocker/README.md
T
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

460 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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**(共享游戏文件)<br>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`:
```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 拉取、打标、推送
```bash
# 该镜像只有 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,目标机拉取后比对,确认跑的是同一个镜像:
```bash
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`:
```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` 成功 → 再验证:
> ```bash
> 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 准备数据目录
```bash
sudo mkdir -p /srv/squad/inst1
# 镜像内以 uid 1000 (steam) 运行,必须给写权限
sudo chown -R 1000:1000 /srv/squad
```
### 6.2 配置
```bash
cp .env.example .env
$EDITOR .env
```
### 6.3 启动
```bash
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`,否则可能出现解析问题:
> ```bash
> sudo sed -i -E "s/^Password=.*$/Password=<你的密码>\r/" Rcon.cfg
> ```
> 生成随机密码时建议直接在服务器上生成,不要经过聊天记录或剪贴板:
> ```bash
> openssl rand -base64 24 | tr -d '/+=' | cut -c1-24
> ```
改完重启生效:
```bash
docker compose restart squad1
```
---
## 7. 多实例
### 7.1 RCON 端口:自动处理,但有个后续陷阱
**[实测]** `entry.sh` 会在启动时自动重写 `Rcon.cfg` 里的端口:
```bash
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
```bash
docker compose --profile instance2 up -d
```
启用前确认硬件满足 §2.2 的双实例要求。
---
## 8. 镜像中未见于文档的环境变量
以下变量**不在镜像的默认 `ENV` 列表里**,是从 `entry.sh` 源码中确认的 **[实测]**,
但相当有用:
### 8.1 `SERVER_NAME`
设置后,`entry.sh` 会把它写入 `Server.cfg` 的 `ServerName`(源码第 22–25 行):
```bash
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 更新,所以游戏发新版本时:
```bash
docker compose restart squad1
```
更新镜像本身:
```bash
docker compose pull && docker compose up -d
```
游戏文件在卷里,不会丢。
### 查看日志
```bash
docker compose logs -f --tail=200 squad1
```
服务端自身日志在 `/srv/squad/inst1/SquadGame/Saved/Logs/`。
容器日志轮转已在 `docker-compose.yml` 里配置(50MB × 3)。也可以在
`/etc/docker/daemon.json` 里设为全局默认。
### 停止
```bash
docker compose stop squad1
```
> **[实测]** `entry.sh` 最后一行是 `bash SquadGameServer.sh ...` 而非 `exec`,
> PID 1 是 bash 脚本。bash 在同步等待子进程时不转发 SIGTERM,
> 所以优雅停止未必真的优雅。`docker-compose.yml` 里设了
> `stop_grace_period: 120s` 尽量留出落盘时间,但不保证。
> 重要操作前建议先在游戏内用 RCON 确认无人在线。
### 备份
只需备份配置,不含游戏本体:
```bash
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 内置特殊变量,环境变量覆盖不了它** —— 每次读取都返回随机整数。
实测:
```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 服务不启动<br>(不是端口冲突) | §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(中文):https://squad.fandom.com/wiki/Server_Installation/zh
- 镜像仓库:https://hub.docker.com/r/cm2network/squad/
- Squad AppID:`403240`,创意工坊 ID:`393380`
> 官方 wiki 中文页用 6301 / 26301 作端口示例,那是部分托管商的习惯值,
> **不是镜像默认值**。本文统一采用镜像默认的 Squad 官方标准端口 7787 / 27165。