部署:bubblewrap 沙箱、编译检查、访问口令、Docker Compose
- 学生程序每次运行放进 bubblewrap:断网,只读挂载系统文件、运行时和自己的工程目录, 看不到其他进程和服务器环境变量;限制堆内存 128 MB、CPU 时间 120 秒、运行 60 分钟, nice 10,全站同时运行上限 150;启动时自检沙箱,不可用时拒绝运行 - 编译检查(SIM001):网络、反射、启动进程、调用本机代码、加载程序集、dynamic 给出中文报错 - 访问口令:cookie 保存口令哈希,按真实 IP 限制输错次数;前端口令页 - 服务端配置集中到 config.ts,可用环境变量覆盖;Bun 直接提供打包好的网页 - Dockerfile(多阶段,服务端打包成单文件)、compose.yaml(Caddy 只收 HTTP,接入 NPM 的 npm_proxy 网络)、deploy/deploy.sh(本机构建后经 ssh 传到服务器)和部署说明 - 测试:沙箱隔离、内存上限、CPU 时间上限、编译检查 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
081ac6f924
commit
697f31e9ce
25 files changed
+869
-52
No files matched your search
@@ -0,0 +1,17 @@
|
||||
# HTTPS 由 Nginx Proxy Manager 负责,Caddy 只在 80 端口收 HTTP,转发给应用(包括 WebSocket /ws)。
|
||||
{
|
||||
auto_https off
|
||||
servers {
|
||||
# NPM 在 Docker 内网里,信任它带来的 X-Forwarded-For,取到学生的真实 IP。
|
||||
# 如果 NPM 前面还有公网上的 CDN,把 CDN 的 IP 段加在 private_ranges 后面。
|
||||
trusted_proxies static private_ranges
|
||||
trusted_proxies_strict
|
||||
}
|
||||
}
|
||||
|
||||
:80 {
|
||||
encode zstd gzip
|
||||
reverse_proxy app:3000 {
|
||||
header_up X-Real-IP {client_ip}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
# 部署
|
||||
|
||||
服务器上的结构:
|
||||
|
||||
```
|
||||
学生浏览器 ──HTTPS──▶ Nginx Proxy Manager(443) ──npm_proxy 网络──▶ caddy(iot-sim:80) ──▶ app(Bun + .NET + bubblewrap)
|
||||
```
|
||||
|
||||
- **app**:Bun 服务端、打包好的网页、C# 编译服务。学生程序每次运行都放进 bubblewrap 沙箱:断网,看不到服务器文件和其他进程,工程目录只读,限制内存(128 MB)、CPU 时间(120 秒)和运行时长(60 分钟)。
|
||||
- **caddy**:只收 HTTP,转发给 app,压缩网页;从 NPM 转发来的 `X-Forwarded-For` 里取学生的真实 IP。
|
||||
- 不映射宿主机端口,NPM 通过外部网络 `npm_proxy` 访问 `iot-sim:80`。
|
||||
|
||||
## 服务器要求
|
||||
|
||||
- Linux,Docker 和 Docker Compose v2,已有外部网络 `npm_proxy`(NPM 所在的网络)。
|
||||
- 2 核 8G 足够两个班同时使用(实测 100 个程序同时运行约 1.1 GB)。
|
||||
- 沙箱需要宿主机允许非特权用户命名空间。**Debian 12(本平台的服务器)默认允许**,不用设置,可用 `sysctl kernel.unprivileged_userns_clone` 确认输出为 1。
|
||||
- **Ubuntu 23.10 及以上**默认禁止,沙箱会起不来,需要放开:
|
||||
|
||||
```bash
|
||||
echo 'kernel.apparmor_restrict_unprivileged_userns=0' | sudo tee /etc/sysctl.d/60-userns.conf
|
||||
sudo sysctl --system
|
||||
```
|
||||
|
||||
其他发行版一般不需要。服务器启动时会自检沙箱,不通过会在日志里打印原因,并拒绝运行学生代码(不会在无隔离的情况下运行)。
|
||||
|
||||
## 第一次部署
|
||||
|
||||
1. 在服务器上准备目录和口令:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/iot-sim && cd ~/iot-sim
|
||||
# 部署脚本会上传 .env.example;也可以直接新建 .env:
|
||||
echo 'ACCESS_PASSWORD=换成你的口令' > .env
|
||||
chmod 600 .env
|
||||
```
|
||||
|
||||
2. 在本机(开发机)执行:
|
||||
|
||||
```bash
|
||||
deploy/deploy.sh 用户@服务器
|
||||
```
|
||||
|
||||
脚本在本机构建镜像,通过 ssh 传到服务器并启动。日志最后应看到"C# 编译服务已就绪",且没有"沙箱无法启动"。
|
||||
|
||||
3. 在 Nginx Proxy Manager 里添加 Proxy Host:
|
||||
- Domain Names:已备案的域名
|
||||
- Scheme:`http`,Forward Hostname / IP:`iot-sim`,Forward Port:`80`
|
||||
- **打开 Websockets Support**(运行程序靠 WebSocket,不打开会一直连不上)
|
||||
- SSL:申请证书,打开 Force SSL
|
||||
|
||||
4. 浏览器打开域名,输入口令进入。
|
||||
|
||||
## 日常操作
|
||||
|
||||
在服务器的 `~/iot-sim` 目录下:
|
||||
|
||||
| 做什么 | 命令 |
|
||||
|---|---|
|
||||
| 更新到新版本 | 在本机再执行一次 `deploy/deploy.sh 用户@服务器` |
|
||||
| 换口令 | 修改 `.env` 后 `docker compose up -d`(所有人要重新输入口令) |
|
||||
| 看日志 | `docker compose logs -f app` |
|
||||
| 看资源占用 | `docker stats` |
|
||||
| 重启 / 停止 | `docker compose restart` / `docker compose down` |
|
||||
|
||||
## 可调参数
|
||||
|
||||
写在 `.env` 里,改完 `docker compose up -d` 生效。
|
||||
|
||||
| 变量 | 默认 | 说明 |
|
||||
|---|---|---|
|
||||
| `ACCESS_PASSWORD` | (必填) | 访问口令 |
|
||||
| `APP_MEM_LIMIT` | `4g` | 整个平台的内存上限 |
|
||||
| `SIM_MAX_RUNS` | `150` | 全站同时运行的学生程序上限,超过时提示"服务器繁忙" |
|
||||
| `SIM_HEAP_LIMIT_MB` | `128` | 每个学生程序的内存上限 |
|
||||
| `SIM_CPU_LIMIT_SECONDS` | `120` | 每次运行累计 CPU 时间上限(防死循环) |
|
||||
| `SIM_MAX_RUN_MINUTES` | `60` | 每次运行最长时间 |
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
# 在本机构建镜像,传到服务器后启动(服务器不需要访问 Docker Hub,也不用自己编译)。
|
||||
# 用法:deploy/deploy.sh 用户@服务器 [服务器上的目录,默认 iot-sim]
|
||||
# 第一次部署前,先按 deploy/README.md 在服务器上准备好 .env
|
||||
set -euo pipefail
|
||||
|
||||
host=${1:?用法:deploy/deploy.sh 用户@服务器 [目录]}
|
||||
dir=${2:-iot-sim}
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
echo "== 构建镜像"
|
||||
docker compose build app
|
||||
docker pull -q caddy:2-alpine
|
||||
|
||||
echo "== 上传配置文件到 $host:$dir"
|
||||
ssh "$host" "mkdir -p $dir/deploy"
|
||||
scp -q compose.yaml .env.example "$host:$dir/"
|
||||
scp -q deploy/Caddyfile "$host:$dir/deploy/"
|
||||
|
||||
echo "== 上传镜像(约 130 MB)"
|
||||
docker save iot-sim:latest | gzip -1 | ssh "$host" 'gunzip | docker load'
|
||||
if ! ssh "$host" 'docker image inspect caddy:2-alpine >/dev/null 2>&1'; then
|
||||
docker save caddy:2-alpine | gzip -1 | ssh "$host" 'gunzip | docker load'
|
||||
fi
|
||||
|
||||
echo "== 启动"
|
||||
ssh "$host" "cd $dir && test -f .env || { echo '服务器上还没有 .env:cp .env.example .env 后设置 ACCESS_PASSWORD'; exit 1; }
|
||||
docker compose up -d --no-build --remove-orphans && docker image prune -f >/dev/null
|
||||
sleep 5 && docker compose logs --tail 5 app"
|
||||
Reference in new issue
Block a user