🎉 We've raised a $5M seed led by AIX Ventures!

Read more

Serve notebooks from GitHub on the fly with marimo

Serve notebooks from GitHub on the fly with marimo

If you’re new to marimo, check out our GitHub repo: marimo is free and open source.

Transform your repository of notebooks into a collection of interactive data apps. In this blog, we’ll explore how to serve your collection of Python notebooks as data apps directly from a GitHub repository using marimo. This is a great demonstration of a few of marimo’s unique features:

  • File format: marimo notebooks are just pure Python files (.py), ensuring compatibility with modern Python tools like uv, docker, and version control systems. No need for dealing with code buried inside JSON.
  • ASGI-compatibility: marimo’s server can mount notebooks as web apps on any ASGI server, making it easy to deploy data apps.
  • Self-contained: No need for complex configuration files or environment setup - everything is contained in a single Python file.

How It Works

We’ll use marimo’s ASGI server to serve notebooks from a GitHub repository. The server reads notebooks and creates individual marimo apps for each one, all while keeping the code simple and maintainable.

Step 1: Import the Essentials

First, import the necessary libraries:

# /// script
# requires-python = ">=3.12"
# dependencies = ["fastapi", "marimo", "starlette", "requests", "pydantic", "jinja2"]
# ///
import os
import tempfile
from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
import marimo
import requests
from pathlib import Path

We’re using PEP 723 to declare our dependencies inline, instead of a separate requirements.txt. Doing so allows us to run this script directly with uv without needing a separate requirements file. This is also what powers marimo’s sandboxed notebooks.

Step 2: Download Notebooks from GitHub

Next, download the Python files from the GitHub repository:

GITHUB_REPO = os.environ.get("GITHUB_REPO", "marimo-team/marimo")
ROOT_DIR = os.environ.get("GITHUB_ROOT_DIR", "examples/ui")
 
def download_github_files(repo: str, path: str = "") -> list[tuple[str, str]]:
    """Download files from GitHub repo, returns list of (file_path, content)"""
    api_url = f"https://api.github.com/repos/{repo}/contents/{path}"
    response = requests.get(api_url)
    response.raise_for_status()
 
    files: list[tuple[str, str]] = []
    for item in response.json():
        if item["type"] == "file" and item["name"].endswith(".py"):
            content_response = requests.get(item["download_url"])
            files.append((Path(path) / item["name"], content_response.text))
        elif item["type"] == "dir":
            files.extend(download_github_files(repo, str(Path(path) / item["name"])))
    return files
 
files = download_github_files(GITHUB_REPO, ROOT_DIR)

This function recursively retrieves all Python files from the specified GitHub repository. Since marimo notebooks are just Python files, we don’t need any special conversion or post-processing.

Step 3: Create a marimo Server

Create a marimo server with each notebook as an app under their respective paths:

server = marimo.create_asgi_app()
tmp_dir = tempfile.TemporaryDirectory()
app_names: list[str] = []
 
for file_path, content in files:
    app_name = Path(file_path).stem
    local_path = Path(tmp_dir.name) / file_path
 
    # Create directories if they don't exist
    local_path.parent.mkdir(parents=True, exist_ok=True)
 
    # Write file content
    local_path.write_text(content)
 
    # Add to marimo server
    print(f"Adding app: {app_name}")
    server = server.with_app(path=f"/{app_name}", root=str(local_path))
    app_names.append(app_name)

marimo’s ASGI server makes it easy to serve multiple notebooks under different paths. Each notebook becomes its own interactive data app, complete with UI elements and reactivity.

Step 4: Set Up the FastAPI App

Set up a FastAPI app to serve as the entry point:

app = FastAPI()
templates = Jinja2Templates(directory=Path(__file__).parent / "templates")
 
@app.get("/")
async def home(request: Request):
    return templates.TemplateResponse(
        "home.html", {"request": request, "app_names": app_names}
    )
 
# Mount the marimo server
app.mount("/", server.build())

And finally, this block runs the server:

if __name__ == "__main__":
    import uvicorn
 
    uvicorn.run(app, host="0.0.0.0", port=7860, log_level="info")

Our template directory has a single file: templates/home.html, which lists all the apps with a clean UI. You can customize this to your liking. Or even inline it in the script, giving you a single file to run and share.

<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Home</title>
    <script src="https://cdn.tailwindcss.com"></script>
  </head>
  <body class="bg-gray-100">
    <div class="container mx-auto px-4 py-8">
      <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
        {% for app_name in app_names %}
        <a href="/{{ app_name }}" class="block">
          <div class="bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow duration-300 p-4">
            <h3 class="text-lg font-semibold text-blue-600 hover:text-blue-800">{{ app_name }}</h3>
          </div>
        </a>
        {% endfor %}
      </div>
    </div>
  </body>
</html>

Now, it can be easily run with:

uv run --no-project main.py

Bonus: Deploy with Docker

For deployment, you can package your application with Docker:

FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim
 
# Change these to the repo and path you want to serve
ENV GITHUB_REPO=marimo-team/marimo
ENV GITHUB_ROOT_DIR=examples/ui
 
COPY main.py .
COPY templates/ templates/
 
CMD ["uv", "run", "--no-project", "main.py"]

You can find an example of this setup in our HuggingFace Space.

Conclusion

This guide demonstrates how to build a single-file Python application that serves marimo notebooks from a GitHub repository. Should you use this in production? Probably not. However, this is pretty close; some considerations to make this production-ready:

  1. Downloading the GitHub files in a build process, not on the fly.
  2. Installing the dependencies in the Docker build, not the Docker command (e.g. uv run ...).
  3. Implementing authentication (marimo has some built-in support).

We’d love to hear what fun ways are you using marimo’s unique features, so please share them with us on Discord!