Skip to main content

Tube Archivist

A self-hosted YouTube archive with search and media management.

Tube Archivist is a self-hosted application for downloading, indexing, and managing YouTube videos. It uses Elasticsearch for full-text search across your archived content and provides a Netflix-style web UI for browsing channels and videos. Self-hosting gives you a permanent, searchable personal archive of YouTube content independent of YouTube's availability.

Alternatives considered​

Cloud Hosted

ToolOpen SourceFree TierMonthly Cost
YouTube PremiumNoNoFrom $13.99/mo

Self Hosted

ToolOpen SourceFull FeaturesNotes
yt-dlpYesYesCLI only; no web UI or indexing
InvidiousYesYesProxies YouTube; does not archive locally

Installation​

Architecture​

  • Deployments: tubearchivist (main app), archivist-es (Elasticsearch), Valkey (Redis)
  • Images: bbilly1/tubearchivist, bbilly1/tubearchivist-es, valkey/valkey, all digest-pinned via Renovate
  • Storage: Longhorn PVC (cache, k8up.io/backup: "true") for Elasticsearch data; NFS PVs from TrueNAS for downloads and video archive
  • Networking: ClusterIP services for app and ES; HTTPRoute via internal gateway

Security​

  • Tube Archivist runs as runAsUser: 1000, runAsGroup: 1000
  • Valkey: runAsUser: 999, runAsNonRoot: true, allowPrivilegeEscalation: false, capabilities dropped
  • Secrets SOPS-encrypted with age

Updates​

Managed by Renovate. All images are digest-pinned.

Data Management​

  • PVC: cache (Longhorn-encrypted, k8up.io/backup: "true") for Elasticsearch index data
  • NFS: TrueNAS NFS PVs for downloads staging and video archive
  • Backups: k8up Schedules to Hetzner and Backblaze via restic, covering the Elasticsearch data volume.

User Management​

No OIDC configured. User accounts managed through the Tube Archivist web UI.

Configuration Management​

  • ES_URL, REDIS_CON, HOST_UID, HOST_GID, TA_HOST from SOPS-encrypted secret
  • Elasticsearch and Redis connection details injected from secret

Usage​

Subscribe to YouTube channels in the UI and trigger downloads. Tube Archivist queues downloads via Redis and stores videos on the NFS share. Search your archive using the Elasticsearch-backed full-text search. Browser extension can queue videos from YouTube directly.

Cluster-specific deviations from the above live in the per-cluster README — see k8s/apps/tube-archivist/talos/README.md.

Cluster Deployment​

Tube Archivist — Talos cluster

Kubernetes Metadata
  • Image: bbilly1/tubearchivist-es:8.19.0@sha256:9da63fb1973ec3d57daf6916be948eddd0d8a404cc8e447c938480c85fe2c554
  • Image: bbilly1/tubearchivist:v0.5.12@sha256:ba1c846ddd0c6fdd0f040727129d2466e03b7a0c26499223839d450c7586ac09
  • Image: valkey/valkey:9.1.2-alpine@sha256:48332870af354a799964c0012ae1194a0bf2bf894eb508f945810596dc2d8d11
