# MeTube reads at tag 2026.09.29 (github.com/alexta69/metube, commit a3896604540223ecbb8258b41b1d745d62647d16), 2026-10-02T06:53:24Z

## 1.7 the entrypoint — docker-entrypoint.sh (whole file)
#!/bin/sh

PUID="${UID:-$PUID}"
PGID="${GID:-$PGID}"
AUDIO_DOWNLOAD_DIR="${AUDIO_DOWNLOAD_DIR:-$DOWNLOAD_DIR}"

echo "Setting umask to ${UMASK}"
umask ${UMASK}
echo "Creating download directory (${DOWNLOAD_DIR}), audio download directory (${AUDIO_DOWNLOAD_DIR}), state directory (${STATE_DIR}), and temp dir (${TEMP_DIR})"
mkdir -p "${DOWNLOAD_DIR}" "${AUDIO_DOWNLOAD_DIR}" "${STATE_DIR}" "${TEMP_DIR}"

do_upgrade() {
    echo "Upgrading yt-dlp to nightly channel..."
    if ! python3 -m pip --version >/dev/null 2>&1; then
        echo "pip not found; attempting ensurepip"
        python3 -m ensurepip --upgrade >/dev/null 2>&1 || true
    fi
    if ! python3 -m pip install -U --pre "yt-dlp[default,curl-cffi,deno]"; then
        echo "Warning: yt-dlp nightly upgrade failed; continuing with existing installation"
        return 1
    fi
    echo "yt-dlp nightly upgrade complete"
    return 0
}

run_supervised() {
    while true; do
        "$@" &
        child_pid=$!
        trap 'kill -TERM "$child_pid" 2>/dev/null; wait "$child_pid" 2>/dev/null' TERM INT
        wait "$child_pid"
        exit_code=$?
        trap - TERM INT
        if [ "$exit_code" -eq 42 ]; then
            echo "MeTube requested yt-dlp update restart (exit 42)"
            do_upgrade || true
            continue
        fi
        return "$exit_code"
    done
}

nightly_enabled() {
    [ -n "${YTDL_NIGHTLY_UPDATE_TIME}" ]
}

disable_nightly_for_non_root() {
    if nightly_enabled; then
        echo "YTDL_NIGHTLY_UPDATE_TIME is set but this container runs as a non-root user; nightly yt-dlp updates are not supported. Ignoring YTDL_NIGHTLY_UPDATE_TIME."
        unset YTDL_NIGHTLY_UPDATE_TIME
    fi
}

if [ `id -u` -eq 0 ] && [ `id -g` -eq 0 ]; then
    if [ "${PUID}" -eq 0 ]; then
        echo "Warning: it is not recommended to run as root user, please check your setting of the PUID/PGID (or legacy UID/GID) environment variables"
    fi
    if [ "${CHOWN_DIRS:-true}" != "false" ]; then
        echo "Changing ownership of download and state directories to ${PUID}:${PGID}"
        chown -R "${PUID}":"${PGID}" /app "${DOWNLOAD_DIR}" "${AUDIO_DOWNLOAD_DIR}" "${STATE_DIR}" "${TEMP_DIR}"
    fi
    if nightly_enabled; then
        echo "YTDL_NIGHTLY_UPDATE_TIME is set to ${YTDL_NIGHTLY_UPDATE_TIME}; upgrading yt-dlp on startup"
        do_upgrade || true
    fi
    echo "Starting BgUtils POT Provider"
    gosu "${PUID}":"${PGID}" bgutil-pot server >/tmp/bgutil-pot.log 2>&1 &
    echo "Running MeTube as user ${PUID}:${PGID}"
    run_supervised gosu "${PUID}":"${PGID}" python3 app/main.py
    exit $?
else
    echo "User set by docker; running MeTube as `id -u`:`id -g`"
    disable_nightly_for_non_root
    echo "Starting BgUtils POT Provider"
    bgutil-pot server >/tmp/bgutil-pot.log 2>&1 &
    run_supervised python3 app/main.py
    exit $?
fi

## 1.7 Dockerfile ENTRYPOINT/CMD/ENV
75:ENV PUID=1000
76:ENV PGID=1000
77:ENV UMASK=022
79:ENV DOWNLOAD_DIR=/downloads
80:ENV STATE_DIR=/downloads/.metube
81:ENV TEMP_DIR=/downloads
82:ENV PORT=8081
84:EXPOSE 8081
90:ENV METUBE_VERSION=$VERSION
92:ENTRYPOINT ["/usr/bin/tini", "-g", "--", "./docker-entrypoint.sh"]

