commit 84bcf8d1679ec9aecd3e014fdb9f45b9e2cc1444
parent 869b7d75ab1cf2750db2f505c706f0d052d2c1b5
Author: MTRNord <MTRNord@users.noreply.github.com>
Date: Sun, 1 Feb 2026 18:54:35 +0100
Set up peertube (hopefully lol)
Signed-off-by: MTRNord <MTRNord@users.noreply.github.com>
Diffstat:
6 files changed, 737 insertions(+), 0 deletions(-)
diff --git a/apps/talos_cluster/kustomization.yaml b/apps/talos_cluster/kustomization.yaml
@@ -32,3 +32,4 @@ resources:
- ./immich
- ./convex
- ./clickstack
+ - ./peertube
diff --git a/apps/talos_cluster/peertube/deployment.yaml b/apps/talos_cluster/peertube/deployment.yaml
@@ -0,0 +1,342 @@
+# Redis deployment for PeerTube
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: peertube-redis
+ namespace: peertube
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: peertube-redis
+ template:
+ metadata:
+ labels:
+ app: peertube-redis
+ spec:
+ containers:
+ - name: redis
+ image: redis:8-alpine
+ ports:
+ - containerPort: 6379
+ name: redis
+ volumeMounts:
+ - name: redis-data
+ mountPath: /data
+ resources:
+ requests:
+ memory: "128Mi"
+ cpu: "100m"
+ limits:
+ memory: "256Mi"
+ volumes:
+ - name: redis-data
+ emptyDir:
+ sizeLimit: 1Gi
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: peertube-redis
+ namespace: peertube
+spec:
+ selector:
+ app: peertube-redis
+ ports:
+ - name: redis
+ port: 6379
+ targetPort: 6379
+---
+# PeerTube main deployment with nginx sidecar
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: peertube
+ namespace: peertube
+spec:
+ replicas: 1
+ strategy:
+ type: Recreate
+ selector:
+ matchLabels:
+ app: peertube
+ template:
+ metadata:
+ labels:
+ app: peertube
+ spec:
+ containers:
+ # Nginx sidecar for S3 caching and static content
+ - name: nginx
+ image: nginx:1.27-alpine
+ ports:
+ - containerPort: 8080
+ name: http
+ protocol: TCP
+ env:
+ - name: S3_ENDPOINT
+ valueFrom:
+ secretKeyRef:
+ name: peertube-secrets
+ key: S3_ENDPOINT
+ - name: S3_HOST
+ valueFrom:
+ secretKeyRef:
+ name: peertube-secrets
+ key: S3_HOST
+ volumeMounts:
+ - name: nginx-config
+ mountPath: /etc/nginx/templates/default.conf.template
+ subPath: peertube.conf
+ readOnly: true
+ - name: nginx-cache
+ mountPath: /var/cache/nginx
+ resources:
+ requests:
+ memory: "128Mi"
+ cpu: "100m"
+ limits:
+ memory: "512Mi"
+ livenessProbe:
+ httpGet:
+ path: /api/v1/ping
+ port: http
+ initialDelaySeconds: 60
+ periodSeconds: 30
+ timeoutSeconds: 10
+ readinessProbe:
+ httpGet:
+ path: /api/v1/ping
+ port: http
+ initialDelaySeconds: 30
+ periodSeconds: 10
+ timeoutSeconds: 5
+
+ # PeerTube application
+ - name: peertube
+ image: chocobozzz/peertube:production-bookworm
+ ports:
+ - containerPort: 9000
+ name: peertube
+ protocol: TCP
+ - containerPort: 1935
+ name: rtmp
+ protocol: TCP
+ env:
+ # Database configuration (using CNPG cluster)
+ - name: PEERTUBE_DB_HOSTNAME
+ value: "pg-cluster-v2-rw.postgres-cluster.svc.cluster.local"
+ - name: PEERTUBE_DB_USERNAME
+ value: "peertube"
+ - name: PEERTUBE_DB_PASSWORD
+ valueFrom:
+ secretKeyRef:
+ name: peertube-secrets
+ key: POSTGRES_PASSWORD
+ - name: PEERTUBE_DB_NAME
+ value: "peertube"
+ - name: PEERTUBE_DB_SSL
+ value: "false"
+ # Redis configuration
+ - name: PEERTUBE_REDIS_HOSTNAME
+ value: "peertube-redis"
+ - name: PEERTUBE_REDIS_PORT
+ value: "6379"
+ # Webserver configuration
+ - name: PEERTUBE_WEBSERVER_HOSTNAME
+ value: "peertube.mtrnord.blog"
+ - name: PEERTUBE_WEBSERVER_PORT
+ value: "443"
+ - name: PEERTUBE_WEBSERVER_HTTPS
+ value: "true"
+ # Trust proxy (envoy gateway and nginx sidecar)
+ - name: PEERTUBE_TRUST_PROXY
+ value: '["127.0.0.1", "loopback", "10.0.0.0/8", "100.64.0.0/10"]'
+ # Secret
+ - name: PEERTUBE_SECRET
+ valueFrom:
+ secretKeyRef:
+ name: peertube-secrets
+ key: PEERTUBE_SECRET
+ # Email configuration (using existing mailserver)
+ - name: PEERTUBE_SMTP_HOSTNAME
+ value: "mail.midnightthoughts.space"
+ - name: PEERTUBE_SMTP_PORT
+ value: "465"
+ - name: PEERTUBE_SMTP_USERNAME
+ valueFrom:
+ secretKeyRef:
+ name: peertube-secrets
+ key: SMTP_USERNAME
+ - name: PEERTUBE_SMTP_PASSWORD
+ valueFrom:
+ secretKeyRef:
+ name: peertube-secrets
+ key: SMTP_PASSWORD
+ - name: PEERTUBE_SMTP_FROM
+ value: "noreply@peertube.mtrnord.blog"
+ - name: PEERTUBE_SMTP_TLS
+ value: "true"
+ - name: PEERTUBE_SMTP_DISABLE_STARTTLS
+ value: "true"
+ - name: PEERTUBE_ADMIN_EMAIL
+ value: "support@midnightthoughts.space"
+ # S3 Object Storage Configuration (Hetzner)
+ - name: PEERTUBE_OBJECT_STORAGE_ENABLED
+ value: "true"
+ - name: PEERTUBE_OBJECT_STORAGE_ENDPOINT
+ valueFrom:
+ secretKeyRef:
+ name: peertube-secrets
+ key: S3_ENDPOINT
+ - name: PEERTUBE_OBJECT_STORAGE_REGION
+ valueFrom:
+ secretKeyRef:
+ name: peertube-secrets
+ key: S3_REGION
+ - name: PEERTUBE_OBJECT_STORAGE_CREDENTIALS_ACCESS_KEY_ID
+ valueFrom:
+ secretKeyRef:
+ name: peertube-secrets
+ key: S3_ACCESS_KEY_ID
+ - name: PEERTUBE_OBJECT_STORAGE_CREDENTIALS_SECRET_ACCESS_KEY
+ valueFrom:
+ secretKeyRef:
+ name: peertube-secrets
+ key: S3_SECRET_ACCESS_KEY
+ # S3 Bucket Configuration - Web Videos
+ - name: PEERTUBE_OBJECT_STORAGE_WEB_VIDEOS_BUCKET_NAME
+ valueFrom:
+ secretKeyRef:
+ name: peertube-secrets
+ key: S3_BUCKET_NAME
+ - name: PEERTUBE_OBJECT_STORAGE_WEB_VIDEOS_PREFIX
+ value: "web-videos/"
+ - name: PEERTUBE_OBJECT_STORAGE_WEB_VIDEOS_BASE_URL
+ value: "https://peertube.mtrnord.blog/static/web-videos"
+ # S3 Bucket Configuration - Streaming Playlists (HLS)
+ - name: PEERTUBE_OBJECT_STORAGE_STREAMING_PLAYLISTS_BUCKET_NAME
+ valueFrom:
+ secretKeyRef:
+ name: peertube-secrets
+ key: S3_BUCKET_NAME
+ - name: PEERTUBE_OBJECT_STORAGE_STREAMING_PLAYLISTS_PREFIX
+ value: "streaming-playlists/"
+ - name: PEERTUBE_OBJECT_STORAGE_STREAMING_PLAYLISTS_BASE_URL
+ value: "https://peertube.mtrnord.blog/static/streaming-playlists"
+ # S3 Bucket Configuration - User Exports
+ - name: PEERTUBE_OBJECT_STORAGE_USER_EXPORTS_BUCKET_NAME
+ valueFrom:
+ secretKeyRef:
+ name: peertube-secrets
+ key: S3_BUCKET_NAME
+ - name: PEERTUBE_OBJECT_STORAGE_USER_EXPORTS_PREFIX
+ value: "user-exports/"
+ - name: PEERTUBE_OBJECT_STORAGE_USER_EXPORTS_BASE_URL
+ value: "https://peertube.mtrnord.blog/static/user-exports"
+ # S3 Bucket Configuration - Original Video Files
+ - name: PEERTUBE_OBJECT_STORAGE_ORIGINAL_VIDEO_FILES_BUCKET_NAME
+ valueFrom:
+ secretKeyRef:
+ name: peertube-secrets
+ key: S3_BUCKET_NAME
+ - name: PEERTUBE_OBJECT_STORAGE_ORIGINAL_VIDEO_FILES_PREFIX
+ value: "original-video-files/"
+ - name: PEERTUBE_OBJECT_STORAGE_ORIGINAL_VIDEO_FILES_BASE_URL
+ value: "https://peertube.mtrnord.blog/static/original-video-files"
+ # S3 Bucket Configuration - Captions
+ - name: PEERTUBE_OBJECT_STORAGE_CAPTIONS_BUCKET_NAME
+ valueFrom:
+ secretKeyRef:
+ name: peertube-secrets
+ key: S3_BUCKET_NAME
+ - name: PEERTUBE_OBJECT_STORAGE_CAPTIONS_PREFIX
+ value: "captions/"
+ - name: PEERTUBE_OBJECT_STORAGE_CAPTIONS_BASE_URL
+ value: "https://peertube.mtrnord.blog/static/captions"
+ # S3 ACL settings
+ - name: PEERTUBE_OBJECT_STORAGE_UPLOAD_ACL_PUBLIC
+ value: "public-read"
+ - name: PEERTUBE_OBJECT_STORAGE_UPLOAD_ACL_PRIVATE
+ value: "private"
+ volumeMounts:
+ - name: data
+ mountPath: /data
+ - name: config
+ mountPath: /config
+ resources:
+ requests:
+ memory: "512Mi"
+ cpu: "250m"
+ limits:
+ memory: "2Gi"
+ startupProbe:
+ httpGet:
+ path: /api/v1/ping
+ port: peertube
+ initialDelaySeconds: 30
+ periodSeconds: 10
+ failureThreshold: 30
+
+ volumes:
+ - name: data
+ persistentVolumeClaim:
+ claimName: peertube-data
+ - name: config
+ persistentVolumeClaim:
+ claimName: peertube-config
+ - name: nginx-config
+ configMap:
+ name: peertube-nginx
+ - name: nginx-cache
+ emptyDir:
+ sizeLimit: 15Gi
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: peertube
+ namespace: peertube
+spec:
+ selector:
+ app: peertube
+ ports:
+ - name: http
+ port: 8080
+ targetPort: 8080
+ - name: rtmp
+ port: 1935
+ targetPort: 1935
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+ name: peertube
+ namespace: peertube
+spec:
+ parentRefs:
+ - name: envoy-gateway
+ namespace: envoy-gateway
+ hostnames:
+ - peertube.mtrnord.blog
+ rules:
+ - backendRefs:
+ - name: peertube
+ port: 8080
+ timeouts:
+ request: 600s
+ backendRequest: 0s
+---
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: BackendTrafficPolicy
+metadata:
+ name: peertube-policy
+ namespace: peertube
+spec:
+ targetRef:
+ group: gateway.networking.k8s.io
+ kind: HTTPRoute
+ name: peertube
+ compression:
+ - type: Gzip
+ - type: Brotli
diff --git a/apps/talos_cluster/peertube/kustomization.yaml b/apps/talos_cluster/peertube/kustomization.yaml
@@ -0,0 +1,15 @@
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+namespace: peertube
+resources:
+ - secret.yaml
+ - pvc.yaml
+ - deployment.yaml
+configMapGenerator:
+ - name: peertube-nginx
+ namespace: peertube
+ files:
+ - peertube.conf=nginx/peertube.conf
+ options:
+ labels:
+ app: peertube
diff --git a/apps/talos_cluster/peertube/nginx/peertube.conf b/apps/talos_cluster/peertube/nginx/peertube.conf
@@ -0,0 +1,311 @@
+# PeerTube nginx configuration with S3 caching for Hetzner Object Storage
+# Based on official PeerTube S3 cache configuration
+
+# Cache paths for S3 content
+proxy_cache_path /var/cache/nginx/s3 levels=1:2 keys_zone=CACHE-S3:100m inactive=48h max_size=10G;
+proxy_cache_path /var/cache/nginx/s3-ts levels=1:2 keys_zone=CACHE-S3-TS:10m inactive=60s max_size=1G;
+
+upstream peertube_backend {
+ server 127.0.0.1:9000;
+}
+
+server {
+ listen 8080;
+ server_name _;
+
+ access_log /var/log/nginx/peertube.access.log;
+ error_log /var/log/nginx/peertube.error.log;
+
+ # Performance optimizations
+ tcp_nopush on;
+ tcp_nodelay on;
+ keepalive_timeout 30;
+ client_body_timeout 30s;
+ client_header_timeout 10s;
+ send_timeout 10s;
+ reset_timedout_connection on;
+ proxy_ignore_client_abort on;
+
+ # Gzip compression
+ gzip on;
+ gzip_vary on;
+ gzip_types text/css application/javascript font/truetype font/opentype application/vnd.ms-fontobject image/svg+xml application/xml;
+ gzip_min_length 1000;
+ gzip_buffers 16 8k;
+ gzip_comp_level 2;
+
+ ##
+ # PeerTube API and Application
+ ##
+
+ location @api {
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+
+ client_max_body_size 100k;
+
+ proxy_connect_timeout 10m;
+ proxy_send_timeout 10m;
+ proxy_read_timeout 10m;
+ send_timeout 10m;
+
+ proxy_pass http://peertube_backend;
+ }
+
+ location / {
+ try_files /dev/null @api;
+ }
+
+ # Resumable uploads
+ location ~ ^/api/v1/videos/(upload-resumable|([^/]+/source/replace-resumable))$ {
+ client_max_body_size 0;
+ proxy_request_buffering off;
+ try_files /dev/null @api;
+ }
+
+ location ~ ^/api/v1/users/[^/]+/imports/import-resumable$ {
+ client_max_body_size 0;
+ proxy_request_buffering off;
+ try_files /dev/null @api;
+ }
+
+ # Video uploads
+ location ~ ^/api/v1/videos/(upload|([^/]+/studio/edit))$ {
+ limit_except POST HEAD { deny all; }
+ client_max_body_size 12G;
+ add_header X-File-Maximum-Size 8G always;
+ proxy_request_buffering off;
+ try_files /dev/null @api;
+ }
+
+ # Runner job updates
+ location ~ ^/api/v1/runners/jobs/[^/]+/(update|success)$ {
+ client_max_body_size 0;
+ proxy_request_buffering off;
+ try_files /dev/null @api;
+ }
+
+ # Other media uploads
+ location ~ ^/api/v1/(videos|video-playlists|video-channels|users/me) {
+ client_max_body_size 12M;
+ add_header X-File-Maximum-Size 8M always;
+ try_files /dev/null @api;
+ }
+
+ ##
+ # WebSocket support
+ ##
+
+ location @api_websocket {
+ proxy_http_version 1.1;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+
+ proxy_pass http://peertube_backend;
+ }
+
+ location /socket.io {
+ try_files /dev/null @api_websocket;
+ }
+
+ location /tracker/socket {
+ proxy_read_timeout 15m;
+ try_files /dev/null @api_websocket;
+ }
+
+ location ~ ^/plugins/[^/]+(/[^/]+)?/ws/ {
+ try_files /dev/null @api_websocket;
+ }
+
+ ##
+ # S3 Cache Proxy - Main media files
+ # Configured for Hetzner Object Storage
+ ##
+
+ set $s3_backend '${S3_ENDPOINT}';
+ set $s3_host '${S3_HOST}';
+
+ # Cache S3 files for a long time (filenames change when content updates)
+ location @s3 {
+ limit_except GET OPTIONS {
+ deny all;
+ }
+
+ resolver 1.1.1.1 8.8.8.8 valid=300s;
+ resolver_timeout 10s;
+
+ proxy_set_header Host $s3_host;
+ proxy_set_header Connection '';
+ proxy_set_header Authorization '';
+ proxy_set_header Range $slice_range;
+ proxy_hide_header Set-Cookie;
+ proxy_hide_header 'Access-Control-Allow-Origin';
+ proxy_hide_header 'Access-Control-Allow-Methods';
+ proxy_hide_header 'Access-Control-Allow-Headers';
+ proxy_hide_header x-amz-id-2;
+ proxy_hide_header x-amz-request-id;
+ proxy_hide_header x-amz-meta-server-side-encryption;
+ proxy_hide_header x-amz-server-side-encryption;
+ proxy_hide_header x-amz-bucket-region;
+ proxy_hide_header x-amzn-requestid;
+ proxy_ignore_headers Set-Cookie;
+ proxy_pass $s3_backend$uri;
+ proxy_intercept_errors off;
+
+ proxy_cache CACHE-S3;
+ proxy_cache_valid 200 206 48h;
+ proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
+ slice 1m;
+ proxy_cache_key $host$uri$is_args$args$slice_range;
+ proxy_http_version 1.1;
+
+ expires 1y;
+ add_header Cache-Control public;
+ add_header 'Access-Control-Allow-Origin' '*';
+ add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS';
+ add_header 'Access-Control-Allow-Headers' 'Range,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
+ add_header X-Cache-Status $upstream_cache_status;
+ add_header X-Content-Type-Options nosniff;
+ add_header Content-Security-Policy "default-src 'none'; form-action 'none'";
+ }
+
+ # .ts files are live fragments - cache briefly
+ location @s3-ts {
+ limit_except GET OPTIONS {
+ deny all;
+ }
+
+ resolver 1.1.1.1 8.8.8.8 valid=300s;
+ resolver_timeout 10s;
+
+ proxy_set_header Host $s3_host;
+ proxy_set_header Connection '';
+ proxy_set_header Authorization '';
+ proxy_set_header Range $slice_range;
+ proxy_hide_header Set-Cookie;
+ proxy_hide_header 'Access-Control-Allow-Origin';
+ proxy_hide_header 'Access-Control-Allow-Methods';
+ proxy_hide_header 'Access-Control-Allow-Headers';
+ proxy_hide_header x-amz-id-2;
+ proxy_hide_header x-amz-request-id;
+ proxy_hide_header x-amz-meta-server-side-encryption;
+ proxy_hide_header x-amz-server-side-encryption;
+ proxy_hide_header x-amz-bucket-region;
+ proxy_hide_header x-amzn-requestid;
+ proxy_ignore_headers Set-Cookie;
+ proxy_pass $s3_backend$uri;
+ proxy_intercept_errors off;
+
+ proxy_cache CACHE-S3-TS;
+ proxy_cache_valid 200 206 2m;
+ proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
+ slice 1m;
+ proxy_cache_key $host$uri$is_args$args$slice_range;
+ proxy_http_version 1.1;
+
+ expires 1y;
+ add_header Cache-Control public;
+ add_header 'Access-Control-Allow-Origin' '*';
+ add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS';
+ add_header 'Access-Control-Allow-Headers' 'Range,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
+ add_header X-Cache-Status $upstream_cache_status;
+ add_header X-Content-Type-Options nosniff;
+ add_header Content-Security-Policy "default-src 'none'; form-action 'none'";
+ }
+
+ # M3U8 and JSON files for live videos - don't cache
+ location @s3_nocache {
+ limit_except GET OPTIONS {
+ deny all;
+ }
+
+ resolver 1.1.1.1 8.8.8.8 valid=300s;
+ resolver_timeout 10s;
+
+ proxy_set_header Host $s3_host;
+ proxy_set_header Connection '';
+ proxy_set_header Authorization '';
+ proxy_set_header Range $http_range;
+ proxy_hide_header Set-Cookie;
+ proxy_hide_header 'Access-Control-Allow-Origin';
+ proxy_hide_header 'Access-Control-Allow-Methods';
+ proxy_hide_header 'Access-Control-Allow-Headers';
+ proxy_hide_header x-amz-id-2;
+ proxy_hide_header x-amz-request-id;
+ proxy_hide_header x-amz-meta-server-side-encryption;
+ proxy_hide_header x-amz-server-side-encryption;
+ proxy_hide_header x-amz-bucket-region;
+ proxy_hide_header x-amzn-requestid;
+ proxy_ignore_headers Set-Cookie;
+ proxy_pass $s3_backend$uri;
+ proxy_intercept_errors off;
+
+ expires 0;
+ proxy_cache off;
+
+ add_header 'Access-Control-Allow-Origin' '*';
+ add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS';
+ add_header 'Access-Control-Allow-Headers' 'Range,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
+ add_header X-Cache-Status $upstream_cache_status;
+ add_header X-Content-Type-Options nosniff;
+ add_header Content-Security-Policy "default-src 'none'; form-action 'none'";
+ }
+
+ ##
+ # Static media routing to S3 cache
+ ##
+
+ # Private content goes through PeerTube for auth
+ location ~ ^(/static/(webseed|web-videos|streaming-playlists/hls)/private/)|^/download {
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_limit_rate 5M;
+ proxy_pass http://peertube_backend;
+ }
+
+ # Public static content - route to S3 cache
+ location ~ ^/static/(webseed|web-videos|redundancy|streaming-playlists)/ {
+ limit_rate_after 5M;
+ set $peertube_limit_rate 5M;
+ limit_rate $peertube_limit_rate;
+
+ if ($request_method = 'OPTIONS') {
+ add_header Access-Control-Allow-Origin '*';
+ add_header Access-Control-Allow-Methods 'GET, OPTIONS';
+ add_header Access-Control-Allow-Headers 'Range,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
+ add_header Access-Control-Max-Age 1728000;
+ add_header Content-Type 'text/plain charset=UTF-8';
+ add_header Content-Length 0;
+ return 204;
+ }
+
+ if ($request_method = 'GET') {
+ add_header Access-Control-Allow-Origin '*';
+ add_header Access-Control-Allow-Methods 'GET, OPTIONS';
+ add_header Access-Control-Allow-Headers 'Range,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
+ }
+
+ # Rewrite paths for S3
+ rewrite ^/static/webseed/(.*)$ /web-videos/$1 break;
+ rewrite ^/static/(.*)$ /$1 break;
+
+ try_files $uri @s3;
+ }
+
+ # .ts files for live streaming
+ location ~ ^/static/.*\.ts$ {
+ rewrite ^/static/(.*)$ /$1 break;
+ try_files $uri @s3-ts;
+ }
+
+ # M3U8 and JSON files - no cache
+ location ~ ^/static/.*\.(json|m3u8)$ {
+ rewrite ^/static/(.*)$ /$1 break;
+ try_files $uri @s3_nocache;
+ }
+}
diff --git a/apps/talos_cluster/peertube/pvc.yaml b/apps/talos_cluster/peertube/pvc.yaml
@@ -0,0 +1,25 @@
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: peertube-data
+ namespace: peertube
+spec:
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 10Gi
+ storageClassName: longhorn
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: peertube-config
+ namespace: peertube
+spec:
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 1Gi
+ storageClassName: longhorn
diff --git a/apps/talos_cluster/peertube/secret.yaml b/apps/talos_cluster/peertube/secret.yaml
@@ -0,0 +1,43 @@
+apiVersion: v1
+kind: Secret
+metadata:
+ name: peertube-secrets
+ namespace: peertube
+type: Opaque
+stringData:
+ #ENC[AES256_GCM,data:x5GTGfLM1X2OTh4ROERmyfPo6MfDxPrVIlekqoVxf5RtEvE8brA60uO9PlyhHCerUWFmAFb9,iv:Z1cu5L9Xot0EjDh8Ca2DgncHEGdhNT+9D+VEQH1oEHE=,tag:WO5M2G1ALwXtcIJB3PJkaQ==,type:comment]
+ PEERTUBE_SECRET: ENC[AES256_GCM,data:2AOadpq3ArxzzMcOFpWJMnCV23tKvIT9S6zbwu4P0ZvXClTLx+fHueN0e1atXW6GEptMDAGOkQjbN8pyFMCt/Q==,iv:GuOVzJWe1koB8SM/2syO5oRNO7arjlxNdhYqwooLNHg=,tag:aigMJZnD0/PhITTmbneBfA==,type:str]
+ #ENC[AES256_GCM,data:0krcszGeYWC3/0c5KMgfLA6IALJLRBk4T4oa93AzcNW4z9M7kox0h28uWN7SmVt63cb2U5fRvXXmyh8tKVIcISTV9Dqxxo7J19yHdss=,iv:fHQqbO0ZGCVxO/5g9pMBZ31sro5PZ+p1L9NKjDFHME4=,tag:bEdPLPM1L9FPIXk66Yc81w==,type:comment]
+ POSTGRES_PASSWORD: ENC[AES256_GCM,data:5YeR3Uu+ODd96qnmroN9S+VC2Ie/Kbiq8k5xupXnEzCYxDYvQrg3xKU/xxBk4NzQ6GfpB8cUcNrve7E9Srl9,iv:x7XiiqyopyXHbZT27+YHVd6Q+WmCw7pmhIoWSuweKnU=,tag:aa6AaBfVOJH/6vyWkXy3Tw==,type:str]
+ #ENC[AES256_GCM,data:SMTQUokPK3kx+ZNkLI8iLK2bBrzE0a+3Co0aKX5afV/fYfcEx6h3JQE3,iv:CL+0HFMJv8RTkwkh7sWf9GeS+q4rhqifTsnRd5zwO3M=,tag:38Y2uvmxavd7hAr/0U+Rxw==,type:comment]
+ SMTP_USERNAME: ENC[AES256_GCM,data:afePsswXPFK0tsKGkfAaS9lEOvpfryo5/o8SMhI1,iv:xr6g18NWPzVnAbAHkD8bBZh+XueSMwPpIbWT0P4/O1Y=,tag:mdnl02VaUE72RmySfC7lWg==,type:str]
+ SMTP_PASSWORD: ENC[AES256_GCM,data:U8aWsBZMcD2Ec9n0BIGi42odB9qpj6beL/6Hq5hBL6JxiKDtI1vSlrwdoBtn954gsON+Xu2IaWpKXcVijr7vPA==,iv:QfTzIl3JCrGzNyuJdDGV9BWY1cj1Fp5zltgHXOxy9qU=,tag:4t5XlnK2OSNKY/G88ah9JA==,type:str]
+ #ENC[AES256_GCM,data:qkQxHNk1f7LxXuNCeweTDrOT5bh0nQ13ufHucKxW9cf2Sa8X/Nal3A==,iv:cg8fXasy261RfLrVvneQ3rVxfh95EBpOh9Naxemp90I=,tag:KvuVIN9je48jsKHt/W21GQ==,type:comment]
+ #ENC[AES256_GCM,data:t/JqW5WvfXoH+ZjAdvjO4SXTl0ggSFrZfEhrwcI5xdfE6qQrpIIfxVoivN5VDvin02Hy3P9jDy8Gj9Gq6d3ydQ==,iv:zlSOPWVV8nlP+hYENf8VB6iM1S7DXW/k0dDm3iGIcNE=,tag:zOOzKDxTQfkTXp8LKMSwMQ==,type:comment]
+ S3_ENDPOINT: ENC[AES256_GCM,data:8V7LcQHkRisqLpcrTJecoRKE5aJdaaYMvoA+/qo89zbGTQQ=,iv:IKkVAuAc36aKS2EblED+0uICM589IHIL4+NgEC9Fev4=,tag:6vU6zRxnH6Uz94eEi4gJtA==,type:str]
+ #ENC[AES256_GCM,data:fLgSFxHMpUxVVAR8BFgS698VTXR+YYDxIpAvEviNOTYZmo2tY27dOrToBHnrgQ4h/pPf0mXw8AUcLyigtLI=,iv:Mzdm1b5OFQIOFk47pdWzu++YY2/F5wjfutUrCVqMHXM=,tag:jfuRPiVp/j+Aj+kFT289Bg==,type:comment]
+ S3_HOST: ENC[AES256_GCM,data:3hw2lRBbDg+gOrx9i0E29Dms4Ft7DNIs9djvijj1bWqb4WSemhwgks2EvRo=,iv:A8R9yEldwsALAEWwBjTonlzSkKkqwOCPHlJYsKvhqyI=,tag:x3jzKkHonHf5DcKl7UkcSQ==,type:str]
+ S3_REGION: ENC[AES256_GCM,data:JCcxfA==,iv:611n91/fxaKF+6l/c+sPOF/iQpUYmBob0lT6LzhqPVQ=,tag:iAn0Z5nWWHdc8u9PooCGsA==,type:str]
+ S3_BUCKET_NAME: ENC[AES256_GCM,data:7EY7t9cPlREmVUHn4u/2Vg==,iv:oYos47/HI6M6umx81LEMYaBk0kV5IqbU5AuuW1/5gBg=,tag:YbQJnUXYssc4p8ex7YRfkA==,type:str]
+ S3_ACCESS_KEY_ID: ENC[AES256_GCM,data:0wSX9rDvDyAAhVZkkSEHjG7kTds=,iv:NnuuP5uh8Ox2nnbW9utt+k0M4dXqyZIN58jldsNmPds=,tag:/bzMM6y4fWkybjyEQdh5YA==,type:str]
+ S3_SECRET_ACCESS_KEY: ENC[AES256_GCM,data:aNivAmpg0C/dt+XA8l6rmnPdDOxFuOvWUF1LcUcMepR1OsUnLIqLqQ==,iv:scUeY1wCUazQ6dCY4FPdg48gSsRfI+BpWLzuo8H9M38=,tag:X08Et2ovTw5EkOjO+baZMA==,type:str]
+sops:
+ kms: []
+ gcp_kms: []
+ azure_kv: []
+ hc_vault: []
+ age:
+ - recipient: age1esjyg2qfy49awv0ptkzvpk425adczjr38m37w2mmcahzc4p8n54sll2nzh
+ enc: |
+ -----BEGIN AGE ENCRYPTED FILE-----
+ YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBicmJZQWVZRFlZNkQwamlw
+ TXBDMjdxWjdpOS9YWEU0OFJhR1JUN3VsZG5RCnc0cG13UUt1ZXpJbHdaUDVlNVRN
+ dEdGOWhpNXlSSGU4QVkvMGhicWMyaHcKLS0tIDh4QSsvM0lqOThGc2l4NmM0ZU9Y
+ MFlKYnhuYjVMU29ES05rM294cUN5TWcKzjgLbVM6baupM7DwELuofeAlXtltL68A
+ BCyn+w1vzj3CmhVSEx+u8dshlpF1u8NeZe1m20BEDYChg/eNoGFHWg==
+ -----END AGE ENCRYPTED FILE-----
+ lastmodified: "2026-02-01T17:36:57Z"
+ mac: ENC[AES256_GCM,data:HGvlYWK9VbvmHh/SfruCX/ZiV8V8QS6e+KzrsAnnkGrEYckPbe/IB7NF3RJmDK/fb/B+Y6y1qdrXMygXax6y3tiUyu42H/97Kj6mjDFzTjO0TWbodK68vmLrLM2775IDdz2WMAV0cq03ddVUPkIHI6roRI0QrA1AZAk4xyw2Feg=,iv:eS5MwiWuyottGkqlJOcZtLeWHjnqwOxUg/M3wG58ZpY=,tag:SwFFGZ2rBnuQIEWot+2JPA==,type:str]
+ pgp: []
+ encrypted_regex: ^(apiKey|appUserPassword|otelUserPassword|harborAdminPassword|totpVaultKey|kimaiAppSecret|kimaiAdminPassword|GITHUB_CLIENT_ID|GITHUB_CLIENT_SECRET|GITHUB_PRIVATE_KEY|woosh|root_password|rspamd_password|pgdb_password|matrix_access_token|pgdb_remote_url|hmac_secret_key|adminPassword|adminEmail|jenkinsAdminEmail|securityRealm|gerrit.config|routing_key|DATABASE_URL|SMTP_PASSWORD|SECRET_KEY_BASE|admin_password|extraCommands|key|clickhouseDatabaseURL|databaseURL|client_id|client_secret|secret_key_base|otp_secret|private_key|public_key|primaryKey|deterministicKey|keyDerivationSalt|token|clientId|secretKey|installationId|installationKey|uriOverride|adminToken.value|password.value|sql_password|erlangCookie|AUTHENTICATION_PASSWORD|ROOM_API_SECRET_KEY|adminPassword|configPassword|adminUser|configUser|MAIL_PASSWORD|APP_KEY|api_key|api_secret|keys|livekit_key|livekit_secret|secret_key|admin_pass|admin_email|mariadbPassword|mariadbRootPassword|privateKey|data|stringData|PASSWD|password|pass|postgresPassword|postgresqlPassword|redminePassword|smtpPassword|registration_shared_secret|shared_secret|secret|admin_token|integrationKey|rootPassword|adminPassword|adminUser|adminEmail|emailPassword|secretKey|appId|clientSecret|webhookSecret)$
+ version: 3.9.1