Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions .github/workflows/deno-check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,43 @@ on:
jobs:
testing:
runs-on: ubuntu-latest
env:
CAMOUFOX_INSTALL_DIR: /var/tmp/youtubei.js/camoufox
DENO_DIR: /var/tmp/youtubei.js/deno-cache
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1"

steps:
- name: Setup repo
uses: actions/checkout@v7

- name: Setup Deno
uses: denoland/setup-deno@v2
with:
deno-version: v2.x

- name: Install Camoufox
run: |
playwright_specifier="$(sed '/^[[:space:]]*\/\//d' deno.jsonc | jq -er '.imports["playwright-core"]')"
deno run -A "${playwright_specifier}" install-deps firefox
camoufox_tag="$(jq -er '.camoufox.linux.x86_64.version' dependencies.json)"
camoufox_version="${camoufox_tag#v}"
camoufox_checksum="$(jq -er '.camoufox.linux.x86_64.sha256' dependencies.json)"
mkdir -p /var/tmp/youtubei.js/camoufox
curl -fsSL --output /tmp/camoufox.zip \
"https://github.com/daijro/camoufox/releases/download/${camoufox_tag}/camoufox-${camoufox_version}-lin.x86_64.zip"
echo "${camoufox_checksum} /tmp/camoufox.zip" | sha256sum -c -
unzip -q /tmp/camoufox.zip -d /var/tmp/youtubei.js/camoufox
printf '{"version":"%s","release":"%s"}\n' \
"${camoufox_version%%-*}" "${camoufox_version#*-}" \
> /var/tmp/youtubei.js/camoufox/version.json
rm /tmp/camoufox.zip

- name: Verify formatting
run: deno task format

- name: Verify typing
run: deno task check

- name: Run linter
run: deno task lint

Expand Down Expand Up @@ -50,7 +71,7 @@ jobs:
PROXY_PID=$!
sleep 3s
curl -s -x http://127.0.0.1:18080 --retry 5 --retry-delay 2 --retry-connrefused https://check.torproject.org/api/ip; echo
rm -rf /var/tmp/youtubei.js
rm -f /var/tmp/youtubei.js/kv_cache.sqlite3*
if PROXY=http://127.0.0.1:18080 deno task test; then
OPERA_SUCCESS=true
kill $PROXY_PID 2>/dev/null
Expand All @@ -69,7 +90,7 @@ jobs:
echo "=== Tor attempt $attempt/12 ==="
sudo pkill -HUP tor
curl -s --socks5 127.0.0.1:9150 https://check.torproject.org/api/ip; echo
rm -rf /var/tmp/youtubei.js
rm -f /var/tmp/youtubei.js/kv_cache.sqlite3*
if PROXY=socks5://127.0.0.1:9150 deno task test; then
echo "=== Tests passed with tor ==="
exit 0
Expand Down
9 changes: 6 additions & 3 deletions .github/workflows/release-binaries.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,15 @@ jobs:
- name: Build binary
run: |
deno compile \
--self-extracting \
--include ./dependencies.json \
--include ./src/lib/helpers/youtubePlayerReq.ts \
--include ./src/lib/helpers/getFetchClient.ts \
--include ./src/lib/jobs/worker.ts \
--allow-import=github.com:443,jsr.io:443,cdn.jsdelivr.net:443,esm.sh:443,deno.land:443 \
--allow-net --allow-env --allow-sys=hostname \
--allow-read=.,/var/tmp/youtubei.js,/tmp/invidious-companion.sock \
--allow-write=/var/tmp/youtubei.js,/tmp/invidious-companion.sock \
--allow-net --allow-env --allow-sys=hostname,homedir,osRelease --allow-run --allow-ffi \
--allow-read=.,/usr/bin/ldd,/var/tmp/youtubei.js,/tmp/invidious-companion.sock,/tmp/companionsock \
--allow-write=/var/tmp/youtubei.js,/tmp/invidious-companion.sock,/tmp/companionsock \
--target=${{ matrix.target }} \
--output ${{ steps.binary-name.outputs.name }} \
src/main.ts \
Expand Down
98 changes: 82 additions & 16 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,23 @@ ARG THC_AMD64_SHA256='cb1797948015da46c222764a99ee30c06a6a9a30f5b87f212a28ea3c6d
# we can use these aliases and let dependabot remain simple
# inspired by:
# https://github.com/dependabot/dependabot-core/issues/2057#issuecomment-1351660410
FROM alpine:3.24 AS dependabot-alpine
FROM debian:13-slim AS dependabot-debian

