background
=
s
G
F
m
4
f
|
A
V
H
7
k
Q
&
p
F
-
R
z
z
N
-
R
$
H
j
n
*
N
0
P

favicon
favicon
回归终端
 

通过 Docker 部署

Reverier-Xu at 2026-01-28 11:20:46 3.8+ R2SPL

本文介绍使用 Docker Compose 部署回归终端平台服务、PostgreSQL、Valkey、NATS、registry、VictoriaLogs 和入口网关,同时通过外置 Kubernetes 控制面运行动态题目容器。

新手推荐

Docker Compose 部署方案可以用于生产环境,尤其适合把平台节点和题目 Kubernetes 集群分离的比赛或练习场。它的主要限制是:平台服务和基础组件不在 Kubernetes 内,无法使用 Kubernetes 对这些组件做统一调度、滚动发布、节点亲和、PodDisruptionBudget、PVC 调度和多节点故障迁移。 Kubernetes 只负责调度 challenge pods;如果希望平台、数据库、缓存、队列、日志和题目容器都由 Kubernetes 统筹规划,请使用 集群内部部署

本方案也支持通过 Podman 与 Podman Compose 部署,甚至可以说更推荐使用 Podman 技术栈,Podman 不依赖宿主机 root 权限,可以纯用户权限运行和管理,在某些场景下安全性可能相比 Docker 更佳一些。

部署结构

推荐结构如下:

组件部署位置说明
platformDocker Compose回归终端平台服务,读取 /etc/ret2shell/config.toml,通过挂载的 kubeconfig 访问外部 Kubernetes。
PostgreSQLDocker Compose平台主数据库,使用 Compose volume 持久化。
ValkeyDocker Compose会话、令牌、缓存和限速状态。
NATS JetStreamDocker Compose后台任务和事件队列。
VictoriaLogsDocker Compose可选但推荐保留,用于集中日志检索。
registryDocker Compose可选受控题目镜像仓库。Kubernetes 节点必须能访问它的外部地址。
gatewayDocker Compose / 外置网关暴露 Web、API 和 WebSocket。TLS 可以由此处或外部负载均衡处理。
Kubernetes外置集群只运行 ret2shell-challenge namespace 下的动态题目 Pod 和 Service。

这种结构把平台数据放在稳定的 Compose 主机上,把题目算力放在 Kubernetes 集群中。平台主机需要能访问 Kubernetes API;Kubernetes 节点需要能访问题目镜像仓库。

准备目录

以下示例默认把部署文件放在 /opt/ret2shell。如果使用其他路径,后续命令中的路径也要同步调整。

sudo mkdir -p /opt/ret2shell/config /opt/ret2shell/nginx
sudo chown -R "$USER:$USER" /opt/ret2shell
cd /opt/ret2shell

使用 uuidgen 生成若干个随机 uuidv4,把生成的 UUID 分别用于 config.toml 中的 signing_key.env 中的 POSTGRES_PASSWORDVALKEY_PASSWORDNATS_TOKEN。不要继续使用示例密码。

创建 .env

R2S_VERSION=<release-version>
 
POSTGRES_PASSWORD=<replace-with-random-postgres-password>
VALKEY_PASSWORD=<replace-with-random-valkey-password>
NATS_TOKEN=<replace-with-random-nats-token>
 
REGISTRY_BIND=10.0.0.10

R2S_VERSION 应替换为实际部署的 release 版本。REGISTRY_BIND 是平台主机上允许 Kubernetes 节点访问 registry 的内网地址;如果不用内置 registry,可以删除 registry 服务和 [cluster.registry] 配置。

推荐 Compose 配置

创建 docker-compose.yml

name: ret2shell
 
