Files
wehub-resource-sync e4dcfc49aa
Tests / Lint and Format (push) Waiting to run
Tests / Web Node Tests (push) Waiting to run
Tests / Import Check (Python 3.11) (push) Waiting to run
Tests / Import Check (Python 3.12) (push) Waiting to run
Tests / Import Check (Python 3.13) (push) Waiting to run
Tests / Import Check (Python 3.14) (push) Waiting to run
Tests / Python Tests (Python 3.11) (push) Blocked by required conditions
Tests / Python Tests (Python 3.12) (push) Blocked by required conditions
Tests / Python Tests (Python 3.13) (push) Blocked by required conditions
Tests / Python Tests (Python 3.14) (push) Blocked by required conditions
Tests / Test Summary (push) Blocked by required conditions
chore: import upstream snapshot with attribution
2026-07-13 13:00:43 +08:00

207 lines
9.2 KiB
YAML

# ============================================
# DeepTutor — Podman Compose (rootless, read-only rootfs)
# ============================================
# Requires:
# * podman 4.1+ (rootless)
# * a compose provider. On podman 5.x without the native Go-based
# compose plugin, `podman compose` is a thin wrapper around
# `podman-compose` (Python). Both work; the `docker-compose` CLI
# also works if it's installed and pointed at the podman socket.
# This file is `podman-compose` 1.5+ compatible.
# * Verify with: `podman compose version` and `podman info | grep -i rootless`
#
# Bring up (this file lives next to docker-compose.yml, so always pass -f):
# cp .env.example .env # then edit
# podman compose -f compose.yaml up -d
# podman compose -f compose.yaml ps
# podman compose -f compose.yaml logs -f deeptutor
#
# Tip: alias it for the session — alias dc='podman compose -f compose.yaml'
# or export COMPOSE_FILE=compose.yaml so the default `podman compose`
# picks this file up without -f.
#
# Tear down (keeps volumes):
# podman compose -f compose.yaml down
# Wipe data (DESTRUCTIVE):
# podman compose -f compose.yaml down -v
#
# Top-level choices (see git log / PR for full rationale):
# * All services run with `read_only: true`. The `tmpfs:` mounts below are
# the only writable surface inside each container's rootfs.
# * `userns_mode: keep-id` plus the `:U` suffix on every volume mount
# implements rootless-keep-uid: host UID $UID maps to container UID $UID.
# Files you create on the host are visible as your own user inside.
# * Host-side port bindings are LOOPBACK ONLY (127.0.0.1:). Drop the
# `127.0.0.1:` prefix to expose on all interfaces.
# * Two services: `deeptutor` (backend+frontend in one GHCR image, run
# under supervisord) and `pocketbase` (optional auth/storage sidecar).
# The sandbox-runner sidecar from docker-compose.yml is intentionally
# NOT included: the main app falls back to bwrap (Linux, if installed
# in the image) or the restricted subprocess backend controlled by
# the `sandbox_allow_subprocess` setting in system.json. See
# deeptutor/services/sandbox/config.py:build_backend().
# * The backend and frontend run as a non-root `deeptutor` user (UID
# 1000). supervisord runs as root (PID 1), the entrypoint chowns
# `/app/data`, and each program is dropped to `deeptutor` via its
# per-program `user=` directive, so the app processes are UID 1000
# inside the container. Under `userns_mode: keep-id` that maps to the
# host user.
# * URL knowledge lives in `data/user/settings/system.json` (in-network
# `next_public_api_base` for the typical case, or
# `next_public_api_base_external` for cloud/external). The entrypoint
# reads the JSON on every start and exports `DEEPTUTOR_API_BASE_URL`,
# which `web/proxy.ts` reads to rewrite `/api/*` and `/ws/*` to the
# configured backend at request time. There is no build-time
# placeholder, no runtime `sed -i` on the bundle, and no compose env
# var for the API base.
# * Runtime settings (ports, auth, model catalog, integrations) live in
# data/user/settings/*.json INSIDE the deeptutor-data volume. The
# entrypoint unsets BACKEND_PORT, FRONTEND_PORT, NEXT_PUBLIC_API_BASE,
# NEXT_PUBLIC_API_BASE_EXTERNAL, AUTH_ENABLED, POCKETBASE_URL, etc.
# and re-exports them from the JSONs on every start. So: edit JSONs
# + `podman compose restart deeptutor`, NOT compose env vars.
# ============================================
name: deeptutor
services:
# ----------------------------------------------------------
# PocketBase — optional auth + storage sidecar
# ----------------------------------------------------------
# Activated by setting `integrations.pocketbase_url` to
# `http://pocketbase:8090` in data/user/settings/integrations.json.
# Leave blank to run with the SQLite fallback (single-user / invite-only).
pocketbase:
image: ghcr.io/muchobien/pocketbase:latest
container_name: deeptutor-pocketbase
pull_policy: always
restart: unless-stopped
userns_mode: keep-id
read_only: true
tmpfs:
- /tmp:size=64m,mode=1777
- /run:size=16m,mode=0755
- /var/run:size=8m,mode=0755
ports:
- "127.0.0.1:${HOST_PORT_POCKETBASE:-8090}:8090"
volumes:
# Bind mount a host directory so the host user (UID $UID) owns
# the SQLite DB and pocketbase's public/hooks dirs. With
# userns_mode: keep-id, the process inside is also UID $UID, so
# reads and writes line up. (The pocketbase image's entrypoint
# uses --dir=/pb_data --publicDir=/pb_public --hooksDir=/pb_hooks
# — absolute paths, no /pb/ prefix.)
- ./data/pocketbase:/pb_data:U
- ./data/pocketbase/public:/pb_public:U
- ./data/pocketbase/hooks:/pb_hooks:U
networks:
- deeptutor
healthcheck:
test:
- CMD
- wget
- --quiet
- --tries=1
- --spider
- http://localhost:8090/api/health
interval: 10s
timeout: 5s
retries: 3
start_period: 10s
# ----------------------------------------------------------
# DeepTutor — backend (FastAPI :8001) + frontend (Next.js :3782)
# ----------------------------------------------------------
# Both run inside the same image, supervised by supervisord under the
# unprivileged `deeptutor` user (UID 1000). The image's entrypoint loads
# runtime settings from data/user/settings/*.json and exports them into
# the supervisord children's environment, so changing ports/auth/providers
# means editing JSONs + `podman compose restart`.
deeptutor:
image: ghcr.io/hkuds/deeptutor:latest
container_name: deeptutor
pull_policy: always
restart: unless-stopped
userns_mode: keep-id
read_only: true
# Writable surfaces inside the RO rootfs. Each tmpfs is scoped to a
# specific need:
# /tmp — scratch for Python, uvicorn, Node, Next.js +
# supervisord's pidfile (/tmp/supervisord.pid)
# /run — standard Linux runtime dir
# /var/run — standard runtime dir (kept writable for any tooling)
# /var/log — covers any supervisord child that defaults there
# /root — catches stray $HOME-style writes (image sets
# PYTHONDONTWRITEBYTECODE=1, but be safe)
# /home — same, for any non-root code path
tmpfs:
- /tmp:size=512m,mode=1777
- /run:size=32m,mode=0755
- /var/run:size=8m,mode=0755
- /var/log:size=64m,mode=0755
- /root:size=16m,mode=0700
- /home:size=16m,mode=0755
ports:
- "127.0.0.1:${HOST_PORT_BACKEND:-8001}:8001"
- "127.0.0.1:${HOST_PORT_FRONTEND:-3782}:3782"
volumes:
# Bind mount a host directory so the host user (UID $UID) owns
# the entire data tree. With userns_mode: keep-id the process
# inside is also UID $UID, so writes from the FastAPI backend,
# Next.js, and supervisord all line up. The same path was used
# by the original docker-compose.yml; we keep it for consistency.
# The data tree holds: admin workspace + runtime settings
# (data/user), per-user workspaces (data/users), partners
# (data/partners), accounts/grants/audit (data/system),
# knowledge bases, memory, logs. One tree to back up.
- ./data:/app/data:U
environment:
# Time zone — picked up by Python (time.tzset) and Next.js at boot.
- TZ=${TZ:-UTC}
# Local LLM (LM Studio / Ollama / vLLM) base URLs in
# data/user/settings/model_catalog.json should use
# `http://host.containers.internal:PORT` (podman) — NOT localhost,
# which inside the container is the container's own loopback.
# Uncomment to enable PocketBase auth (also set integrations.json):
# - POCKETBASE_URL=http://pocketbase:8090
# - POCKETBASE_EXTERNAL_URL=http://localhost:8090
healthcheck:
test:
- CMD
- curl
- -fsS
- http://localhost:8001/
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
depends_on:
pocketbase:
condition: service_healthy
networks:
- deeptutor
# Optional hardening — uncomment to taste. supervisord runs as root
# (PID 1) and setuids each program down to UID 1000, which needs
# CAP_SETUID/CAP_SETGID — so do NOT `cap_drop: ALL` here, it would stop
# the backend/frontend from spawning. `no-new-privileges` is safe (a
# root process dropping its own privileges is not "gaining" any).
# security_opt:
# - no-new-privileges:true
# # cap_drop: ALL is unsafe — supervisord needs CAP_SETUID/CAP_SETGID to
# # drop its children to the deeptutor user.
# Resource limits. Honored on cgroup v2 hosts; podman warns and skips
# on cgroup v1 or no-cgroup environments.
# pids_limit: 1024
# mem_limit: 4g
# cpus: 4.0
networks:
deeptutor:
driver: bridge
# Named volumes intentionally not used: with userns_mode: keep-id the
# container process runs as the host user (UID $UID), but podman
# auto-creates named volumes with UID 100000 (userns-mapped root),
# so 755 perms + wrong owner = PermissionError on the first JSON
# write. Bind mounts on a host directory you own work cleanly.