📖 Developer

Self-Host a yt-dlp Web UI with Python & Flask

📅 October 8, 2026 ⏱️ 15 min read Developer

This tutorial walks through the complete architecture and implementation of a production-grade YouTube downloading web service using Python Flask, yt-dlp, FFmpeg, and SQLite. Whether you're studying the codebase behind YT Downloader or building your own private media archiving system, this guide explains every major design decision.

⚡ Direct Answer / Key Takeaway

How do you self-host a private yt-dlp web downloader? Clone the open-source repository, install dependencies in a Python virtual environment (pip install -r requirements.txt), ensure FFmpeg is in your system PATH, and start the Flask server via Gunicorn or the built-in development runner. The app utilizes SQLite WAL mode for job queue persistence and ThreadPoolExecutor for concurrent media multiplexing.

  • Stack: Python 3.10+, Flask, SQLite WAL, yt-dlp, FFmpeg.
  • Concurrency: ThreadPoolExecutor with bounded worker limits.
  • Auto Maintenance: 24-hour background file cleanup janitor.

Architecture Overview

A media downloader web service has fundamentally different performance requirements than a typical CRUD web application. Download operations are long-running (seconds to minutes), CPU-intensive (FFmpeg transcoding), and I/O bound (reading from network streams and writing to disk). The system must handle these without blocking the Flask request-response cycle.

The YT Downloader architecture uses three primary layers:

  • HTTP Layer (Flask): Lightweight request routing, JSON API, and template rendering. Immediately validates and queues requests without blocking.
  • Job Queue (ThreadPoolExecutor): Background thread pool with configurable concurrency (default: 3 workers). Long-running downloads execute entirely in worker threads.
  • State Store (SQLite WAL): Thread-safe SQLite database with Write-Ahead Logging enabled for concurrent reads during write operations. Stores job metadata, status, progress, and file paths.

Project Structure

text
yt-downloader/
├── app.py              # Flask application factory, routes, error handlers
├── downloader.py       # DownloaderService, job management, yt-dlp integration
├── blog_posts.py       # Blog content data module
├── templates/          # Jinja2 HTML templates
│   ├── base.html       # Master layout with nav, footer, JSON-LD
│   ├── index.html      # Main tool dashboard
│   └── blog/           # Blog index and post templates
├── static/
│   ├── css/
│   │   ├── style.css       # Core dark design system
│   │   └── content.css     # SEO content page typography
│   └── js/
│       └── app.js          # React + API client
├── downloads/          # Temporary media storage (gitignored)
├── requirements.txt
└── .env.example

The DownloaderService Class

The core service object encapsulates all media extraction logic. Here's a simplified view of the key responsibilities:

python
class DownloaderService:
    def __init__(self, config: dict):
        self.config = config
        self.executor = ThreadPoolExecutor(max_workers=int(config['MAX_CONCURRENT_DOWNLOADS']))
        self.db = self._init_database()
        self.ffmpeg_available = self._check_ffmpeg()

    def submit_job(self, url: str, quality: str, format_type: str) -> dict:
        # Validate, create DB record, and submit to thread pool.
        job_id = str(uuid.uuid4())
        self._db_create_job(job_id, url, quality, format_type)
        self.executor.submit(self._download_worker, job_id)
        return self._get_job(job_id)

    def _download_worker(self, job_id: str):
        # Worker thread: run yt-dlp, track progress, handle errors.
        try:
            self._db_update_status(job_id, 'downloading')
            ydl_opts = self._build_ydl_opts(job_id)
            with yt_dlp.YoutubeDL(ydl_opts) as ydl:
                ydl.download([url])
            self._db_update_status(job_id, 'completed')
        except Exception as e:
            self._db_update_status(job_id, 'failed', error=str(e))

URL Validation & SSRF Protection

Media downloader services are prime targets for Server-Side Request Forgery (SSRF) attacks. Without robust URL validation, an attacker could submit internal IP addresses (e.g., http://169.254.169.254/latest/meta-data/ on AWS) and use the downloader as a proxy to exfiltrate cloud metadata credentials.

YT Downloader implements multi-layer SSRF protection:

  1. Host allowlist: Only youtube.com, youtu.be, and youtube-nocookie.com hostnames are permitted.
  2. IP range blocking: After DNS resolution, private IP ranges (10.x.x.x, 172.16-31.x.x, 192.168.x.x, 127.x.x.x, ::1) are rejected.
  3. Scheme enforcement: Only https:// and http:// schemes are permitted (no file://, ftp://, etc.).

See our detailed SSRF Protection deep dive article for the full implementation.

Database Schema & WAL Mode

sql
CREATE TABLE IF NOT EXISTS jobs (
    id TEXT PRIMARY KEY,
    url TEXT NOT NULL,
    status TEXT NOT NULL DEFAULT 'queued',
    quality TEXT,
    format_type TEXT,
    title TEXT,
    thumbnail TEXT,
    progress REAL DEFAULT 0,
    file_path TEXT,
    error_message TEXT,
    retry_count INTEGER DEFAULT 0,
    created_at TEXT,
    updated_at TEXT
);

PRAGMA journal_mode=WAL;
PRAGMA synchronous=NORMAL;

Production Deployment Checklist

  • Set FLASK_ENV=production to suppress debug stack traces
  • Generate a strong random SECRET_KEY value
  • Deploy behind Nginx with TLS (Let's Encrypt / Caddy)
  • Set up weekly pip install --upgrade yt-dlp cron jobs
  • Mount the /downloads directory on fast local SSD storage
  • Configure log rotation for Flask and Gunicorn access logs
  • Add HTTP Basic Auth or Tailscale VPN if deploying for personal use only

Production Deployment: Systemd Service & Nginx Reverse Proxy

To run your self-hosted Flask media downloader as a resilient Linux background service that automatically restarts after system reboots, configure a systemd service unit:

ini
[Unit]
Description=YT Downloader Flask Application
After=network.target

[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/yt-downloader
Environment="PATH=/var/www/yt-downloader/.venv/bin:/usr/bin"
ExecStart=/var/www/yt-downloader/.venv/bin/gunicorn --workers 3 --bind 127.0.0.1:5000 app:app
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Nginx Reverse Proxy with Large Request Support

Pair your Gunicorn backend with an Nginx reverse proxy configured to handle media buffering and large download payloads:

nginx
server {
    listen 80;
    server_name yourdomain.com;

    client_max_body_size 64M;

    location / {
        proxy_pass http://127.0.0.1:5000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
Browse All Articles
← Back to Blog
Start Downloading
Use the Downloader →

Related Articles