Docker 新建数据卷时属主是 root,而平台用普通用户(APP_UID)运行,SQLite 打不开数据库, 新容器反复退出。compose 加一个启动前只运行一次的 init-data(root 把 /data 交给平台用户), Dockerfile 里也预先建好属于平台用户的 /data。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
81 lines
2.8 KiB
YAML
81 lines
2.8 KiB
YAML
# 部署:先在 .env 里写好 TEACHER_PASSWORD(参考 .env.example),再 docker compose up -d
|
|
# HTTPS 由 Nginx Proxy Manager 负责,它在 npm_proxy 网络里转发到 iot-sim:80;步骤见 deploy/README.md
|
|
services:
|
|
# 启动前把数据卷交给平台用户(Docker 新建数据卷时属主是 root,平台用普通用户运行,写不进去)
|
|
init-data:
|
|
image: iot-sim:latest
|
|
# 和 app 同一个镜像(构建有缓存,不会重复构建);写上 build,新服务器上就不会去 Docker Hub 拉这个镜像
|
|
build: .
|
|
user: root
|
|
command: ["sh", "-c", "chown -R \"$$APP_UID\" /data"]
|
|
volumes:
|
|
- data:/data
|
|
restart: "no"
|
|
networks: [default]
|
|
|
|
app:
|
|
image: iot-sim:latest
|
|
build: .
|
|
restart: unless-stopped
|
|
depends_on:
|
|
init-data:
|
|
condition: service_completed_successfully
|
|
environment:
|
|
# 老师账号(只有一个);学生账号由老师在网页上导入名单
|
|
TEACHER_USERNAME: ${TEACHER_USERNAME:-admin}
|
|
TEACHER_PASSWORD: ${TEACHER_PASSWORD:?请在 .env 里设置 TEACHER_PASSWORD}
|
|
DATA_DIR: /data
|
|
# AI 评阅(DeepSeek);不设置密钥就不做 AI 评阅
|
|
DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY:-}
|
|
AI_MODEL: ${AI_MODEL:-deepseek-flash}
|
|
# 以下可在 .env 里调整,说明见 deploy/README.md
|
|
SIM_MAX_RUNS: ${SIM_MAX_RUNS:-150}
|
|
SIM_HEAP_LIMIT_MB: ${SIM_HEAP_LIMIT_MB:-128}
|
|
SIM_CPU_LIMIT_SECONDS: ${SIM_CPU_LIMIT_SECONDS:-120}
|
|
SIM_MAX_RUN_MINUTES: ${SIM_MAX_RUN_MINUTES:-60}
|
|
# 容器文件系统只读,只有 /tmp(学生工程的临时目录)和数据卷可写
|
|
read_only: true
|
|
tmpfs:
|
|
- /tmp:size=1g
|
|
# 数据库(账号、班级、学生作品),重新部署不会丢
|
|
volumes:
|
|
- data:/data
|
|
cap_drop: [ALL]
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
# bubblewrap 要创建用户命名空间、挂载新的 /proc,Docker 默认的 seccomp 规则和 /proc 遮罩会拦住它。
|
|
# 放开后,学生程序仍然在 bubblewrap 里:断网、看不到其他进程和文件、不是 root。
|
|
- seccomp=unconfined
|
|
- apparmor=unconfined
|
|
- systempaths=unconfined
|
|
# 整个平台的资源上限(两个班同时运行约需 1.5 GB),可在 .env 里用 APP_MEM_LIMIT 调整
|
|
mem_limit: ${APP_MEM_LIMIT:-4g}
|
|
pids_limit: 4096
|
|
expose: ["3000"]
|
|
networks: [default]
|
|
|
|
caddy:
|
|
image: caddy:2-alpine
|
|
restart: unless-stopped
|
|
depends_on: [app]
|
|
expose:
|
|
- 80
|
|
networks:
|
|
default:
|
|
npm_proxy:
|
|
# NPM 里转发到 http://iot-sim:80
|
|
aliases: [iot-sim]
|
|
volumes:
|
|
- ./deploy/Caddyfile:/etc/caddy/Caddyfile:ro
|
|
- caddy_data:/data
|
|
- caddy_config:/config
|
|
|
|
volumes:
|
|
data:
|
|
caddy_data:
|
|
caddy_config:
|
|
|
|
networks:
|
|
npm_proxy:
|
|
external: true
|