# TubeTube: a self-hosted YouTube downloader with a web UI on yt-dlp

> TubeTube is a small Python web application that puts a browser interface in front of yt-dlp, with multithreaded downloads, YAML-driven per-folder format settings, subtitle handling and a container image you can run with docker compose. The hard part of using it is not the code, it is keeping the yt-dlp version in the image current.

**MattBlackOnly/TubeTube** — YouTube Downloader using yt-dlp

- Repository: https://github.com/MattBlackOnly/TubeTube
- Stars: 678 · Forks: 46
- Language: Python
- License: GPL-3.0
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/mattblackonly-tubetube

## A web front end wrapped around yt-dlp

TubeTube is described as a simple YouTube downloader, and the features list says what that means in practice: multithreaded downloads for fast simultaneous transfers, custom locations and formats driven by YAML settings, a mobile-optimised interface for small screens, a choice between audio and video downloads, and support for live video, including multiple live streams. The implementation, visible from the repository layout and the requirements file, is a Flask application with flask_socketio for the live updates, served by gunicorn and gevent, with gevent-websocket alongside. The download engine is yt-dlp, pinned to a specific dated release, and pyyaml handles the settings file. The directory tree holds a tubetube package with static assets, a start.sh entrypoint, a start_config.py helper and separate requirements files for the base and default dependency sets. The audience is a person who wants a graphical way to queue and watch downloads rather than a command line, running on hardware they already own.

## Folder names in settings.yaml have to match the compose volumes

The configuration model is unusual enough to be worth spelling out, because it is where most of the setup friction comes from. You create a settings.yaml file in the config directory, and its top-level keys are your library folder names, each holding the formats to use for that folder. The example shows four of them. General keeps m4a audio with format id 140 and mp4 video with format id 625, Music switches the audio extension to mp3, Podcast keeps m4a, and Video has no audio settings at all:

```yaml
General:
  audio_ext: m4a
  audio_format_id: '140'
  video_ext: mp4
  video_format_id: '625'
Music:
  audio_ext: mp3
  audio_format_id: '140'
Podcast:
  audio_ext: m4a
  audio_format_id: '140'
Video:
  audio_format_id: '140'
  video_ext: mp4
  video_format_id: '625'
```

The rules that follow are the important part. The volume paths in docker-compose.yml have to match the names in settings.yaml, in other words the General folder has to land at /data/General. You can create as many directory locations as you like in settings.yaml, but each one has to be mapped individually in the compose file. The paths you write there are host paths, and you have to replace the placeholder values with real ones on your machine. The format identifiers are yt-dlp format ids, so choosing a different video quality means looking up the id you want.

## Environment variables choose codecs, subtitles and thread count

The compose file passes a long list of environment variables, and they are the second half of the configuration. The core group is the container's user and group, PUID and PGID, both defaulting to 1000, which matches the appuser and appgroup created inside the image. Then there is VERBOSE_LOGS to pass verbose logging through to yt-dlp, TRIM_METADATA to trim metadata from the saved files, and a set of preference variables for the output: PREFERRED_LANGUAGE defaults to en, PREFERRED_AUDIO_CODEC to aac, PREFERRED_VIDEO_CODEC to vp9 and PREFERRED_VIDEO_EXT to mp4. The subtitle group is EMBED_SUBS and WRITE_SUBS, both false by default, ALLOW_AUTO_SUBS, which the table marks as defaulting to true, SUBTITLE_FORMAT as vtt and SUBTITLE_LANGUAGES as en. THREAD_COUNT controls how many downloads run at once and defaults to 4. The comments in the file are worth reading, because a couple of the stated defaults do not match what the table says, and the value in the compose file is what actually takes effect.

## Subtitles fall back to auto-generated only when you allow it

The subtitle behaviour is defined by three switches that interact, and the README is precise about the combinations. With WRITE_SUBS set, actual subtitles are saved to a separate subtitle file, and if no actual subtitles are available, no subtitle file is created. With EMBED_SUBS set, actual subtitles are embedded into the video, and again nothing is included if no actual subtitles are present. ALLOW_AUTO_SUBS provides the fallback in both cases, writing or embedding automatically generated subtitles when the real ones are missing. The guidance is to enable ALLOW_AUTO_SUBS together with either WRITE_SUBS or EMBED_SUBS, since that combination first tries to download actual subtitles and falls back to the generated ones. If you enable only one of the two output switches and leave ALLOW_AUTO_SUBS off, the result for a video without human subtitles is a file with no subtitle track at all.