# Retrieve the deno binary from the repository
FROM denoland/deno:bin-2.9.2 AS deno-bin
FROM golang:1.25-bookworm AS go-builder

# Build the shell-free volume bootstrap used by the final image.
FROM go-builder AS bootstrap-builder
WORKDIR /src
COPY ./docker/camoufox-bootstrap.go ./main.go
RUN CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o /camoufox-bootstrap ./main.go

# Stage for creating the non-privileged user
FROM dependabot-alpine AS user-stage

RUN adduser -u 10001 -S appuser

# Stage for downloading files using curl from Debian
FROM dependabot-debian AS debian-curl
RUN DEBIAN_FRONTEND='noninteractive' && export DEBIAN_FRONTEND && \
apt-get update && apt-get install -y curl xz-utils
apt-get update && apt-get install -y curl jq xz-utils

# Download tiny-health-checker from GitHub
FROM debian-curl AS thc-download
Expand Down Expand Up @@ -86,6 +87,22 @@ ARG TINI_VERSION
ENV TINI_VERSION="${TINI_VERSION}"
COPY --from=tini-download /tini /tini

# Download a reproducible Camoufox build for the target architecture.
FROM debian-curl AS camoufox-download
COPY ./dependencies.json /dependencies.json
RUN arch="$(dpkg --print-architecture)" && \
case "${arch}" in \
amd64) camoufox_arch='x86_64' ;; \
arm64) camoufox_arch='arm64' ;; \
*) echo "Unsupported Camoufox architecture: ${arch}" >&2; exit 1 ;; \
esac && \
camoufox_tag="$(jq -er --arg arch "${camoufox_arch}" '.camoufox.linux[$arch].version' /dependencies.json)" && \
camoufox_version="${camoufox_tag#v}" && \
checksum="$(jq -er --arg arch "${camoufox_arch}" '.camoufox.linux[$arch].sha256' /dependencies.json)" && \
curl -fsSL --output /camoufox.zip \
"https://github.com/daijro/camoufox/releases/download/${camoufox_tag}/camoufox-${camoufox_version}-lin.${camoufox_arch}.zip" && \
echo "${checksum} /camoufox.zip" | sha256sum -c -

# Stage for using git from Debian
FROM dependabot-debian AS debian-git
RUN DEBIAN_FRONTEND='noninteractive' && export DEBIAN_FRONTEND && \
Expand Down Expand Up @@ -114,6 +131,7 @@ WORKDIR /app

COPY deno.lock ./
COPY deno.jsonc ./
COPY dependencies.json ./

COPY ./src/ ./src/

Expand All @@ -124,41 +142,89 @@ RUN --mount=type=bind,rw,source=.git,target=/app/.git \
--mount=type=cache,target="${DENO_DIR}" \
deno task compile

FROM gcr.io/distroless/cc AS app
FROM dependabot-debian AS camoufox-runtime

# Copy group file for the non-privileged user from the user-stage
COPY --from=user-stage /etc/group /etc/group

