Update invidious stack to use companion approach

- Replace inv_sig_helper with invidious-companion for better YouTube API handling
- Add healthcheck for main container
- Add resource limits for all containers
- Add SQL init scripts for fresh database setup
- Update README with invidious secrets documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-20 12:52:19 -05:00
parent 608cc9253d
commit 9f61b06592
13 changed files with 289 additions and 15 deletions

View File

@@ -18,6 +18,7 @@ stacks/
├── perilous/ # Blog/website ├── perilous/ # Blog/website
├── ramz/ # Go web app ├── ramz/ # Go web app
├── bookclub/ # Form mailer ├── bookclub/ # Form mailer
├── invidious/ # YouTube frontend
├── watchtower/ # Auto container updates ├── watchtower/ # Auto container updates
├── dockge/ # Container management UI ├── dockge/ # Container management UI
└── smokeping/ # Network monitoring └── smokeping/ # Network monitoring
@@ -96,6 +97,13 @@ Set these in Gitea → Repository → Settings → Actions → Secrets:
|--------|-------------| |--------|-------------|
| `PERILOUS_CODE_SERVER_PASSWORD` | Code-server password | | `PERILOUS_CODE_SERVER_PASSWORD` | Code-server password |
### Invidious
| Secret | Description |
|--------|-------------|
| `INVIDIOUS_DB_PASSWORD` | PostgreSQL password |
| `INVIDIOUS_HMAC_KEY` | Generate: `openssl rand -hex 16` |
| `INVIDIOUS_COMPANION_KEY` | Generate: `openssl rand -hex 16` (must match companion) |
## Runner Setup ## Runner Setup
The workflow requires a self-hosted runner on the prod server: The workflow requires a self-hosted runner on the prod server:

View File

@@ -1,5 +1,4 @@
DOMAIN=${DOMAIN} DOMAIN=${DOMAIN}
INVIDIOUS_DB_PASSWORD=${INVIDIOUS_DB_PASSWORD} INVIDIOUS_DB_PASSWORD=${INVIDIOUS_DB_PASSWORD}
INVIDIOUS_VISITOR_DATA=${INVIDIOUS_VISITOR_DATA}
INVIDIOUS_PO_TOKEN=${INVIDIOUS_PO_TOKEN}
INVIDIOUS_HMAC_KEY=${INVIDIOUS_HMAC_KEY} INVIDIOUS_HMAC_KEY=${INVIDIOUS_HMAC_KEY}
INVIDIOUS_COMPANION_KEY=${INVIDIOUS_COMPANION_KEY}

View File

@@ -0,0 +1,12 @@
-- Table: public.annotations
-- DROP TABLE public.annotations;
CREATE TABLE IF NOT EXISTS public.annotations
(
id text NOT NULL,
annotations xml,
CONSTRAINT annotations_id_key UNIQUE (id)
);
GRANT ALL ON TABLE public.annotations TO current_user;

View File

@@ -0,0 +1,30 @@
-- Table: public.channel_videos
-- DROP TABLE public.channel_videos;
CREATE TABLE IF NOT EXISTS public.channel_videos
(
id text NOT NULL,
title text,
published timestamp with time zone,
updated timestamp with time zone,
ucid text,
author text,
length_seconds integer,
live_now boolean,
premiere_timestamp timestamp with time zone,
views bigint,
CONSTRAINT channel_videos_id_key UNIQUE (id)
);
GRANT ALL ON TABLE public.channel_videos TO current_user;
-- Index: public.channel_videos_ucid_idx
-- DROP INDEX public.channel_videos_ucid_idx;
CREATE INDEX IF NOT EXISTS channel_videos_ucid_idx
ON public.channel_videos
USING btree
(ucid COLLATE pg_catalog."default");

View File

@@ -0,0 +1,25 @@
-- Table: public.channels
-- DROP TABLE public.channels;
CREATE TABLE IF NOT EXISTS public.channels
(
id text NOT NULL,
author text,
updated timestamp with time zone,
deleted boolean,
subscribed timestamp with time zone,
CONSTRAINT channels_id_key UNIQUE (id)
);
GRANT ALL ON TABLE public.channels TO current_user;
-- Index: public.channels_id_idx
-- DROP INDEX public.channels_id_idx;
CREATE INDEX IF NOT EXISTS channels_id_idx
ON public.channels
USING btree
(id COLLATE pg_catalog."default");

