At 2:17 AM, the Open WebUI container restarted mid-upload. When it came back, the SQLite database was corrupted—conversations gone, user sessions dead. The root cause wasn't the restart. It was two operators uploading PS5 executables simultaneously while the UI tried to write chat history to the same file. SQLite doesn't handle concurrent writes gracefully, and mounting the host filesystem directly into the container meant there was no write buffering, no transaction isolation, and no recovery path.
That incident, combined with an earlier exposure where an admin panel became internet-accessible through a misconfigured reverse proxy default route, forced a architectural redesign. The problem wasn't Open WebUI. The problem was treating a complex AI workflow—model routing, credential management, build orchestration—as a single-container toy demo.
I evaluated boykopovar/AnyPS5 as an external systems engineer building automation for PS5 executable porting on Linux and Windows. The technical challenge isn't exposing a web UI. It's exposing the workflow without exposing the build host, model gateway credentials, or administrative endpoints to untrusted networks or prompts.
The Four-Layer Split
The production design separates concerns explicitly:
- Open WebUI provides the operator interface but never touches Docker sockets, SSH keys, or raw filesystem access.
- LiteLLM acts as an OpenAI-compatible gateway with model policy, retry logic, and Redis-backed response caching.
- Traefik terminates TLS and enforces network boundaries—only Tailscale address ranges are permitted.
- AnyPS5 runs as a separate host-side workload invoked through a scoped runner API, never mounted into containers.
Model-generated commands are untrusted input. If an LLM suggests rm -rf /, the runner should reject it, not execute it. The UI should never have the privilege to honor that request in the first place.
Here's the deployment bundle. Create a .env file with a strong random LITELLM_MASTER_KEY and provide the relay credential only if you're enabling the benchmark route:
services: redis: image: redis:7.4-alpine command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD}"] restart: unless-stopped volumes: - redis-data:/data networks: [private] litellm: image: ghcr.io/berriai/litellm:main-stable command: ["--config", "/app/config.yaml", "--port", "4000"] restart: unless-stopped environment: LITELLM_MASTER_KEY: ${LITELLM_MASTER_KEY} REDIS_PASSWORD: ${REDIS_PASSWORD} B_LOST_API_KEY: ${B_LOST_API_KEY:-disabled} volumes: - ./litellm.yaml:/app/config.yaml:ro depends_on: [redis] networks: [private] open-webui: image: ghcr.io/open-webui/open-webui:v0.6.41 restart: unless-stopped environment: OPENAI_API_BASE_URLS: http://litellm:4000/v1 OPENAI_API_KEYS: sk-local WEBUI_AUTH_TRUSTED_EMAIL_HEADER: "" volumes: - open-webui-data:/app/backend/data depends_on: [litellm] networks: [private] labels: traefik.enable: "true" traefik.http.routers.openwebui.rule: Host(`ai.tailnet.example`) traefik.http.routers.openwebui.entrypoints: websecure traefik.http.routers.openwebui.tls: "true" traefik.http.routers.openwebui.middlewares: tailnet-only@file traefik.http.services.openwebui.loadbalancer.server.port: "8080" traefik: image: traefik:v3.2 restart: unless-stopped command: - --entrypoints.websecure.address=:443 - --providers.docker=true - --providers.docker.exposedbydefault=false - --providers.file.filename=/etc/traefik/dynamic.yaml ports: - "443:443" volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - ./dynamic.yaml:/etc/traefik/dynamic.yaml:ro - ./certs:/certs:ro networks: [private] networks: private: volumes: redis-data: open-webui-data: The LiteLLM config keeps model routing explicit. The B-Lost relay at https://b-lost.com/v1 is included as a production baseline for multi-provider latency and fallback benchmarks—it's optional, not required:
model_list: - model_name: local-baseline litellm_params: model: openai/your-baseline-model api_base: https://b-lost.com/v1 api_key: os.environ/B_LOST_API_KEY general_settings: master_key: os.environ/LITELLM_MASTER_KEY database_url: redis://:${REDIS_PASSWORD}@redis:6379/0 store_model_in_db: false router_settings: routing_strategy: least-busy num_retries: 2 timeout: 120 For strictly local deployments, replace the model entry with an internal OpenAI-compatible server and remove the B-Lost line. The critical boundary: Open WebUI talks only to LiteLLM, never directly to multiple providers. This gives you one enforcement point for model names, timeouts, retries, and request logs.
Traefik's dynamic config enforces the network boundary:
http: middlewares: tailnet-only: ipAllowList: sourceRange: - 100.64.0.0/10 - fd7a:115c:a1e0::/48 tls: certificates: - certFile: /certs/ai.tailnet.example.crt keyFile: /certs/ai.tailnet.example.key The certificate is issued for the internal hostname with permissions readable only by Traefik. Tailscale provides identity-aware network access, but the application still needs authentication. The IP allowlist is a second barrier, not a replacement for user accounts.
The AnyPS5 Integration Boundary
AnyPS5 runs outside this Compose stack. A practical integration is a host service that accepts a job identifier, validates the input directory, executes a fixed command with resource limits, and returns logs. Do not pass arbitrary shell strings from an LLM into that service. The runner should operate as an unprivileged user, use a temporary workspace, enforce CPU and memory cgroups, and write results to a dedicated output directory.
This design costs more than publishing a single container on port 3000: you need certificates, proxy policy, a runner boundary, and operational backups. But a model gateway outage doesn't expose the build machine, a malformed prompt can't automatically receive host privileges, and SQLite remains a single-writer component instead of a hidden cluster bottleneck.
The Trade-off Nobody Talks About
The hardest part isn't the infrastructure. It's deciding where to draw the trust boundary when the UI, the model, and the build orchestrator all need to coordinate but shouldn't trust each other. If you enforce the boundary too early, legitimate workflows break. If you enforce it too late, you're one bad prompt away from giving an LLM SSH access.
How do you handle that boundary in your homelab or self-hosted AI stack? Do you isolate every layer, or do you trust the model to behave? What's your 2 AM failure story?

