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
| Tool | Open Source | Free Tier | Monthly Cost |
|---|---|---|---|
| YouTube Premium | No | No | From $13.99/mo |
Self Hosted
| Tool | Open Source | Full Features | Notes |
|---|---|---|---|
| yt-dlp | Yes | Yes | CLI only; no web UI or indexing |
| Invidious | Yes | Yes | Proxies 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_HOSTfrom 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
- 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