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
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:
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:
- Host allowlist: Only
youtube.com,youtu.be, andyoutube-nocookie.comhostnames are permitted. - 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.
- Scheme enforcement: Only
https://andhttp://schemes are permitted (nofile://,ftp://, etc.).
See our detailed SSRF Protection deep dive article for the full implementation.
Database Schema & WAL Mode
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=productionto suppress debug stack traces - Generate a strong random
SECRET_KEYvalue - Deploy behind Nginx with TLS (Let's Encrypt / Caddy)
- Set up weekly
pip install --upgrade yt-dlpcron jobs - Mount the
/downloadsdirectory 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:
[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:
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;
}
}