services:
  database:
    image: postgres:18-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: ret2shell
      POSTGRES_USER: ret2shell
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - database:/var/lib/postgresql/data
 
  cache:
    image: valkey/valkey:8-alpine
    restart: unless-stopped
    command:
      - valkey-server
      - --appendonly
      - "yes"
      - --requirepass
      - ${VALKEY_PASSWORD}
    volumes:
      - cache:/data
 
  message_queue:
    image: nats:2-alpine
    restart: unless-stopped
    command:
      - -js
      - -sd
      - /data
      - --auth
      - ${NATS_TOKEN}
    volumes:
      - message_queue:/data
 
  vlogs:
    image: victoriametrics/victoria-logs:v1.43.1
    restart: unless-stopped
    command:
      - --storageDataPath=/vlogs
    volumes:
      - vlogs:/vlogs
 
  registry:
    image: registry:3
    restart: unless-stopped
    environment:
      REGISTRY_STORAGE_DELETE_ENABLED: "true"
      REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /var/lib/registry
    command:
      - serve
      - /etc/docker/registry/config.yml
    volumes:
      - registry:/var/lib/registry
    ports:
      - "${REGISTRY_BIND}:5000:5000"
 
  platform:
    image: ghcr.io/ret2shell/ret2shell:${R2S_VERSION}
    restart: unless-stopped
    depends_on:
      - database
      - cache
      - message_queue
      - vlogs
      - registry
    volumes:
      - platform:/var/lib/ret2shell
      - platform_cache:/var/cache/ret2shell
      - platform_log:/var/log/ret2shell
      - ./config/config.toml:/etc/ret2shell/config.toml:ro
      - ./config/sensitive_word_list.txt:/etc/ret2shell/sensitive_word_list.txt:ro
      - ./config/kubeconfig.yaml:/etc/ret2shell/kubeconfig.yaml:ro
    expose:
      - "8080"
 
  gateway:
    image: nginx:alpine
    restart: unless-stopped
    depends_on:
      - platform
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
    ports:
      - "80:80"
 
volumes:
  database:
  cache:
  message_queue:
  vlogs:
  registry:
  platform:
  platform_cache:
  platform_log:

这个配置只把 gateway 的 80 端口和 registry 的内网 5000 端口暴露到宿主机。PostgreSQL、Valkey、NATS 和 VictoriaLogs 保持在 Compose 网络内部。生产环境应在宿主机防火墙或云安全组中限制 5000 端口只允许 Kubernetes 节点访问。

创建 nginx/nginx.conf

user nginx;
worker_processes auto;
 
events {
    worker_connections 1024;
}
 
http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
 
    map $http_upgrade $connection_upgrade {
        default upgrade;
        "" close;
    }
 
    server {
        listen 80 default_server;
        client_max_body_size 1024m;
 
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
 
        location / {
            proxy_pass http://platform:8080;
        }
    }
}

示例让 Nginx 反向代理所有路径,由平台镜像内置的前端静态文件服务返回 Web 页面。需要更高静态文件吞吐时,可以把前端产物单独挂载给 Nginx,并只把 /api、WebSocket 和 registry 管理接口代理到平台。

生产环境通常还需要 TLS。可以在 gateway 前放置云负载均衡、Caddy、宿主机 Nginx,或扩展上面的 Nginx 配置监听 443。启用 HTTPS 后,config.toml 中的 external_https 必须设置为 true

配置平台

创建 config/sensitive_word_list.txt,可以先放一个空文件:

touch config/sensitive_word_list.txt

创建 config/config.toml。下面是与上方 Compose 配套的生产配置起点:

[auditor]
sensitive_word_list = '/etc/ret2shell/sensitive_word_list.txt'
 
[auth]
buffer_time = 21600
expires_time = 86400
signing_key = '<replace-with-random-signing-key>'
 
[bucket]
path = '/var/lib/ret2shell/bucket'
 
[cache]
url = 'redis://:<replace-with-valkey-password>@cache:6379'
 
[captcha]
enabled = true
difficulty = 4
validator = 'pow'
 
[cluster]
enabled = true
auto_infer = false
try_default = false
kube_config_path = '/etc/ret2shell/kubeconfig.yaml'
node_selector = 'challenge'
enable_capture = true
capture_directory = '/var/cache/ret2shell/captures'
 
[cluster.registry]
enabled = true
server = 'registry:5000'
external = 'registry.internal.example.com:5000'
insecure = true
 
[database]
host = 'database'
port = 5432
ssl_mode = 'disable'
user = 'ret2shell'
password = '<replace-with-postgres-password>'
db = 'ret2shell'
 
# email 节推荐在平台部署完成后从 web 界面配置
[email]
enabled = false
host = 'smtp.example.com'
password = ''
port = 587
sender = 'Ret2Shell'
tls = 'starttls'
username = ''
 
[logging]
compress = true
directory = '/var/log/ret2shell'
files_kept = 3
level = 'info,tower_http=info,sqlx=warn'
victoria = 'http://vlogs:9428'
 
[media]
anti_theft = true
limit = 100
path = '/var/lib/ret2shell/media'
 
[queue]
host = 'message_queue'
port = 4222
token = '<replace-with-nats-token>'
 
