Be Your Own Cloud code · PART 3 — THE STACK

Chapter 16 Local AI: The Sanitiser and the Full Assistant

6 blocks · pp. 70–73 in the paperback

Going further than the chapter: The Sanitiser, Presidio, Ollama and Open WebUI: Going Deeper

Part A: The Sanitiser (Low-Hardware Tier)

# p. 70

File /opt/stacks/sanitizer/app.py
from flask import Flask, request, render_template_string
import re

app = Flask(__name__)

SECRET = r'([A-Za-z0-9_]+_(KEY|TOKEN|SECRET|PASSWORD))\s*=.*'

# Order matters: emails before domains, so an address isn't half-
# redacted.
PATTERNS = [
    (r'[\w.-]+@[\w.-]+\.\w+', 'EMAIL'),
    (r'([0-9]{1,3}\.){3}[0-9]{1,3}', 'IP'),
    (r'\b[\w.-]+\.(com|net|org|io|dev|xyz)\b', 'DOMAIN'),
]

def sanitize(text):
    text = re.sub(SECRET, r'\1=REDACTED', text)
    for pattern, label in PATTERNS:
        # real value -> placeholder, for this one paste only
        seen = {}
        def number(match):
            value = match.group(0)
            if value not in seen:
                seen[value] = 'REDACTED-%s-%d' % (label, len(seen) + 1)
            return seen[value]
        text = re.sub(pattern, number, text)
    return text

PAGE = '''
<h2>Sanitizer</h2>
<form method="post">
<textarea name="input_text" rows="12" cols="80">
{{ input_text }}</textarea><br>
<button type="submit">Sanitize</button>
</form>
<h3>Result (nothing is saved):</h3>
<textarea rows="12" cols="80" readonly>{{ output_text }}</textarea>
'''

@app.route('/', methods=['GET', 'POST'])
def index():
    input_text = ''
    output_text = ''
    if request.method == 'POST':
        input_text = request.form.get('input_text', '')
        output_text = sanitize(input_text)
    return render_template_string(PAGE, input_text=input_text,
                                  output_text=output_text)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

# p. 71

File /opt/stacks/sanitizer/Dockerfile
# Dockerfile
FROM python:3.12-slim
WORKDIR /app
RUN pip install flask
COPY app.py .
CMD ["python", "app.py"]

# p. 71

File /opt/stacks/sanitizer/docker-compose.yml
services:
  sanitizer:
    build: .
    container_name: sanitizer
    restart: unless-stopped
    networks:
      - proxy   # Chapter 8's shared network -- no host port published

networks:
  proxy:
    external: true

# p. 72

Add to /opt/stacks/sanitizer/docker-compose.yml

Paste under services:, after the sanitizer service — the indentation is already right.

# Add these to the sanitizer's docker-compose.yml, so all three share
# a network.
# The front end calls http://presidio-analyzer:3000 -- no published
# ports needed.
  presidio-analyzer:
    image: mcr.microsoft.com/presidio-analyzer:2.2.362
    restart: unless-stopped
  presidio-anonymizer:
    image: mcr.microsoft.com/presidio-anonymizer:2.2.362
    restart: unless-stopped
# Presidio is API-first -- a paste-box front end like the Flask
# example above
# still needs to sit on top of it for a comfortable interface.

Part B: The Full Assistant (Ollama + Open WebUI)

# p. 72

File /opt/stacks/ollama/docker-compose.yml
services:
  ollama:
    image: ollama/ollama:0.35.0
    container_name: ollama
    restart: unless-stopped
    volumes:
      - ./ollama-data:/root/.ollama
    ports:
      - '127.0.0.1:11434:11434'
    # Uncomment for NVIDIA GPU passthrough:
    # deploy:
    #   resources:
    #     reservations:
    #       devices:
    #         - driver: nvidia
    #           count: 1
    #           capabilities: [gpu]

  open-webui:
    image: ghcr.io/open-webui/open-webui:v0.11.4
    container_name: open-webui
    restart: unless-stopped
    depends_on:
      - ollama
    environment:
      - OLLAMA_BASE_URL=http://ollama:11434
    volumes:
      - ./webui-data:/app/backend/data
    ports:
      - '8090:8080'

# p. 73

Run Terminal, in /opt/stacks/ollama
docker compose up -d
docker exec -it ollama ollama pull qwen2.5-coder:7b
# 16GB+ VRAM: try qwen2.5-coder:14b or deepseek-coder-v2:16b instead

Reproduced exactly from Be Your Own Cloud, Chapter 16. The book explains what each line does and what to do when it doesn't work. About the book

Something here not working as the book describes? Tell the author.