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
207 lines
9.2 KiB
YAML
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.
|