fix(youtube): bake JS-challenge solver into backend image + Gitea CI
Build and Publish Backend Image / build (push) Failing after 14s

YouTube playback relied on manual changes inside the running container
(deno install + /etc/yt-dlp.conf) that would be lost on any recreate.
Make it permanent and reproducible:

- Dockerfile.backend: install pinned deno 2.8.3 as yt-dlp's JS runtime and
  write a system-wide /etc/yt-dlp.conf enabling the EJS challenge solver
  (--js-runtimes deno, --remote-components ejs:github, --force-ipv4).
  Replaces the unreliable node-based config.
- .gitea/workflows/build-backend.yml: build Dockerfile.backend and push
  git.qomar.pw/omar/ts6-manager/backend:latest (amd64). Needs REGISTRY_TOKEN.
- docker-compose.yml: pull backend from the Gitea registry; frontend/sidecar
  stay on upstream clusterzx images. Mount ./secrets read-only for optional
  YouTube cookies via YT_COOKIE_FILE.
- gitignore/dockerignore: never commit cookies (live Google session).
- DEPLOY.md: deployment + CI notes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-16 07:27:57 +03:00
co-authored by Claude Opus 4.8
parent dd26e57954
commit b93d476903
7 changed files with 148 additions and 8 deletions
+3
View File
@@ -6,3 +6,6 @@
.git/
.claude/
.vscode/
secrets/
youtube-cookies.txt
cookies.txt
+73
View File
@@ -0,0 +1,73 @@
name: Build and Publish Backend Image
# Builds the backend Docker image (with the baked-in YouTube JS-challenge fix:
# deno runtime + yt-dlp EJS solver) and pushes it to Gitea's built-in registry.
#
# Image: git.qomar.pw/omar/ts6-manager/backend
# Triggers: push to main, any vX.Y.Z tag, and manual dispatch.
#
# Requires repo secret REGISTRY_TOKEN = a PAT with scope `write:package`.
# (The auto GITEA_TOKEN has no Packages write permission.)
on:
push:
branches:
- main
tags:
- 'v*'
paths:
- 'packages/**'
- 'Dockerfile.backend'
- 'pnpm-lock.yaml'
- 'package.json'
- '.gitea/workflows/build-backend.yml'
workflow_dispatch:
env:
REGISTRY: git.qomar.pw
# github.repository in Gitea = "omar/ts6-manager" (already lowercase)
IMAGE: git.qomar.pw/${{ github.repository }}/backend
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Gitea registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME || 'omar' }}
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix=sha-
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push
uses: docker/build-push-action@v6
env:
DOCKER_BUILD_RECORD_UPLOAD: "false"
DOCKER_BUILD_SUMMARY: "false"
with:
context: .
file: ./Dockerfile.backend
target: production
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+6
View File
@@ -14,3 +14,9 @@ packages/backend/data/
packages/backend/prisma/migrations/
.claude/
build-and-push.ps1
# Secrets — NEVER commit. youtube-cookies.txt is a live Google session.
youtube-cookies.txt
cookies.txt
secrets/*
!secrets/.gitkeep
+42
View File
@@ -0,0 +1,42 @@
# Deploy (omar fork)
This fork bakes the YouTube playback fix into the **backend** image so it survives
container/host restarts. The fix: `deno` JS runtime + a system-wide `/etc/yt-dlp.conf`
that enables yt-dlp's EJS challenge solver (`--remote-components ejs:github`,
`--js-runtimes deno`, `--force-ipv4`). See `Dockerfile.backend`.
## How images are built
Gitea Actions (`.gitea/workflows/build-backend.yml`) builds `Dockerfile.backend` on
every push to `main` and pushes it to the Gitea registry:
```
git.qomar.pw/omar/ts6-manager/backend:latest
```
`frontend` and `sidecar` use the upstream `clusterzx/ts6-manager:*` public images
unchanged.
**Required repo secret:** `REGISTRY_TOKEN` — a Gitea PAT with scope `write:package`
(Settings → Applications → Generate New Token). Optional `REGISTRY_USERNAME`
(defaults to `omar`).
## Deploy on the server
```sh
cd /opt/ts6-manager
# 1. Provide config (JWT_SECRET etc.)
cp .env.example .env && $EDITOR .env
# 2. (Optional) YouTube cookies — a Netscape-format export. Skippable: playback
# works without them via the EJS solver. NEVER commit this file.
mkdir -p secrets
cp /path/to/youtube-cookies.txt secrets/youtube-cookies.txt
# 3. Pull & run
docker login git.qomar.pw # once, with a read:package token
docker compose pull
docker compose up -d
```
Update to a fresh build later with `docker compose pull && docker compose up -d`.
+12 -2
View File
@@ -2,6 +2,11 @@
FROM node:20-slim AS base
RUN corepack enable && corepack prepare pnpm@9 --activate
# Deno: JS runtime yt-dlp uses to solve YouTube's nsig / player-JS challenge via the
# EJS remote component. `node` alone is unreliable for this; deno is the proven runtime.
# Pinned to the version verified working in production. See /etc/yt-dlp.conf below.
COPY --from=denoland/deno:bin-2.8.3 /deno /usr/local/bin/deno
# Install system deps: openssl (Prisma), ffmpeg (audio), python3+pip (yt-dlp)
RUN apt-get update && \
apt-get install -y --no-install-recommends \
@@ -41,9 +46,14 @@ COPY --from=build /app/packages/backend/package.json ./packages/backend/package.
COPY --from=build /app/packages/backend/node_modules ./packages/backend/node_modules
WORKDIR /app/packages/backend
# System-wide yt-dlp config (works regardless of which user the process runs as).
# This is the fix that solves YouTube playback: deno runtime + EJS challenge solver.
RUN mkdir -p data /data/music && \
mkdir -p /root/.config/yt-dlp && \
echo "--js-runtimes node" > /root/.config/yt-dlp/config
printf '%s\n' \
'--js-runtimes deno' \
'--remote-components ejs:github' \
'--force-ipv4' \
> /etc/yt-dlp.conf
EXPOSE 3001
ENV SIDECAR_URL=http://ts6-sidecar:9800
+8 -6
View File
@@ -1,8 +1,8 @@
version: '3.8'
services:
backend:
image: clusterzx/ts6-manager:backend
# Custom backend with the baked-in YouTube fix (deno + yt-dlp EJS solver),
# built by Gitea Actions and published to the Gitea registry.
image: git.qomar.pw/omar/ts6-manager/backend:latest
container_name: ts6-backend
restart: unless-stopped
environment:
@@ -12,7 +12,9 @@ services:
- JWT_SECRET=${JWT_SECRET:?Set JWT_SECRET in .env or environment}
- ENCRYPTION_KEY=${ENCRYPTION_KEY:-}
- TS_ALLOW_SELF_SIGNED=${TS_ALLOW_SELF_SIGNED:-false}
- YT_COOKIE_FILE=${YT_COOKIE_FILE:-}
# Optional YouTube cookies. Drop the file at ./secrets/youtube-cookies.txt.
# If absent, playback still works via the deno + EJS challenge solver.
- YT_COOKIE_FILE=${YT_COOKIE_FILE:-/secrets/youtube-cookies.txt}
- JWT_ACCESS_EXPIRY=15m
- JWT_REFRESH_EXPIRY=7d
- FRONTEND_URL=${FRONTEND_URL:-http://localhost:3000}
@@ -22,8 +24,8 @@ services:
volumes:
- backend-data:/app/packages/backend/data
- music-data:/data/music
# Optional: mount YouTube cookies file for yt-dlp
# - ./cookies.txt:/app/cookies.txt
# Mounted read-only; the actual cookie file is gitignored (never committed).
- ./secrets:/secrets:ro
ports:
- "3001:3001"
depends_on:
+4
View File
@@ -0,0 +1,4 @@
# Place runtime secrets here (gitignored). For YouTube cookies, drop a Netscape-format
# cookies export as ./secrets/youtube-cookies.txt — it is mounted read-only into the
# backend at /secrets/youtube-cookies.txt (see docker-compose.yml / YT_COOKIE_FILE).
# Never commit the actual cookie file: it is a live Google session.