View File

@@ -0,0 +1,22 @@
-- Table: public.nonces
-- DROP TABLE public.nonces;
CREATE TABLE IF NOT EXISTS public.nonces
(
nonce text,
expire timestamp with time zone,
CONSTRAINT nonces_id_key UNIQUE (nonce)
);
GRANT ALL ON TABLE public.nonces TO current_user;
-- Index: public.nonces_nonce_idx
-- DROP INDEX public.nonces_nonce_idx;
CREATE INDEX IF NOT EXISTS nonces_nonce_idx
ON public.nonces
USING btree
(nonce COLLATE pg_catalog."default");

View File

@@ -0,0 +1,19 @@
-- Table: public.playlist_videos
-- DROP TABLE public.playlist_videos;
CREATE TABLE IF NOT EXISTS public.playlist_videos
(
title text,
id text,
author text,
ucid text,
length_seconds integer,
published timestamptz,
plid text references playlists(id),
index int8,
live_now boolean,
PRIMARY KEY (index,plid)
);
GRANT ALL ON TABLE public.playlist_videos TO current_user;

View File

@@ -0,0 +1,29 @@
-- Type: public.privacy
-- DROP TYPE public.privacy;
CREATE TYPE public.privacy AS ENUM
(
'Public',
'Unlisted',
'Private'
);
-- Table: public.playlists
-- DROP TABLE public.playlists;
CREATE TABLE IF NOT EXISTS public.playlists
(
title text,
id text primary key,
author text,
description text,
video_count integer,
created timestamptz,
updated timestamptz,
privacy privacy,
index int8[]
);
GRANT ALL ON public.playlists TO current_user;

View File

@@ -0,0 +1,23 @@
-- Table: public.session_ids
-- DROP TABLE public.session_ids;
CREATE TABLE IF NOT EXISTS public.session_ids
(
id text NOT NULL,
email text,
issued timestamp with time zone,
CONSTRAINT session_ids_pkey PRIMARY KEY (id)
);
GRANT ALL ON TABLE public.session_ids TO current_user;
-- Index: public.session_ids_id_idx
-- DROP INDEX public.session_ids_id_idx;
CREATE INDEX IF NOT EXISTS session_ids_id_idx
ON public.session_ids
USING btree
(id COLLATE pg_catalog."default");

View File

@@ -0,0 +1,29 @@
-- Table: public.users
-- DROP TABLE public.users;
CREATE TABLE IF NOT EXISTS public.users
(
updated timestamp with time zone,
notifications text[],
subscriptions text[],
email text NOT NULL,
preferences text,
password text,
token text,
watched text[],
feed_needs_update boolean,
CONSTRAINT users_email_key UNIQUE (email)
);
GRANT ALL ON TABLE public.users TO current_user;
-- Index: public.email_unique_idx
-- DROP INDEX public.email_unique_idx;
CREATE UNIQUE INDEX IF NOT EXISTS email_unique_idx
ON public.users
USING btree
(lower(email) COLLATE pg_catalog."default");

View File

@@ -0,0 +1,23 @@
-- Table: public.videos
-- DROP TABLE public.videos;
CREATE UNLOGGED TABLE IF NOT EXISTS public.videos
(
id text NOT NULL,
info text,
updated timestamp with time zone,
CONSTRAINT videos_pkey PRIMARY KEY (id)
);
GRANT ALL ON TABLE public.videos TO current_user;
-- Index: public.id_idx
-- DROP INDEX public.id_idx;
CREATE UNIQUE INDEX IF NOT EXISTS id_idx
ON public.videos
USING btree
(id COLLATE pg_catalog."default");

View File

