Upload files to "/"

This commit is contained in:
2026-08-19 10:11:18 +00:00
parent 8b1f4d8736
commit 6050ef3433

603
jellyfin-kubernetes-sop.md Normal file
View File

@@ -0,0 +1,603 @@
# Jellyfin on Kubernetes --- Deployment & Operations SOP
> **Scope:** Deploy Jellyfin with Helm on Kubernetes, pin it to the
> dedicated storage node, persist config/cache on Longhorn, expose it
> through NodePort/Nginx Proxy Manager, and upload media through a
> multi-hop SSH path.
## 1. Final Architecture
``` text
User / Browser
|
| HTTPS: media.jutsugen.top
v
Nginx Proxy Manager
|
| HTTP -> Kubernetes NodeIP:31096
v
NodePort Service
|
v
Jellyfin Pod :8096
|
+-- /config -> Longhorn PVC (5Gi)
+-- /cache -> Longhorn PVC (10Gi)
+-- /media -> storage-node hostPath (read-only)
+-- /downloads -> storage-node hostPath
```
Dedicated node:
``` text
kurasuta-storage-vm-exp
Internal IP: 10.20.30.32
Label: kubernetes.io/hostname=kurasuta-storage-vm-exp
Taint: storage=true:NoSchedule
```
## 2. Prerequisites
- Working Kubernetes cluster and Helm.
- Longhorn StorageClass available (`longhorn`).
- Storage node labelled and tainted as shown above.
- Nginx Proxy Manager can reach a Kubernetes node IP.
- DNS record for `media.jutsugen.top` points to the NPM entry point.
- A `jellyfin` Linux user exists on the storage node with UID/GID
`1001:1001`.
## 3. Storage Node Directories
On `kurasuta-storage-vm-exp`, use:
``` text
/home/jellyfin/jellyfin/
├── config/
├── downloads/
└── media/
├── movies/
└── series/
```
Verify identity and directory ownership:
``` bash
id jellyfin
ls -ld /home/jellyfin/jellyfin /home/jellyfin/jellyfin/*
```
Expected Jellyfin identity:
``` text
uid=1001(jellyfin) gid=1001(jellyfin)
```
Host media/download directories should be accessible to that user.
## 4. Storage Design
Use the following split:
-----------------------------------------------------------------------
Container path Backend Purpose
----------------------- ----------------------- -----------------------
`/config` Longhorn PVC, 5Gi Jellyfin DB, users,
settings, metadata
state
`/cache` Longhorn PVC, 10Gi Cache/transcoding
working data
`/media` hostPath Movies and series
`/downloads` hostPath Downloader staging area
-----------------------------------------------------------------------
Do **not** define duplicate `config` or `media` volumes. The chart's
built-in persistence configuration and custom volumes must not produce
two volumes with the same name or two mounts on the same path.
## 5. Pod Security Context
The official Jellyfin image is used:
``` text
docker.io/jellyfin/jellyfin:latest
```
Run Jellyfin as the storage user's UID/GID:
``` yaml
podSecurityContext:
runAsUser: 1001
runAsGroup: 1001
fsGroup: 1001
```
`fsGroup: 1001` is important for writable Longhorn PVCs. Without it,
Jellyfin may fail with an error similar to:
``` text
System.UnauthorizedAccessException: Access to the path '/config/log' is denied.
Permission denied
```
## 6. Scheduling on the Storage Node
Pin Jellyfin to the storage node:
``` yaml
nodeSelector:
kubernetes.io/hostname: kurasuta-storage-vm-exp
```
The node has this taint:
``` text
storage=true:NoSchedule
```
Therefore the pod requires a matching toleration:
``` yaml
tolerations:
- key: storage
operator: Equal
value: "true"
effect: NoSchedule
```
Without this toleration, the node selector can match but the pod will
remain unschedulable.
## 7. Media and Download hostPaths
The final hostPath mapping is:
``` yaml
volumes:
- name: media
hostPath:
path: /home/jellyfin/jellyfin/media
type: Directory
- name: downloads
hostPath:
path: /home/jellyfin/jellyfin/downloads
```
Container mounts:
``` yaml
volumeMounts:
- name: media
mountPath: /media
readOnly: true
- name: downloads
mountPath: /downloads
```
Keeping `/media` read-only protects source media from accidental
modification/deletion by Jellyfin.
## 8. Network Exposure
Kubernetes Ingress is not required because Nginx Proxy Manager is the
external reverse proxy.
Expose Jellyfin as NodePort:
``` text
Service type: NodePort
Service port: 8096
NodePort: 31096
```
Expected service:
``` text
jellyfin NodePort <ClusterIP> <none> 8096:31096/TCP
```
Traffic path:
``` text
NPM -> http://10.20.30.32:31096 -> Service -> Jellyfin Pod:8096
```
Do not use HTTPS directly against port `31096`; this backend is HTTP.
TLS is terminated by Nginx Proxy Manager.
## 9. Helm Validation Before Deployment
From the chart directory, validate values/chart syntax:
``` bash
helm lint -f override_values.yaml .
```
Expected:
``` text
1 chart(s) linted, 0 chart(s) failed
```
Render the chart before installation and inspect it:
``` bash
helm template jellyfin -f override_values.yaml . > jellyfin.yaml
```
Verify the rendered Deployment contains:
``` text
image: docker.io/jellyfin/jellyfin:latest
runAsUser: 1001
runAsGroup: 1001
fsGroup: 1001
/config -> jellyfin-config PVC
/cache -> jellyfin-cache PVC
/media -> /home/jellyfin/jellyfin/media
/downloads -> /home/jellyfin/jellyfin/downloads
nodeSelector -> kurasuta-storage-vm-exp
toleration -> storage=true:NoSchedule
```
Also verify there are no duplicate `config`/`media` volume names or
mount paths.
## 10. Deploy Jellyfin
Create the namespace:
``` bash
kubectl create namespace jellyfin
```
Install/upgrade the release:
``` bash
helm upgrade --install jellyfin \
--namespace jellyfin \
-f override_values.yaml \
.
```
Helm release status must include the namespace:
``` bash
helm status jellyfin --namespace jellyfin
```
A Helm status of `deployed` only confirms resource submission; always
verify the pod separately.
## 11. Runtime Verification
Check the pod:
``` bash
kubectl get pods -n jellyfin -o wide
```
Expected:
``` text
READY STATUS RESTARTS
1/1 Running 0
```
Confirm it is scheduled on:
``` text
kurasuta-storage-vm-exp
```
Check PVCs:
``` bash
kubectl get pvc -n jellyfin
```
Both config and cache claims should be `Bound`.
If the pod crashes:
``` bash
kubectl describe pod -n jellyfin <pod-name>
kubectl logs -n jellyfin <pod-name>
```
The permission issue encountered during this deployment was fixed by
ensuring the Longhorn volume had appropriate group access through the
pod security context (`fsGroup`).
## 12. Service and NodePort Verification
Check Service:
``` bash
kubectl get svc -n jellyfin
```
Check EndpointSlice:
``` bash
kubectl get endpointslices.discovery.k8s.io -n jellyfin
```
The Jellyfin endpoint should point to the pod on port `8096`.
Test ClusterIP:
``` bash
curl -v --max-time 5 http://<JELLYFIN_CLUSTER_IP>:8096/
```
Test NodePort:
``` bash
curl -v --max-time 5 http://10.20.30.32:31096/
```
A successful Jellyfin root request can return:
``` text
HTTP/1.1 302 Found
Server: Kestrel
Location: web/
```
NodePort was also verified through the other cluster node IPs,
confirming kube-proxy forwarding was functional.
## 13. kube-proxy Notes
The cluster's kube-proxy reported:
``` text
Using iptables proxy
Using iptables Proxier
```
The ConfigMap had `mode: ""`, but runtime logs confirmed iptables mode.
Historical kube-proxy logs also contained RBAC `forbidden` errors for
watching Services, EndpointSlices, Nodes, and ServiceCIDRs. Because
current ClusterIP and NodePort forwarding worked, these errors were not
the Jellyfin blocker, but they should be audited separately as cluster
maintenance.
## 14. Nginx Proxy Manager
Create a Proxy Host with:
``` text
Domain: media.jutsugen.top
Scheme: http
Forward IP: 10.20.30.32
Forward Port: 31096
WebSockets: enabled
```
First verify HTTP proxying. After the dashboard works through the
domain, configure the SSL certificate in NPM and force HTTPS as
appropriate.
Final path:
``` text
https://media.jutsugen.top
|
v
Nginx Proxy Manager (TLS termination)
|
| HTTP
v
10.20.30.32:31096
|
v
Jellyfin
```
## 15. Jellyfin Library Layout
Recommended host layout:
``` text
/home/jellyfin/jellyfin/media/
├── movies/
│ ├── Interstellar (2014)/
│ │ └── Interstellar (2014).mkv
│ └── Inception (2010)/
│ └── Inception (2010).mkv
└── series/
└── Breaking Bad/
└── Season 01/
├── Breaking Bad S01E01.mkv
└── Breaking Bad S01E02.mkv
```
Inside Jellyfin, use container paths---not host paths:
``` text
Movies library -> /media/movies
Shows library -> /media/series
```
Do not configure `/home/jellyfin/...` in the Jellyfin UI because that
path exists on the Kubernetes node, not inside the container.
## 16. Manual Media Upload Through Multi-Hop SSH
Network path:
``` text
Windows PC
|
v
Jump Server: 36.255.71.187:22001
|
v
Master: 103.209.42.105:22002
|
v
Storage: 10.20.30.32:22
```
The first jump requires password + MFA. The master is reachable only
through the first jump and uses a private key already present on that
jump server.
A standard multi-hop `ProxyJump` from Windows failed because the Windows
SSH client tried to authenticate to the second hop itself. The chosen
pattern was to let Jump-1 execute the second SSH hop and use its
existing key.
Example Windows SSH config:
``` sshconfig
Host jump1
HostName 36.255.71.187
User mosiur
Port 22001
Host storage-node
HostName 10.20.30.32
User root
Port 22
ProxyCommand ssh jump1 "ssh -p 22002 -W %h:%p mosiur@103.209.42.105"
```
Test:
``` powershell
ssh storage-node
```
Once authentication is working, copy a movie:
``` powershell
scp "C:\Movies\movie.mkv" storage-node:/home/jellyfin/jellyfin/media/movies/
```
Or a directory:
``` powershell
scp -r "C:\Movies\Interstellar (2014)" storage-node:/home/jellyfin/jellyfin/media/movies/
```
### SSH troubleshooting notes
`Permission denied (publickey)` on the second hop means the network
connection reached the server but authentication failed.
`Host key verification failed` means server identity verification
failed, not MFA/password validation. Verify host-key fingerprints before
accepting changed/new host keys.
For storage root login, `PermitRootLogin yes` alone does not guarantee
password authentication. Effective SSH configuration, PAM, password
state, `Match` blocks, and `PasswordAuthentication` can still affect
login.
## 17. Adding Media to Jellyfin
After the file exists on the storage node under:
``` text
/home/jellyfin/jellyfin/media/movies/
```
open Jellyfin:
``` text
Dashboard -> Libraries -> Add Media Library
```
For movies:
``` text
Content type: Movies
Display name: Movies
Folder: /media/movies
```
For series:
``` text
Content type: Shows
Display name: Series
Folder: /media/series
```
Save and run a library scan if required. Test one media file end-to-end
before bulk uploading the library.
## 18. Optional Automated Download Architecture
For an automated workflow, the planned stack can be:
``` text
Prowlarr -> Radarr/Sonarr -> qBittorrent -> /downloads
|
v
organized import
|
v
/media/movies or /media/series
|
v
Jellyfin
```
Deploy and validate one component at a time, starting with the download
client. Only use sources/content you are authorized to download.
## 19. Quick Troubleshooting Matrix
-----------------------------------------------------------------------
Symptom First check
----------------------------------- -----------------------------------
Pod `Pending` Node selector, taint/toleration,
PVC binding
`/config/log` permission denied `fsGroup`, PVC permissions
Pod `CrashLoopBackOff` `kubectl logs` +
`kubectl describe pod`
Service has no backend EndpointSlice / selector
ClusterIP works, NodePort fails kube-proxy/NodePort path
HTTPS to `:31096` gives Use HTTP; TLS terminates at NPM
wrong-version error
`Permission denied (publickey)` Authentication/key for that hop
over SSH
`Host key verification failed` `known_hosts`/host fingerprint
Jellyfin cannot see uploaded movie File location, permissions,
`/media/...` library path
-----------------------------------------------------------------------
## 20. Final State Checklist
``` text
[✓] Dedicated storage node selected
[✓] storage=true:NoSchedule tolerated
[✓] Jellyfin runs as UID/GID 1001
[✓] Config persisted on Longhorn
[✓] Cache persisted on Longhorn
[✓] Media mounted from storage node
[✓] Downloads directory mounted
[✓] Jellyfin pod 1/1 Running
[✓] ClusterIP functional
[✓] NodePort 31096 functional
[✓] Nginx Proxy Manager routing functional
[✓] Jellyfin dashboard reachable
[✓] Multi-hop media transfer path established
[→] Create Movies/Series libraries and add media
[ ] Optional automated downloader stack
```