实机部署验证时发现 21114/tcp 不监听,根因不是端口冲突,而是 Rcon.cfg 的 Password= 为空导致 RCON 服务不启动。补充: - 服务端的确切告警日志与设置成功后的日志 - Rcon.cfg 为 CRLF 行尾,sed 修改需保留 \r - 建议在服务器本机生成随机密码,不经过剪贴板或聊天记录 - 排障表新增该条目 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
460 lines
16 KiB
Markdown
460 lines
16 KiB
Markdown
# 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。
|