While working with Open WebUI, I noticed a significant gap: ChatGPT can natively generate formatted documents, but most open-source models cannot. This limitation became particularly frustrating when I needed to produce professional deliverables like reports, spreadsheets, and presentations directly from AI responses.
CD ProJect (Create Documents from Python and Jupyter) solves this by giving all models in Open WebUI the ability to generate production-ready files. Instead of being limited to text output, your models can now create .docx, .pdf, .xlsx, .pptx, csv, markdown and other formats through secure Python execution.
The implementation is straightforward but carefully designed:
- Jupyter runs in an isolated Docker container (no host system exposure)
- Document generation happens through standard Python libraries
- Files are delivered via a dedicated webserver with security measures
- All components integrate cleanly with Open WebUI
I built this specifically because I was tired of:
- Copying AI responses into Word/Excel manually
- Struggling with formatting issues
- Wishing my local models had ChatGPT's document capabilities
The setup requires moderate steps to get up and running (detailed in the documentation), but the trade-off is worth it: your models gain professional document creation abilities while maintaining enterprise security standards. If you've ever wished your local models could do what ChatGPT does with documents, this is for you.
It is better to use the provided tool's system prompt to constrain the tool's behavior, define its persona, specify input/output formats, and set explicit boundaries for its operation, ensuring predictable and reliable function calls and preventing unwanted actions or failures.
Here's how everything fits together step by step:
-
The model generates Python code that creates files (e.g.,
docx,pdf,xlsx,pptx,csv,txt,rtf,odt,ods,odp,html,xml, etc...). -
The code is wrapped and sent to the Jupyter server running inside Docker (default port:
8888).
-
Access Control
- The Jupyter server is not publicly exposed (Local).
- Only the tool can send code to it.
- This means even if a remote user interacts with OpenWebUI, the Jupyter server remains isolated and secure.
-
File Creation
- The Jupyter server executes the Python code.
- Files are created inside Docker and stored in a volume mapped to a host directory (e.g.,
jupyter_data/).
-
File URL Generation
-
Once created, the tool provides a generated download link.
-
Example:
https://openwebui.binarycells.com/backend-api/files/download?user_id=ce7aa5b1-0083-433e-acff-d2e5b3bda778&chat_id=7169d05f-2166-4b88-b176-ed9c5657c439&file_name=_EMHWczQxJzGp-EPdjoWJg.docx
-
-
Cloudflared inspects requests for specific paths:
path: /backend-api/files/*
-
Matching requests are redirected to the local file-serving webserver.
-
The webserver parses query parameters from the request URL:
?user_id=ce7aa5b1-0083-433e-acff-d2e5b3bda778&chat_id=7169d05f-2166-4b88-b176-ed9c5657c439&file_name=_EMHWczQxJzGp-EPdjoWJg.docx -
Extracted key-value pairs:
user_id→ce7aa5b1-0083-433e-acff-d2e5b3bda778chat_id→7169d05f-2166-4b88-b176-ed9c5657c439file_name→_EMHWczQxJzGp-EPdjoWJg.docx
-
The server uses this information to locate the file in the host and serve it securely.
Running Jupyter directly on the host is dangerous - it can execute arbitrary commands and modify files anywhere.
Solution: run Jupyter in a Docker container with a mapped directory.
- Safe: Jupyter can only read/write inside the mapped folder (
jupyter_data). - Isolated: Host system files are never exposed.
Create a file named Dockerfile (no extension) with:
FROM jupyter/base-notebook:latest
# Install Pandoc (required for document conversions)
RUN apt-get update && apt-get install -y pandoc
# Install additional Python libraries for file generation
RUN pip install \
python-docx \
python-pptx \
openpyxl \
reportlab \
fpdf2 \
pdfplumber \
pandas \
pillowRun:
docker build -t IMG_NAME .IMG_NAME→ a name for your Docker image (e.g.,jupyter-file-creator).- This command only needs to be run once unless you update the Dockerfile.
mkdir -p YOUR_HOST_DIRECTORY- This folder will be mounted into the container at
/mnt/data. - All files created in Jupyter notebooks will appear here.
- Files outside this folder remain protected.
# Optional: create a custom Docker network
docker network create NETWORK_NAME
# Run Jupyter inside Docker
docker run -it --rm --name CONTAINER_NAME --network NETWORK_NAME -p HOST_PORT:DOCKER_PORT -v YOUR_HOST_DIRECTORY:/mnt/data IMG_NAME jupyter lab --ip=0.0.0.0 --port=DOCKER_PORT --IdentityProvider.token=YOUR_TOKEN --no-browserNETWORK_NAME→ your custom Docker network name (e.g.,jupyter-network).
Omit--network NETWORK_NAMEif you skipped network creation.CONTAINER_NAME→ a name for the container (e.g.,jupyter-lab-container).HOST_PORT→ port on your machine (e.g.,8888).DOCKER_PORT→ port inside the container (usually8888).YOUR_HOST_DIRECTORY→ full path to the folder created in Prepare a Host Folder for Data.IMG_NAME→ the Docker image built in Build the Docker Image.YOUR_TOKEN→ a secure token generated by the script below.
Must match the token configured in the tool.
import hashlib
import base64
def generate_token_from_password(password):
"""
Generate a token from a password
Same password will generate the same token with 100 iterations
"""
_password = password.encode("utf-8")
for _ in range(100):
key = hashlib.sha256(_password).digest()
_password = base64.urlsafe_b64encode(key)
token = base64.urlsafe_b64encode(key).decode("utf-8")
return token
if __name__ == "__main__":
password = input("Enter a password: ")
token = generate_token_from_password(password)
print(f"\nYour generated token: {token}")
print("\nUse this as: --IdentityProvider.token=YOUR_TOKEN\n")👍 Now, when the tool requests file creation:
- Code runs in the isolated Jupyter container.
- Files are getting stored in
YOUR_HOST_DIRECTORY. - Now moving to → Cloudflare + webserver handle secure delivery of files.
We'll use FastAPI to serve user-generated files.
pip install fastapi uvicornYou can also install additional packages if needed for your environment.
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import FileResponse
import uvicorn
import os
# === PLACEHOLDERS START ===
# Full path to the directory created in [Prepare a Host Folder for Data]
DIRECTORY = "PATH_TO_HOST_DIRECTORY"
# Port for the webserver (e.g., 8081)
PORT = PORT_NUMBER
# === PLACEHOLDERS END ===
# Create directory if it doesn't exist
if not os.path.exists(DIRECTORY):
os.makedirs(DIRECTORY)
print(f"Serving files from: {DIRECTORY}")
print(f"Running on port: {PORT}\n")
app = FastAPI(
title="Files API",
description="API for serving user-generated files",
version="0.1.0"
)
# Root endpoint
@app.get("/")
async def index():
return {"message": "Welcome to the Files API."}
# Serve user-generated files
@app.get("/backend-api/files/download")
async def serve(request: Request):
try:
params = request.query_params
user_id = params.get("user_id")
chat_id = params.get("chat_id")
file_name = params.get("file_name")
print(f"Full URL: {request.url}")
print(f"Query Params: {params}")
if not user_id or not chat_id or not file_name:
raise HTTPException(detail="Missing user_id, chat_id, or file_name parameter.", status_code=400)
file_path = os.path.join(DIRECTORY, user_id, chat_id, file_name)
print(f"Resolved file path: {file_path}")
if not os.path.exists(file_path):
raise HTTPException(detail="File not found.", status_code=404)
return FileResponse(
path=file_path,
filename=file_name,
media_type="application/octet-stream"
)
except Exception as e:
raise HTTPException(detail=str(e), status_code=500)
if __name__ == "__main__":
# Replace 'user_files_webserver' with your actual Python filename (without .py)
uvicorn.run(
"user_files_webserver:app",
host="0.0.0.0",
port=PORT,
reload=False # Set to True only for development
)Placeholders explained:
PORT_NUMBER→ port to run your file server (e.g., 8081).PATH_TO_HOST_DIRECTORY→ full path to the directory created in Prepare a Host Folder for Data (e.g.,/opt/openwebui/jupyter_data).
If you don't have a domain or don't plan to use Cloudflare tunnels to serve files externally, skip to Serving Files (Local).
This section explains how to securely expose your local file server and OpenWebUI instance over the internet using Cloudflared tunnels.
If you already have a tunnel, skip this step. Otherwise:
cloudflared tunnel login
cloudflared tunnel create TUNNEL_NAMETUNNEL_NAME→ name for your tunnel (e.g.,my-secure-tunnel).
This creates a credentials.json file:
- Linux/macOS:
~/.cloudflared/ - Windows:
C:\Users\<YourUser>\.cloudflared\
Create docker-compose.yml:
version: "3.8"
services:
cloudflared:
image: cloudflare/cloudflared:latest
container_name: CONTAINER_NAME
restart: unless-stopped
command: tunnel run
volumes:
- ./config.yml:/etc/cloudflared/config.yml
- PATH_TO_CREDENTIAL_FILE:/etc/cloudflared/tunnel_credentials.json
extra_hosts:
- "host.docker.internal:host-gateway"Placeholders explained:
CONTAINER_NAME→ Docker container name (e.g.,cloudflared-tunnel).PATH_TO_CREDENTIAL_FILE→ absolute path tocredentials.json(e.g.,/home/user/.cloudflared/credentials.json).
Create config.yml:
tunnel: TUNNEL_NAME
credentials-file: /etc/cloudflared/tunnel_credentials.json
ingress:
# Files webserver (local port 8081)
- hostname: YOUR_DOMAIN
service: http://host.docker.internal:WEBSERVER_PORT
path: /backend-api/files/*
# OpenWebUI service (local port 3000)
- hostname: YOUR_DOMAIN
service: http://host.docker.internal:OPEN_WEBUI_PORT
path: /
# Default: return 404 if no match
- service: http_status:404Placeholders explained:
TUNNEL_NAME→ must match the tunnel name from Setting Up Cloudflared Tunnels.YOUR_DOMAIN→ your domain/subdomain (e.g.,llm.binarycells.com).WEBSERVER_PORT→ port of your local file server (e.g., 8081).OPEN_WEBUI_PORT→ port of your local OpenWebUI instance (e.g., 3000).
Start services:
docker compose up --build -dRecreate containers:
docker compose up --force-recreate -dRestart the tunnel:
docker compose down
docker compose up -d✅ Now your file server and OpenWebUI are securely exposed via Cloudflare with proper path routing.
In the tool set the BASE_DOWNLOAD_URL to http://localhost:PORT_OF_THE_WEB_SERVER/backend-api/files/download, where PORT_OF_THE_WEB_SERVER → the port configured in Setting Up the Webserver (e.g., 8081).
| Issue | Solution |
|---|---|
If the model is unable to recognize the tool, please set the Function Calling parameter to Native in OpenWebUI. |
![]() |