@@ -3,8 +3,6 @@ services:
image: quay.io/invidious/invidious:latest image: quay.io/invidious/invidious:latest
container_name: invidious container_name: invidious
restart: unless-stopped restart: unless-stopped
ports:
- "3001:3000"
environment: environment:
INVIDIOUS_CONFIG: | INVIDIOUS_CONFIG: |
db: db:
@@ -14,45 +12,81 @@ services:
host: invidious-db host: invidious-db
port: 5432 port: 5432
check_tables: true check_tables: true
signature_server: inv_sig_helper:12999 invidious_companion:
visitor_data: ${INVIDIOUS_VISITOR_DATA} - private_url: "http://invidious-companion:8282/companion"
po_token: ${INVIDIOUS_PO_TOKEN} public_url: "https://inv.${DOMAIN}"
invidious_companion_key: "${INVIDIOUS_COMPANION_KEY}"
external_port: 443
domain: inv.${DOMAIN}
https_only: true
hmac_key: "${INVIDIOUS_HMAC_KEY}" hmac_key: "${INVIDIOUS_HMAC_KEY}"
hsts: false
log_level: Info
healthcheck:
test: wget -nv --tries=1 --spider http://127.0.0.1:3000/api/v1/trending || exit 1
interval: 30s
timeout: 5s
retries: 2
logging: logging:
options: options:
max-size: "1G" max-size: "1G"
max-file: "4" max-file: "4"
depends_on: depends_on:
- invidious-db - invidious-db
deploy:
resources:
limits:
cpus: '1'
memory: 2G
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
- "traefik.http.services.invidious.loadbalancer.server.port=3000" - "traefik.http.services.invidious.loadbalancer.server.port=3000"
- "traefik.http.routers.invidious.entrypoints=https" - "traefik.http.routers.invidious.entrypoints=https"
- "traefik.http.routers.invidious.rule=Host(`invid.${DOMAIN}`, `i.${DOMAIN}`)" - "traefik.http.routers.invidious.rule=Host(`inv.${DOMAIN}`)"
- "traefik.http.routers.invidious.tls.certresolver=http" - "traefik.http.routers.invidious.tls.certresolver=http"
networks: networks:
- web - web
- default - default
inv_sig_helper: invidious-companion:
image: quay.io/invidious/inv-sig-helper:latest image: quay.io/invidious/invidious-companion:latest
container_name: inv-sig-helper container_name: invidious-companion
command: ["--tcp", "0.0.0.0:12999"]
environment:
- RUST_LOG=info
restart: unless-stopped restart: unless-stopped
environment:
- SERVER_SECRET_KEY=${INVIDIOUS_COMPANION_KEY}
- SERVER_BASE_URL=https://inv.${DOMAIN}/companion
logging:
options:
max-size: "1G"
max-file: "4"
cap_drop: cap_drop:
- ALL - ALL
read_only: true read_only: true
volumes:
- companion-cache:/var/tmp/youtubei.js:rw
security_opt: security_opt:
- no-new-privileges:true - no-new-privileges:true
deploy:
resources:
limits:
cpus: '0.5'
memory: 1G
labels:
- "traefik.enable=true"
- "traefik.http.services.invidious-companion.loadbalancer.server.port=8282"
- "traefik.http.routers.invidious-companion.entrypoints=https"
- "traefik.http.routers.invidious-companion.rule=Host(`inv.${DOMAIN}`) && PathPrefix(`/companion`)"
- "traefik.http.routers.invidious-companion.tls.certresolver=http"
networks:
- web
- default
invidious-db: invidious-db:
image: postgres:14 image: postgres:14
container_name: invidious-db container_name: invidious-db
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- ./postgres:/var/lib/postgresql/data - postgres-data:/var/lib/postgresql/data
- ./config/sql:/config/sql - ./config/sql:/config/sql
- ./docker/init-invidious-db.sh:/docker-entrypoint-initdb.d/init-invidious-db.sh - ./docker/init-invidious-db.sh:/docker-entrypoint-initdb.d/init-invidious-db.sh
environment: environment:
@@ -61,6 +95,15 @@ services:
POSTGRES_PASSWORD: ${INVIDIOUS_DB_PASSWORD} POSTGRES_PASSWORD: ${INVIDIOUS_DB_PASSWORD}
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
deploy:
resources:
limits:
cpus: '0.5'
memory: 1G
volumes:
postgres-data:
companion-cache:
networks: networks:
web: web:

View File

@@ -0,0 +1,12 @@
#!/bin/bash
set -eou pipefail
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/channels.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/videos.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/channel_videos.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/users.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/session_ids.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/nonces.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/annotations.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/playlists.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/playlist_videos.sql