## 1.6 every setting the app reads (app/main.py Config._DEFAULTS) — no key, secret or password among them
    _DEFAULTS = {
        'DOWNLOAD_DIR': '.',
        'AUDIO_DOWNLOAD_DIR': '%%DOWNLOAD_DIR',
        'TEMP_DIR': '%%DOWNLOAD_DIR',
        'DOWNLOAD_DIRS_INDEXABLE': 'false',
        'CUSTOM_DIRS': 'true',
        'CREATE_CUSTOM_DIRS': 'true',
        'CUSTOM_DIRS_EXCLUDE_REGEX': r'(^|/)[.@].*$',
        'DEFAULT_FOLDER': '',
        'DELETE_FILE_ON_TRASHCAN': 'false',
        'STATE_DIR': '.',
        'URL_PREFIX': '',
        'PUBLIC_HOST_URL': 'download/',
        'PUBLIC_HOST_AUDIO_URL': 'audio_download/',
        'OUTPUT_TEMPLATE': '%(title)s.%(ext)s',
        'OUTPUT_TEMPLATE_CHAPTER': '%(title)s - %(section_number)02d - %(section_title)s.%(ext)s',
        'OUTPUT_TEMPLATE_PLAYLIST': '%(playlist_title)s/%(title)s.%(ext)s',
        'OUTPUT_TEMPLATE_CHANNEL': '%(channel)s/%(title)s.%(ext)s',
        'DEFAULT_OPTION_PLAYLIST_ITEM_LIMIT' : '0',
        'SUBSCRIPTION_DEFAULT_CHECK_INTERVAL': '60',
        'SUBSCRIPTION_SCAN_PLAYLIST_END': '50',
        'SUBSCRIPTION_MAX_SEEN_IDS': '50000',
        'CLEAR_COMPLETED_AFTER': '0',
        'YTDL_OPTIONS': '{}',
        'YTDL_OPTIONS_FILE': '',
        'YTDL_OPTIONS_PRESETS': '{}',
        'YTDL_OPTIONS_PRESETS_FILE': '',
        'ALLOW_YTDL_OPTIONS_OVERRIDES': 'false',
        'ALLOW_PRIVATE_ADDRESSES': 'false',
        'CORS_ALLOWED_ORIGINS': '',
        'ROBOTS_TXT': '',
        'HOST': '0.0.0.0',
        'PORT': '8081',
        'HTTPS': 'false',
        'CERTFILE': '',
        'KEYFILE': '',
        'BASE_DIR': '',
        'DEFAULT_THEME': 'auto',
        'MAX_CONCURRENT_DOWNLOADS': '3',
        'LOGLEVEL': 'INFO',
        'ENABLE_ACCESSLOG': 'false',
        'YTDL_NIGHTLY_UPDATE_TIME': '',
    }

## 0.4 / 0.5 network at start and at rest
- yt-dlp self-update: ONLY when YTDL_NIGHTLY_UPDATE_TIME is set (default empty; the template does not set it) — then pip installs yt-dlp nightly at start and at that time each day (exit 42 restart). Off here: the image's yt-dlp stays the pinned one.
98:        'YTDL_NIGHTLY_UPDATE_TIME': '',
187:        if self.YTDL_NIGHTLY_UPDATE_TIME and not _NIGHTLY_TIME_RE.match(self.YTDL_NIGHTLY_UPDATE_TIME):
189:                'Environment variable "YTDL_NIGHTLY_UPDATE_TIME" must be HH:MM (24-hour), got "%s"',
190:                self.YTDL_NIGHTLY_UPDATE_TIME,
755:    time_hhmm = config.YTDL_NIGHTLY_UPDATE_TIME
1532:        sys.exit(42)
- bgutil-pot server: started by the entrypoint (a local PO-token provider for YouTube; it talks to YouTube only when a YouTube download asks for a token).
- no telemetry, no version check: grep for analytics/telemetry/sentry/version-check in app/:
  (empty above = none found; positive control: 'YTDL_NIGHTLY' is found by the same grep shape above)

## 0.7 clients
112:| `CORS_ALLOWED_ORIGINS` | | Comma-separated origins allowed to call MeTube from other sites, as [browser extensions and bookmarklets](https://github.com/alexta69/metube/wiki/Sending-links-to-MeTube) do. `*` allows any site, so prefer naming them. |
118:yt-dlp options are JSON objects keyed by yt-dlp's [API option names](https://github.com/yt-dlp/yt-dlp/blob/master/yt_dlp/YoutubeDL.py#L224), which differ from its command-line flags ([converter](https://github.com/yt-dlp/yt-dlp/blob/master/devscripts/cli_to_api.py)). They apply at three levels, and for an option set at more than one, the more specific level wins:
139:* [Sending links to MeTube](https://github.com/alexta69/metube/wiki/Sending-links-to-MeTube) — browser extensions, bookmarklets, iOS and Android apps, Raycast.