## The container runs as a non-root user and exposes 6543

The Dockerfile is short and shows the runtime contract. It starts from python:3.12-alpine and installs ffmpeg, su-exec and deno with apk, which matters because ffmpeg is what actually muxes the audio and video tracks that yt-dlp downloads separately. It then creates an appgroup and an appuser with the id 1000, sets the Python environment variables including PYTHONPATH pointing at the tubetube package, copies the code, installs the requirements, and creates /config, /data and /temp with the right ownership and 775 permissions on the temp directory. The script start.sh is made executable and used as the entrypoint, and port 6543 is exposed. The presence of deno in the image is worth a second look, since it is not a Python dependency and the README does not explain its role, so anyone building a customised image should find out what it is there for before removing it.

## Cookies for authenticated downloads, and a pinned yt-dlp to keep current

Two operational details decide whether the tool keeps working over time. The first is the cookies file. To use one, you create a cookies.txt file and place it in the config directory, which is how you reach content that requires a signed-in session. The second is the yt-dlp version. The requirements file pins yt_dlp[default]==2026.08.19, so the image ships with a specific dated build, and the README explains the auto-update mechanism in terms of YTDLP_UPDATE and YTDLP_UPDATE_HOUR. When YTDLP_UPDATE is set to latest or nightly, yt-dlp is upgraded automatically once per day at the specified hour, and the hour accepts a value from 0 to 23, with update logs written to ytdlp-update.log in the config directory. It is unset by default, meaning the version pinned in the image is the one that runs. That default is safe and reproducible, and it is also the one that will eventually fall behind, since YouTube changes its formats regularly and a stale yt-dlp is the most common reason a downloader like this stops working.

## Conclusion

TubeTube fits someone who wants a browser front end for yt-dlp on a home server or NAS, with several library folders on different volumes and a choice of audio or video formats per folder. It does not fit a setup where you only need to run yt-dlp from a shell, since the web application adds a service to maintain. Before deploying it, read the notes on where the yt-dlp version is pinned and how YTDLP_UPDATE works, map every folder in settings.yaml to a volume in docker-compose.yml, and decide in advance how you will handle the terms of the service you are downloading from.

## FAQ

### How do I install TubeTube?

It is distributed as a container image, ghcr.io/mattblackonly/tubetube:latest. The README gives a docker-compose.yml that maps your host folders into /data/General, /data/Music, /data/Podcast and /data/Video, mounts a config directory and a temp directory, and publishes port 6543.

### How do the folders in settings.yaml relate to docker-compose.yml?

The volume paths in docker-compose.yml must match the names used in settings.yaml, for example General has to land at /data/General. You can define as many locations as you want in settings.yaml, but each one has to be mapped individually in the compose file.

### How do I get subtitles with TubeTube?

Set WRITE_SUBS to save subtitles as a separate file or EMBED_SUBS to embed them in the video, and set ALLOW_AUTO_SUBS to fall back to automatically generated subtitles when a video has no actual subtitle track. With ALLOW_AUTO_SUBS off, a video without real subtitles gets no subtitle output at all.

### Does TubeTube update yt-dlp automatically?

Not by default. The requirements file pins yt_dlp[default]==2026.08.19 and YTDLP_UPDATE is unset, so the image uses that pinned version. Setting YTDLP_UPDATE to latest or nightly makes the container upgrade yt-dlp once a day at the hour given by YTDLP_UPDATE_HOUR, with logs in ytdlp-update.log in the config directory.

## Sources

- [Issues](https://github.com/MattBlackOnly/TubeTube/issues)
- [License: GPL-3.0](https://github.com/MattBlackOnly/TubeTube/blob/main/LICENSE)
- [MattBlackOnly/TubeTube on GitHub](https://github.com/MattBlackOnly/TubeTube)
- [README](https://github.com/MattBlackOnly/TubeTube/blob/main/README.md)
- [Releases](https://github.com/MattBlackOnly/TubeTube/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/mattblackonly-tubetube