# Copy passwd file for the non-privileged user from the user-stage
COPY --from=user-stage /etc/passwd /etc/passwd
RUN DEBIAN_FRONTEND='noninteractive' && export DEBIAN_FRONTEND && \
apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
libasound2 \
libatk1.0-0t64 \
libcairo-gobject2 \
libcairo2 \
libdbus-1-3 \
libdbus-glib-1-2 \
libfontconfig1 \
libfreetype6 \
libgdk-pixbuf-2.0-0 \
libglib2.0-0t64 \
libgtk-3-0t64 \
libharfbuzz0b \
libpango-1.0-0 \
libpangocairo-1.0-0 \
libx11-6 \
libx11-xcb1 \
libxcb-shm0 \
libxcb1 \
libxcomposite1 \
libxcursor1 \
libxdamage1 \
libxext6 \
libxfixes3 \
libxi6 \
libxrandr2 \
libxrender1 \
libxtst6 && \
rm -rf /var/lib/apt/lists/* /usr/share/doc /usr/share/man

FROM gcr.io/distroless/cc-debian13:nonroot AS app

# Camoufox dynamically loads Firefox/GTK libraries. Copy only the prepared
# runtime filesystem into the shell-less final stage.
COPY --from=camoufox-runtime /lib/ /lib/
COPY --from=camoufox-runtime /usr/lib/ /usr/lib/
COPY --from=camoufox-runtime /usr/share/ /usr/share/
COPY --from=camoufox-runtime /etc/fonts/ /etc/fonts/
COPY --from=camoufox-runtime /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=camoufox-runtime /var/cache/fontconfig/ /var/cache/fontconfig/

COPY --from=thc-bin /thc /thc
COPY --from=tini-bin /tini /tini
COPY --from=bootstrap-builder /camoufox-bootstrap /camoufox-bootstrap
COPY --from=camoufox-download --chown=10001:10001 /camoufox.zip /usr/share/invidious-companion/camoufox.zip
COPY --chmod=0444 ./docker/ldd /usr/bin/ldd

# Copy cache directory and set correct permissions
COPY --from=builder --chown=appuser:nogroup /var/tmp/youtubei.js /var/tmp/youtubei.js
COPY --from=builder --chown=10001:10001 /var/tmp/youtubei.js /var/tmp/youtubei.js

# Set the working directory
WORKDIR /app

COPY --from=builder /app/invidious_companion ./
COPY --chmod=0444 ./dependencies.json ./

ARG HOST PORT THC_VERSION THC_PORT_NAME TINI_VERSION
EXPOSE "${PORT}/tcp"

ENV SERVER_BASE_PATH=/companion \
CAMOUFOX_INSTALL_DIR=/var/tmp/youtubei.js/camoufox \
HOME=/var/tmp/youtubei.js/home \
HOST="${HOST}" \
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 \
PORT="${PORT}" \
TMPDIR=/var/tmp/youtubei.js/tmp \
THC_PORT_NAME="${THC_PORT_NAME}" \
THC_PATH="/healthz" \
THC_VERSION="${THC_VERSION}" \
TINI_VERSION="${TINI_VERSION}"
TINI_VERSION="${TINI_VERSION}" \
XDG_CACHE_HOME=/var/tmp/youtubei.js/xdg-cache \
XDG_DATA_HOME=/var/tmp/youtubei.js/xdg-data

COPY ./config/ ./config/

# Switch to non-privileged user
USER appuser
USER 10001:10001

ENTRYPOINT ["/tini", "--", "/app/invidious_companion"]
ENTRYPOINT ["/tini", "--", "/camoufox-bootstrap"]
CMD ["/app/invidious_companion"]

HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=5 CMD ["/thc"]
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,15 @@ Companion for Invidious which handle all the video stream retrieval from YouTube

### Requirements

- [deno](https://docs.deno.com/runtime/)
- [deno](https://docs.deno.com/runtime/)
- [Camoufox](https://camoufox.com/) for browser-backed PO token generation.
The Docker image installs a pinned headless browser into the existing
`/var/tmp/youtubei.js` volume automatically. Local development downloads it
into the same directory on first startup.

Companion falls back to its previous JSDOM generator when Camoufox cannot be
installed or launched. Camoufox runs with `headless: true`; Xvfb is not used.
The production image uses a shell-less Google Distroless runtime.

### Run Locally (development)

Expand Down
12 changes: 8 additions & 4 deletions deno.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
// Deno tasks definitions.
"tasks": {
// Run the server in development mode with file-watching.
"dev": "deno run --allow-import=github.com:443,jsr.io:443,cdn.jsdelivr.net:443,esm.sh:443,deno.land:443 --allow-net --allow-env --allow-sys=hostname --allow-read=.,/var/tmp/youtubei.js,/tmp/invidious-companion.sock,/tmp/companionsock --allow-write=/var/tmp/youtubei.js,/tmp/invidious-companion.sock,/tmp/companionsock --watch src/main.ts",
"dev": "DENO_DIR=/var/tmp/youtubei.js/deno-cache XDG_CACHE_HOME=/var/tmp/youtubei.js/xdg-cache deno run --allow-import=github.com:443,jsr.io:443,cdn.jsdelivr.net:443,esm.sh:443,deno.land:443 --allow-net --allow-env --allow-sys=hostname,homedir,osRelease --allow-run --allow-ffi --allow-read=.,/usr/bin/ldd,/var/tmp/youtubei.js,/tmp/invidious-companion.sock,/tmp/companionsock --deny-read=/proc/version,/etc/os-release,/proc/self/cgroup --allow-write=/var/tmp/youtubei.js,/tmp/invidious-companion.sock,/tmp/companionsock --watch src/main.ts",
// Compile a self-contained executable binary.
"compile": "deno compile --include ./src/lib/helpers/youtubePlayerReq.ts --include ./src/lib/helpers/getFetchClient.ts --output invidious_companion --allow-import=github.com:443,jsr.io:443,cdn.jsdelivr.net:443,esm.sh:443,deno.land:443 --allow-net --allow-env --allow-read --allow-sys=hostname --allow-write=/var/tmp/youtubei.js,/tmp/invidious-companion.sock,/tmp/companionsock src/main.ts --_version_date=\"$(git log -1 --format=%ci | awk '{print $1}' | sed s/-/./g)\" --_version_commit=\"$(git rev-list HEAD --max-count=1 --abbrev-commit)\"",
"compile": "deno compile --self-extracting --include ./dependencies.json --include ./src/lib/helpers/youtubePlayerReq.ts --include ./src/lib/helpers/getFetchClient.ts --include ./src/lib/jobs/worker.ts --output invidious_companion --allow-import=github.com:443,jsr.io:443,cdn.jsdelivr.net:443,esm.sh:443,deno.land:443 --allow-net --allow-env --allow-sys=hostname,homedir,osRelease --allow-run --allow-ffi --allow-read=.,/usr/bin/ldd,/var/tmp/youtubei.js,/tmp/invidious-companion.sock,/tmp/companionsock --deny-read=/proc/version,/etc/os-release,/proc/self/cgroup --allow-write=/var/tmp/youtubei.js,/tmp/invidious-companion.sock,/tmp/companionsock src/main.ts --_version_date=\"$(git log -1 --format=%ci | awk '{print $1}' | sed s/-/./g)\" --_version_commit=\"$(git rev-list HEAD --max-count=1 --abbrev-commit)\"",
// Run integration tests.
"test": "deno test --allow-import=github.com:443,jsr.io:443,cdn.jsdelivr.net:443,esm.sh:443,deno.land:443 --allow-net --allow-env --allow-sys=hostname --allow-read=.,/var/tmp/youtubei.js,/tmp/invidious-companion.sock --allow-write=/var/tmp/youtubei.js",
"test": "DENO_DIR=/var/tmp/youtubei.js/deno-cache XDG_CACHE_HOME=/var/tmp/youtubei.js/xdg-cache deno test --allow-import=github.com:443,jsr.io:443,cdn.jsdelivr.net:443,esm.sh:443,deno.land:443 --allow-net --allow-env --allow-sys=hostname,homedir,osRelease --allow-run --allow-ffi --allow-read=.,/usr/bin/ldd,/var/tmp/youtubei.js,/tmp/invidious-companion.sock,/tmp/companionsock --deny-read=/proc/version,/etc/os-release,/proc/self/cgroup --allow-write=/var/tmp/youtubei.js,/tmp/invidious-companion.sock,/tmp/companionsock",
// Check code formatting.
"format": "deno fmt --check src/**",
// Type-check the source code without emitting output.
Expand All @@ -29,7 +29,11 @@
"youtubei.js/NavigationEndpoint": "https://cdn.jsdelivr.net/gh/LuanRT/YouTube.js@v17.2.0-deno/deno/src/parser/classes/NavigationEndpoint.ts",
"youtubei.js/PlayerCaptionsTracklist": "https://cdn.jsdelivr.net/gh/LuanRT/YouTube.js@v17.2.0-deno/deno/src/parser/classes/PlayerCaptionsTracklist.ts",
"youtubei.js/TabbedFeed": "https://cdn.jsdelivr.net/gh/LuanRT/YouTube.js@v17.2.0-deno/deno/src/core/mixins/TabbedFeed.ts",
// DOM implementation for Node/Deno (used by YouTube.js).
// Headless anti-detect Firefox used for browser-backed PO tokens.
"camoufox-js": "npm:camoufox-js@0.12.0",
// Camoufox's supported browser automation peer.
"playwright-core": "npm:playwright-core@1.60.0",
// DOM fallback used when Camoufox cannot be installed or launched.
"jsdom": "npm:jsdom@26.1.0",
// Botguard utilities for PO token generation.
"bgutils": "https://esm.sh/bgutils-js@3.2.0",
Expand Down
Loading
Loading