Skip to content

Start typing. Errors match on their exact yt-dlp output.

to move, to open esc to close

Run yt-dlp in Docker (Compose, Volumes and Permissions)

Tested on yt-dlp 2026.07.28 Updated

Quick answer

A container keeps yt-dlp, ffmpeg and Python off your host and makes updating a rebuild. There is no official image, and a six-line Dockerfile is better than trusting a third party's update schedule for a tool whose main failure mode is being out of date:

Dockerfile
FROM python:3.13-alpine

RUN apk add --no-cache ffmpeg ca-certificates \
 && pip install --no-cache-dir -U "yt-dlp[default]"

WORKDIR /downloads
ENTRYPOINT ["yt-dlp"]
build and run
docker build -t yt-dlp .
docker run --rm -v "$PWD:/downloads" --user "$(id -u):$(id -g)" yt-dlp "URL"

The two flags that matter

-v "$PWD:/downloads" is what makes files appear on your machine rather than vanishing with the container. Without it the download succeeds and the result is deleted when the container exits — a confusing five minutes for everyone the first time.

--user "$(id -u):$(id -g)" is what stops every downloaded file being owned by root. Skip it and you will be running chown afterwards forever.

Compose, for something scheduled

compose.yaml
services:
  yt-dlp:
    build: .
    user: "1000:1000"
    volumes:
      - ./downloads:/downloads
      - ./config:/config:ro
      - ./cookies.txt:/config/cookies.txt:ro
    environment:
      - TZ=Europe/London
    command: >
      --config-location /config/yt-dlp.conf
      --download-archive /downloads/archive.txt
      -i
      "CHANNEL_URL"

Three things worth noticing in that file. The archive lives in the mounted volume, so it survives the container and a rerun continues rather than repeating. The config is mounted read-only, so a mistake inside the container cannot rewrite it. And the cookies file is mounted as a single file rather than a directory, which keeps it out of anything else's reach.

Cookies

There is no browser inside the container and no desktop keyring, so --cookies-from-browser cannot work. Export on the host, mount the file:

bash
chmod 600 cookies.txt
docker run --rm \
  -v "$PWD:/downloads" \
  -v "$PWD/cookies.txt:/cookies.txt:ro" \
  --user "$(id -u):$(id -g)" \
  yt-dlp --cookies /cookies.txt "URL"

Read-only matters: yt-dlp does not need to write to it, and a live session in a container is a credential you want as little exposed as possible. The cookies guide covers exporting one.

Staleness is the real cost

A container is a frozen filesystem, and freezing yt-dlp is exactly the wrong thing to do to it. An image built two months ago has a two-month-old yt-dlp inside, with all the extractor breakage that implies.

Rebuild without cache, on a schedule:

bash
docker build --no-cache -t yt-dlp .

Or update at container start, which trades a few seconds per run for never thinking about it:

entrypoint.sh
#!/bin/sh
pip install --no-cache-dir -U --pre "yt-dlp[default]" >/dev/null 2>&1 || true
exec yt-dlp "$@"

|| true so a network hiccup during the update does not abort the download that was the point of the run.

A nightly archive job

crontab -e
0 3 * * * cd /srv/ytdlp && docker compose run --rm yt-dlp >> /var/log/ytdlp.log 2>&1

--rm so containers do not accumulate, and a log file because a job that fails silently at 3am is one you discover in a month. The playlist guide covers the archive-file pattern that makes this safe to rerun.

Alpine or Debian

Alpine is around 120 MB with ffmpeg; Debian slim is closer to 400 MB. Alpine's ffmpeg is occasionally missing an encoder that a niche postprocessing option wants. If you hit ffmpeg exited with code 1 on a container that works fine on the host, switch the base image before debugging further:

Debian-based alternative
FROM python:3.13-slim

RUN apt-get update \
 && apt-get install -y --no-install-recommends ffmpeg ca-certificates \
 && rm -rf /var/lib/apt/lists/* \
 && pip install --no-cache-dir -U "yt-dlp[default]"

WORKDIR /downloads
ENTRYPOINT ["yt-dlp"]

If it still fails

  • Files owned by root. --user was omitted. Fix the existing ones with sudo chown -R "$(id -u):$(id -g)" downloads.
  • Certificate errors on everything. ca-certificates missing from the image.
  • Downloads vanish. No volume mount, or the working directory inside the container is not the mounted one.
  • Rate limited immediately. If this is a VPS, the address range is the reason — HTTP 429. Pace the job.
  • Works on the host, not in the container. Rebuild without cache. It is almost always a stale layer holding an old yt-dlp.

Frequently asked

Why are my downloads owned by root?
Because the container process runs as root by default and the bind-mounted directory keeps whatever UID wrote to it. Passing --user "$(id -u):$(id -g)" makes the container write as you.
Is there an official yt-dlp image?
No. The project ships binaries, not containers. Third-party images exist and vary in how current they are kept; a six-line Dockerfile you rebuild yourself avoids trusting someone else's update schedule for a tool where staleness is the main failure mode.
Can the container read my browser cookies?
No — there is no browser and no keyring inside it. Export a cookies.txt on the host and mount it read-only.
Do I need to rebuild often?
Yes, and that is the one real cost of containerising this. A container built two months ago contains a yt-dlp from two months ago, with all the extractor breakage that implies. Rebuild weekly, or reinstall yt-dlp at container start.