cd /news/ai-infrastructure/building-a-private-ai-control-plane-… · home › topics › ai-infrastructure › article
[ARTICLE · art-166098] src=dev.to ↗ pub= topic=ai-infrastructure verified=true sentiment=↑ positive

Building a Private AI Control Plane for PS5 Porting: What We Learned After SQLite Locked Us Out at 2 AM

An external systems engineer building PS5 executable porting automation on Linux and Windows redesigned a private AI control plane after a 2 AM incident in which concurrent uploads corrupted Open WebUI's SQLite database and an earlier misconfigured reverse proxy exposed an admin panel to the internet. The new Docker Compose stack separates concerns across Redis, LiteLLM, Open WebUI and Traefik, confines services to a private network, and treats model-generated commands as untrusted input that the runner should reject rather than execute.

by read4 min views3 publishedNov 9, 2026

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 up 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 production design separates concerns explicitly:

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.

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 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?

── more in #ai-infrastructure 4 stories · sorted by recency
── more on @open webui 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/building-a-private-a…] indexed:0 read:4min 2026-11-09 · —