[server]
host = '0.0.0.0'
port = 8080
api_base_path = '/api'
cors_origins = 'https://ctf.example.com'
external_domain = 'ctf.example.com'
external_https = true
 
[server.rate_limit]
burst_limit = 32
burst_restore_rate = 500
 
[server.frontend]
serve_type = 'static'
path = '/var/www/html'

.env 变量仅通过 docker-compose 应用于各个服务组件,所以你还需要将其填入 config.toml 中,便于平台后续使用,示例中的密码、token、域名和 registry 地址都要写成最终值。修改配置文件后需要重启 platform 服务:

docker compose restart platform

配置项说明

配置节说明
[auditor]敏感词文件路径,用于队伍名等内容审核。Compose 中应指向 /etc/ret2shell/sensitive_word_list.txt
[auth]登录令牌配置。signing_key 必须替换为随机强密钥,泄露后需要让所有用户重新登录。
[bucket]题目仓库、附件、checker、环境配置等文件的持久化目录。Compose 中放在 platform volume。
[cache]Valkey / Redis 连接地址。上方 Compose 开启了 Valkey 密码,因此 URL 使用 redis://:<password>@cache:6379
[captcha]注册、登录等场景的验证码策略。默认 pow 不依赖第三方服务。
[cluster]动态题目容器控制配置。Compose 场景应关闭 auto_infertry_default,通过 kube_config_path 指向挂载进容器的 kubeconfig。node_selector = 'challenge' 会让题目 Pod 选择带有 ret.sh.cn/workload=challenge 标签的节点。
[cluster.registry]受控题目镜像仓库。server 是平台容器推送镜像使用的 Compose 内部地址,external 是 Kubernetes 节点拉取镜像使用的地址。两者经常不同。
[database]PostgreSQL 连接配置。必须和 Compose 中的数据库用户名、密码、库名保持一致。
[email]SMTP 邮件配置。不开启邮件时保持 enabled = false
[logging]本地日志和 VictoriaLogs 配置。victoria 指向 Compose 内部的 vlogs 服务后,后台日志查询功能才有集中日志来源。
[media]用户上传媒体文件配置。path 应在持久化 volume 内。
[queue]NATS JetStream 连接配置。上方 Compose 使用 token 鉴权,因此这里填写同一个 token。
[server]平台监听地址、外部域名、HTTPS 状态和 CORS。external_domain 必须是用户实际访问的平台域名;有 TLS 入口时设置 external_https = true
[server.rate_limit]API 限流参数。默认值适合起步,正式比赛可以按访问量调整。
[server.frontend]平台内置静态文件服务。使用上方 Nginx 代理所有路径时保持 /var/www/html

配置文件在平台启动时读取,运行过程中不会自动热重载。部分站点名称、页脚、题目运行配置可以在后台管理界面覆盖;基础连接信息、路径、密钥和 kubeconfig 仍应通过 DevOps 流程修改并重启服务。

注入外置 Kubernetes kubeconfig

如果平台不使用 Kubernetes 与动态容器功能,那么接下来的过程都可以忽略。可以在同一服务器上同时安装 docker 与 k8s 集群,有关 k8s/k3s 的快速安装设置可以参考 环境准备

平台容器不在 Kubernetes 集群内,不能使用集群内 ServiceAccount 自动发现。推荐在外置 Kubernetes 集群中创建专用 ServiceAccount,然后把 kubeconfig 以只读文件挂载到 /etc/ret2shell/kubeconfig.yaml

准备 Kubernetes API 地址

平台主机必须能访问 Kubernetes API,例如:

curl -k https://k8s-api.example.com:6443/version

如果使用 k3s,并且 kubeconfig 中的 server 还是 https://127.0.0.1:6443,需要改成平台容器可访问的控制面地址。生产环境建议在安装 k3s 时加入固定的 API 域名或内网 IP 作为 TLS SAN:

curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="server --tls-san k8s-api.example.com" sh -

已经安装的集群也可以导出 kubeconfig 后手动把 clusters[].cluster.server 改成可访问地址,但证书必须信任该地址,否则平台连接 Kubernetes API 会失败。

创建控制账号

在 Kubernetes 控制面执行:

