How to set up a custom domain name and TLS certificate for Plex running on Kubernetes

My Plex server’s remote access setup finally reached a state that I’m happy with, and I want to share that here. The idea here is: I want to expose the server outside of my home network so that I can listen to my music on the go, and do so seamlessly and via TLS encrypted IPv4 and IPv6 connections. Especially the latter is an issue that Plex has not solved yet; the official Remote Access functionality in Plex only supports IPv4. But I got there, and this is roughly how I did it:
With a Fritz!Box as a router and its MyFRITZ!Net service acting as a dynamic DNS service, exposing Plex via IPv4 and IPv6 _outside of the official Remote Access functionality is actually pretty straight-forward. The MyFRITZ!Net service gives every router a unique DNS name à la asdf1234.myfritz.net
that receives updated A and AAAA records, every time the Fritz!Box receives a new IP address assignment. That way services such as DuckDNS are not necessary; all I need to do is create the correct port-forwardings, add the DNS name as a custom server access URL in Plex and presto, the server is exposed to the outside world.
But this does not solve TLS encryption for me. With MyFRITZ!Net there is no way I can present a valid TLS certificate for asdf1234.myfritz.net
from my homeserver. It’s not my domain after all. Hence, all connections towards the server, while working, will be »downgraded« to unencrypted due to the certificate mismatch. Now, to get around that, I created a CNAME record pointing to asdf1234.myfritz.net.
on one of the domains that I own, allowing me to have TLS certificates issued for it by Let’s Encrypt (LE). This way, I just need to give Plex the LE certificate to present.
Kubernetes setup
This is where it gets interesting though, because my homeserver is running Kubernetes, using the lightweight distro k3s. Within Kubernetes, I’m running cert-manager (with a ClusterIssuer configured for Let’s Encrypt) to make the certificate juggling possible.
Please note that the following manifest snippets show only the relevant portions required to follow along. The full manifests can be found at the end of the article:
We start off by creating a Certificate resource for the DNS name:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: plex-janw-xyz-tls
namespace: default
spec:
dnsNames:
- "plex.janw.xyz"
issuerRef:
name: le-issuer-dns
kind: ClusterIssuer
secretName: plex-janw-xyz-tls
With that in place, I can now pass the certificate to Plex. Note here, that cert-manager currently does not generate PKCS#12-format certificate files but PEMs, while Plex expects PKCS#12 and does not work with PEMs. Therefore just mounting the cert’s Secret into the Plex container is not enough. We first must convert the format, and we do so in an initContainer:
initContainers:
- name: convert-cert
image: plexinc/pms-docker
command: [bash]
args:
- c
- |
openssl \
pkcs12 \
-export \
-out /tls-pkcs/tls.pfx \
-in /tls-cert/tls.crt \
-inkey /tls-cert/tls.key \
-password pass:SOMESECRET \
-name plex.janw.xyz \
-certpbe AES-256-CBC \
-keypbe AES-256-CBC \
-macalg SHA256
chmod 755 /tls-pkcs/tls.pfx
ls -lh /tls-pkcs/
volumeMounts:
- mountPath: /tls-cert
name: tls-cert
- mountPath: /tls-pkcs
name: tls-pkcs
# …
volumes:
# …
- name: tls-cert
secret:
secretName: myplex-tls
- name: tls-pkcs
emptyDir: {}
The script in the initContainer will convert the certificate and place it in the tls-pkcs
emptyDir volume. This volume can now be passed to the Plex container itself:
containers:
- name: plex
image: plexinc/pms-docker
volumeMounts:
# …
- mountPath: /tls-pkcs
name: tls-pkcs
ports:
- name: pms
containerPort: 32400
volumes:
# …
- name: tls-cert
secret:
secretName: plex-janw-xyz-tls
- name: tls-pkcs
emptyDir: {}
Plex settings UI
Now the certificate is available to Plex at /tls-pkcs/tls.pfx
, and encrypted with the password SOMESECRET
. Those details can be entered in the Plex server network settings now:

Further down on the settings page the custom server access URL must also be adjusted. Here we need the full external URL, including the port used in the forwarding setup:

Finally we’ll have to restart Plex, because it will load the certificate only on startup. After a rollout the certificate will be picked up though, and Plex should reachable via the new external hostname.
Notes on certificate renewal
You might have noticed the statefulset’s reloader.stakater.com/auto: "true"
annotation. It belongs to Stakater Reloader, a light-weight controller that automatically performs rolling updates on workloads when the ConfigMaps or Secrets they reference are updated. It is particularly useful, for example, when you are doing configuration changes in a ConfigMap but otherwise don’t modify the Deployment that references it. Kubernetes would normally not perform a rollout because the workload did not change, and if there is no dedicated mechanism to reload the config, the workload continues running with the previous configuration. Reloader practically replaces a dedicated config reload mechanism by watching the ConfigMap itself, and doing a rolling update when it changes, thus ensuring the workload adopts the new configuration.
In the setup described above I’m relying on Reloader to be a crude »certificate reloader« for the time when the Let’s Encrypt certificate is renewed. Unfortunately Plex does not support reloading its custom certificate, it is only loaded once at startup time. With the Reloader in place, the renewal will cause the certificate’s Secret to be updated, which in turn causes a new rollout of Plex, now with the renewed certificate. This is far from ideal, because the renewal can happen at any time, while you’re watching something, and that would cause the stream to be interrupted. But in my opinion it is still better than manually restarting the server whenever the renewal occurs.
Full manifests
Click here to expand the full manifests
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: plex-janw-xyz-tls
namespace: default
spec:
dnsNames:
- "plex.janw.xyz"
issuerRef:
name: letsencrypt-issuer
kind: ClusterIssuer
secretName: plex-janw-xyz-tls
---
kind: StatefulSet
apiVersion: apps/v1
metadata:
name: plex
namespace: default
annotations:
reloader.stakater.com/auto: "true"
spec:
replicas: 1
serviceName: plex
selector:
matchLabels:
app: plex
template:
metadata:
labels:
app: plex
app.kubernetes.io/name: plex
spec:
hostNetwork: true
initContainers:
- name: convert-cert
image: plexinc/pms-docker
command: [bash]
args:
- -c
- |
openssl \
pkcs12 \
-export \
-out /tls-pkcs/tls.pfx \
-in /tls-cert/tls.crt \
-inkey /tls-cert/tls.key \
-password pass:SOMESECRET \
-name plex.janw.xyz \
-certpbe AES-256-CBC \
-keypbe AES-256-CBC \
-macalg SHA256
chmod 755 /tls-pkcs/tls.pfx
ls -lh /tls-pkcs/
volumeMounts:
- mountPath: /tls-cert
name: tls-cert
- mountPath: /tls-pkcs
name: tls-pkcs
containers:
- name: plex
image: plexinc/pms-docker
resources:
volumeMounts:
- mountPath: /media
name: media-vol
readOnly: true
- mountPath: /transcode
name: transcode-vol
- mountPath: /config
name: config-vol
- mountPath: /tls-pkcs
name: tls-pkcs
ports:
- name: pms
containerPort: 32400
livenessProbe:
httpGet:
path: /identity
port: 32400
volumes:
- name: media-vol
hostPath:
path: /mnt/media
- name: transcode-vol
hostPath:
path: /mnt/cache
- name: config-vol
hostPath:
path: /mnt/config
- name: tls-cert
secret:
secretName: plex-janw-xyz-tls
- name: tls-pkcs
emptyDir: {}