Rendered manifests (kustomize build)
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
kustomize.toolkit.fluxcd.io/force: enabled
name: tubearchivist
namespace: tube-archivist
spec:
replicas: 1
revisionHistoryLimit: 2
selector:
matchLabels:
app.kubernetes.io/name: tube-archivist
strategy:
rollingUpdate: null
type: Recreate
template:
metadata:
labels:
app.kubernetes.io/name: tube-archivist
spec:
automountServiceAccountToken: false
containers:
- env:
- name: ES_URL
value: http://archivist-es:9200
- name: REDIS_CON
value: redis://valkey:6379
- name: HOST_UID
value: '1000'
- name: HOST_GID
value: '1000'
- name: TA_HOST
value: https://tube-archivist.web.kueber.eu
- name: TZ
value: America/New_York
- name: TA_USERNAME
valueFrom:
secretKeyRef:
key: TA_USERNAME
name: tubearchivist-secrets
- name: TA_PASSWORD
valueFrom:
secretKeyRef:
key: TA_PASSWORD
name: tubearchivist-secrets
- name: ELASTIC_PASSWORD
valueFrom:
secretKeyRef:
key: ELASTIC_PASSWORD
name: tubearchivist-secrets
image: bbilly1/tubearchivist:v0.5.12@sha256:ba1c846ddd0c6fdd0f040727129d2466e03b7a0c26499223839d450c7586ac09
livenessProbe:
exec:
command:
- sh
- -c
- |
code=$(curl -k -s -o /dev/null -w "%{http_code}" http://localhost:8000)
[ "$code" -eq 200 ] || [ "$code" -eq 401 ]
failureThreshold: 5
initialDelaySeconds: 15
periodSeconds: 5
successThreshold: 1
timeoutSeconds: 2
name: tubearchivist
ports:
- containerPort: 8000
name: http
readinessProbe:
exec:
command:
- sh
- -c
- |
code=$(curl -k -s -o /dev/null -w "%{http_code}" http://localhost:8000)
[ "$code" -eq 200 ] || [ "$code" -eq 401 ]
failureThreshold: 5
initialDelaySeconds: 15
periodSeconds: 5
successThreshold: 1
timeoutSeconds: 2
resources:
limits:
memory: 2Gi
requests:
cpu: 50m
memory: 1Gi
volumeMounts:
- mountPath: /youtube
name: videos
subPath: youtube
- mountPath: /cache/download
name: downloads
subPath: tube-archivist
- mountPath: /cache
name: cache
priorityClassName: tier-3
volumes:
- name: videos
persistentVolumeClaim:
claimName: tube-archivist-truenas-nfs-videos
- name: downloads
persistentVolumeClaim:
claimName: tube-archivist-truenas-nfs-downloads
- name: cache
persistentVolumeClaim:
claimName: cache
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
annotations:
kustomize.toolkit.fluxcd.io/force: enabled
name: archivist-es
namespace: tube-archivist
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: elasticsearch
serviceName: archivist-es
template:
metadata:
labels:
app.kubernetes.io/name: elasticsearch
app.kubernetes.io/part-of: tube-archivist
spec:
automountServiceAccountToken: false
containers:
- env:
- name: ELASTIC_PASSWORD
valueFrom:
secretKeyRef:
key: ELASTIC_PASSWORD
name: tubearchivist-secrets
- name: ES_JAVA_OPTS
value: -Xms1g -Xmx1g
- name: xpack.security.enabled
value: 'true'
- name: discovery.type
value: single-node
- name: path.repo
value: /usr/share/elasticsearch/data/snapshot
image: bbilly1/tubearchivist-es:8.19.0@sha256:9da63fb1973ec3d57daf6916be948eddd0d8a404cc8e447c938480c85fe2c554
imagePullPolicy: IfNotPresent
livenessProbe:
exec:
command:
- sh
- -c
- |
code=$(curl -k -s -o /dev/null -w "%{http_code}" http://localhost:9200)
[ "$code" -eq 200 ] || [ "$code" -eq 401 ]
initialDelaySeconds: 60
periodSeconds: 20
name: es
ports:
- containerPort: 9200
name: http
readinessProbe:
exec:
command:
- sh
- -c
- |
code=$(curl -k -s -o /dev/null -w "%{http_code}" http://localhost:9200)
[ "$code" -eq 200 ] || [ "$code" -eq 401 ]
initialDelaySeconds: 60
periodSeconds: 20
resources:
limits:
memory: 3Gi
requests:
cpu: 25m
memory: 2Gi
volumeMounts:
- mountPath: /usr/share/elasticsearch/data
name: es-data
priorityClassName: tier-3
securityContext:
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
runAsGroup: 1000
runAsUser: 1000
volumeClaimTemplates:
- metadata:
annotations:
k8up.io/backup: 'true'
name: es-data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
storageClassName: longhorn-encrypted
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
annotations:
kustomize.toolkit.fluxcd.io/force: enabled
name: valkey
namespace: tube-archivist
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: valkey
serviceName: valkey
template:
metadata:
labels:
app.kubernetes.io/name: valkey
spec:
containers:
- args:
- valkey-server
image: valkey/valkey:9.1.2-alpine@sha256:48332870af354a799964c0012ae1194a0bf2bf894eb508f945810596dc2d8d11
livenessProbe:
initialDelaySeconds: 10
periodSeconds: 10
tcpSocket:
port: 6379
name: valkey
ports:
- containerPort: 6379
name: client
readinessProbe:
initialDelaySeconds: 3
periodSeconds: 5
tcpSocket:
port: 6379
resources:
limits:
memory: 512Mi
requests:
cpu: 50m
memory: 128Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
privileged: false
volumeMounts:
- mountPath: /conf
name: conf
- mountPath: /data
name: data
priorityClassName: tier-3
securityContext:
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
runAsGroup: 1000
runAsNonRoot: true
runAsUser: 999
seccompProfile:
type: RuntimeDefault
volumes:
- emptyDir: {}
name: conf
- emptyDir: {}
name: data