apiVersion: v1
kind: Namespace
metadata:
  name: ret2shell-challenge
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: ret2shell-controller
  namespace: ret2shell-challenge
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: ret2shell-controller-read
rules:
  - apiGroups: [""]
    resources: ["namespaces", "nodes", "configmaps"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: ret2shell-controller
  namespace: ret2shell-challenge
rules:
  - apiGroups: [""]
    resources: ["pods", "pods/log", "pods/portforward", "services"]
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
  - apiGroups: ["networking.k8s.io"]
    resources: ["networkpolicies"]
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: ret2shell-controller
subjects:
  - kind: ServiceAccount
    name: ret2shell-controller
    namespace: ret2shell-challenge
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: ret2shell-controller-read
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: ret2shell-controller
  namespace: ret2shell-challenge
subjects:
  - kind: ServiceAccount
    name: ret2shell-controller
    namespace: ret2shell-challenge
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: ret2shell-controller

保存为 ret2shell-controller.yaml 后应用:

kubectl apply -f ret2shell-controller.yaml
kubectl label node worker-1 ret.sh.cn/workload=challenge
kubectl label node worker-2 ret.sh.cn/workload=challenge

node_selector = 'challenge' 时,平台创建的题目 Pod 只会调度到这些节点。不同比赛也可以在后台单独设置 node selector。

生成 kubeconfig

在能访问 Kubernetes 的机器上生成 config/kubeconfig.yaml

cd /opt/ret2shell
 
TOKEN="$(kubectl -n ret2shell-challenge create token ret2shell-controller --duration=8760h)"
SERVER="$(kubectl config view --raw --minify -o jsonpath='{.clusters[0].cluster.server}')"
CA_DATA="$(kubectl config view --raw --minify -o jsonpath='{.clusters[0].cluster.certificate-authority-data}')"
 
cat > config/kubeconfig.yaml <<EOF
apiVersion: v1
kind: Config
clusters:
  - name: ret2shell
    cluster:
      server: ${SERVER}
      certificate-authority-data: ${CA_DATA}
users:
  - name: ret2shell-controller
    user:
      token: ${TOKEN}
contexts:
  - name: ret2shell
    context:
      cluster: ret2shell
      user: ret2shell-controller
current-context: ret2shell
EOF
 
chmod 600 config/kubeconfig.yaml

如果生成出来的 SERVER 不是平台容器可访问地址,先编辑 config/kubeconfig.yaml 再启动平台。Token 有有效期,到期前需要重新生成 kubeconfig 并重启 platform

配置 registry 拉取地址

启用 Compose 内置 registry 时,Kubernetes 节点会从 [cluster.registry].external 拉取镜像。这个地址必须从每个 Kubernetes 节点可达。

如果使用 k3s 且 registry 通过内网 HTTP 暴露,可以在每个节点配置:

mirrors:
  "registry.internal.example.com:5000":
    endpoint:
      - "http://registry.internal.example.com:5000"

保存为 /etc/rancher/k3s/registries.yaml 后重启 k3s:

sudo systemctl restart k3s

如果 registry 使用正式 TLS 和可信证书,可以把 [cluster.registry].insecure 设置为 false,并让 external 指向 HTTPS registry 域名。无论是否使用 TLS,都应通过防火墙限制 registry 只允许平台主机和 Kubernetes 节点访问。

启动和检查

启动服务:

cd /opt/ret2shell
docker compose pull
docker compose up -d
docker compose ps

检查平台:

docker compose logs -f platform
curl -fsS http://127.0.0.1/api/ping

检查 Kubernetes 连接时,先在宿主机使用同一份 kubeconfig:

KUBECONFIG=/opt/ret2shell/config/kubeconfig.yaml kubectl get nodes -o wide
KUBECONFIG=/opt/ret2shell/config/kubeconfig.yaml kubectl -n ret2shell-challenge get pods,svc

平台启动后会确认 ret2shell-challenge namespace 和默认网络策略。如果日志中出现 kubeconfig、证书、权限或 API 连接错误,优先检查 config/kubeconfig.yaml 中的 server、token 有效期、RBAC 和平台主机到 Kubernetes API 的网络连通性。

备份和升级

生产环境至少备份 PostgreSQL、platform volume、registry volume 和部署配置目录。

数据库备份:

mkdir -p backups
docker compose exec -T database pg_dump -U ret2shell ret2shell > "backups/ret2shell.$(date +%F).sql"

升级平台时,先备份数据库和配置,再修改 .env 中的 R2S_VERSION

docker compose pull platform
docker compose up -d platform gateway
docker compose logs -f platform

如果同时升级 PostgreSQL、Valkey、NATS、registry 或 VictoriaLogs,先阅读对应组件的升级说明,并确认 volume 数据格式兼容。不要在没有备份的情况下直接跨大版本升级数据库。