Script Not Running Automatically to copy pem files to another directory?

PLEASE BEAR WITH ME I AM AN ABSOLUTE BEGINNER TRYING TO LEARN LINUX, JELLYFIN AND SSL CERTIFICATES

With the help of AI, I setup a Jellyfin server and also an SSL certificate to enable the address to have a https. AI told me to setup Jellyfin in a Docker Container, so I followed its instructions to do this, Jellyfin was unable to access the certificates which certbot put into /etc/letsencrypt/live/xxxx.casacam.net/ so it told me to create the script below:

#!/bin/sh
cp /etc/letsencrypt/live/xxxx.casacam.net/fullchain.pem /etc/jellyfin/ssl/
cp /etc/letsencrypt/live/xxxx.casacam.net/privkey.pem /etc/jellyfin/ssl/
chown root:jellyfin /etc/jellyfin/ssl/.pem
chmod 640 /etc/jellyfin/ssl/.pem
docker restart jellyfin

Which copies the certificates to a location that Jellyfin can access

it then told me to Make it executable with the command below
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/copy-jellyfin-certs.sh

And then when certbot.timer runs it should also run the script above.

However the certificates date expired a few days ago (when I went to the website https://xxxx.casacam.net it complained the site was not secure and said the certificate had expired.

I then ran the commands in the script above manually line by line then restarted my rasberry pi, and all was well the site was secured again but I noticed that the cerificate that was now on the site had been issued in February (a month earlier). So this led me to believe that the certbot.timer had worked to renew the certificate files in /etc/letsencrypt/live/xxxx.casacam.net/ but I can only assume to script had not automatically run to copy these files to /etc/jellyfin/ssl/

Can anyone help me try to find out if the script (copy-jellyfin-certs.sh) is configured to run when the certbot.timer updates the SSL Certificate.

As say I am learning at a very basic level, AI has been a great help so far but I can't seem to get it to help me solve this problem.

Thank you in advance

Please fill out the fields below so we can help you better. Note: you must provide your domain name to get help. Domain names for issued certificates are all made public in Certificate Transparency logs (e.g. crt.sh | example.com), so withholding your domain name here does not increase secrecy, but only makes it harder for us to provide help.

My domain is: casacam.net

I ran this command:ls -l /etc/letsencrypt/renewal-hooks/deploy/copy-jellyfin-certs.sh

It produced this output: -rwxr-xr-x 1 root root 258 Dec 17 10:45 /etc/letsencrypt/renewal-hooks/deploy/copy-jellyfin-certs.sh

My web server is (include version): jellyfin 10.11.5

The operating system my web server runs on is (include version): Linux CWPi5 6.12.47+rpt-rpi-2712 #1 SMP PREEMPT Debian 1:6.12.47-1+rpt1 (2025-09-16) aarch64 GNU/Linux

My hosting provider, if applicable, is:

I can login to a root shell on my machine (yes or no, or I don't know): I don't know

I'm using a control panel to manage my site (no, or provide the name and version of the control panel): No

The version of my client is (e.g. output of certbot --version or certbot-auto --version if you're using Certbot): Certbot 4.0.0

What you describe sounds like it should have worked.

Although, I wouldn't have recommended placing that script in the renewal-hooks directory. Sometimes that works best but for your case you should add a --deploy-hook for that specific cert instead. The renewal-hooks directory operates for every cert on your system. You may only have one today but that may not always be true.

I would move that script to somewhere outside of the certbot hooks directory. And use Certbot's reconfigure command to add the hook to your cert. See:

sudo certbot help reconfigure

You might also add a line to your script that writes to your own log to verify it ran. Even something as simple as echoing the current time to a log file. Certbot's log might show it too (I don't recall offhand) but I prefer my own log for my own custom scripts.

Once that is reconfigured you can test with

sudo certbot renew --dry-run --run-deploy-hooks

See the Certbot docs for details but dry-run won't affect your production certs but also does not, by default, run your deploy-hooks. Your deploy hook operates on an existing cert so should be fine to run even without actually getting a fresh production cert.

2 Likes

Where would you suggest I move the script too and what command would I use to do that, also how do I use certbots reconfigure command when I type sudo certbot help reconfigure I get the following come up:

usage:

certbot reconfigure --cert-name CERTNAME [options]

options:
-h, --help show this help message and exit
-c, --config CONFIG_FILE
path to config file (default: /etc/letsencrypt/cli.ini
and ~/.config/letsencrypt/cli.ini)

reconfigure:
Common options that may be updated with the "reconfigure" subcommand:

--cert-name CERTNAME Certificate name to apply. This name is used by
Certbot for housekeeping and in file paths; it doesn't
affect the content of the certificate itself.
Certificate name cannot contain filepath separators
(i.e. '/' or '', depending on the platform). To see
certificate names, run 'certbot certificates'. When
creating a new certificate, specifies the new
certificate's name. (default: the first provided
domain or the name of an existing certificate on your
system for the same domains)
--run-deploy-hooks When performing a test run using --dry-run or
reconfigure, run any applicable deploy hooks. This
includes hooks set on the command line, saved in the
certificate's renewal configuration file, or present
in the renewal-hooks directory. To exclude directory
hooks, use --no-directory-hooks. The hook(s) will only
be run if the dry run succeeds, and will use the
current active certificate, not the temporary test
certificate acquired during the dry run. This flag is
recommended when modifying the deploy hook using
reconfigure. (default: False)
--pre-hook PRE_HOOK Command to be run in a shell before obtaining any
certificates. Unless --disable-hook-validation is
used, the command’s first word must be the absolute
pathname of an executable or one found via the PATH
environment variable. Intended primarily for renewal,
where it can be used to temporarily shut down a
webserver that might conflict with the standalone
plugin. This will only be called if a certificate is
actually to be obtained/renewed. When renewing several
certificates that have identical pre-hooks, only the
first will be executed. (default: None)
--post-hook POST_HOOK
Command to be run in a shell after attempting to
obtain/renew certificates. Unless --disable-hook-
validation is used, the command’s first word must be
the absolute pathname of an executable or one found
via the PATH environment variable. Can be used to
deploy renewed certificates, or to restart any servers
that were stopped by --pre-hook. This is only run if
an attempt was made to obtain/renew a certificate. If
multiple renewed certificates have identical post-
hooks, only one will be run. (default: None)
--deploy-hook DEPLOY_HOOK
Command to be run in a shell once for each
successfully issued certificate. Unless --disable-
hook-validation is used, the command’s first word must
be the absolute pathname of an executable or one found
via the PATH environment variable. For this command,
the shell variable $RENEWED_LINEAGE will point to the
config live subdirectory (for example,
"/etc/letsencrypt/live/example.com") containing the
new certificates and keys; the shell variable
$RENEWED_DOMAINS will contain a space-delimited list
of renewed certificate domains (for example,
"example.com www.example.com") (default: None)
-a, --authenticator AUTHENTICATOR
Authenticator plugin name. (default: None)
-i, --installer INSTALLER
Installer plugin name (also used to find domains).
(default: None)
--webroot Obtain certificates by placing files in a webroot
directory. (default: False)

As I say I am a real beginner, and not sure how to safely use these commands without being in fear of causing more issues. I am trying to learn from helpful people like yourself.

Could be anywhere such as your "home" directory or any other place you want to dedicate for "your things"

The mv command is helpful to, um, move files. I mean, you got pretty far with an AI and it couldn't help with a command to move a file? :slight_smile:

You can learn the name of your certs using sudo certbot certificates

An example Certbot reconfigure is:

sudo certbot reconfigure --cert-name example.com --deploy-hook "/myscript/path/copy-jellyfin-certs.sh"
3 Likes

Ok so I managed to move the script to /home/chris/jellyfin/certbotscript,

I had to use sudo mv instead of just mv as it gave me a permission denied without sudo, is that ok?

Also I tried running the command you suggested adding my cert name and changing the script path to "/home/chris/jellyfin/certbotscript/copy-jellyfin-certs.sh"

But when I pushed enter I got this message below what should I select R or D?

You are attempting to set a --deploy-hook. Would you like Certbot to run deploy
hooks when it performs a dry run with the new settings? This will run all
relevant deploy hooks, including directory hooks, unless --no-directory-hooks is
set. This will use the current active certificate, and not the temporary test
certificate acquired during the dry run.


(R)un deploy hooks/(D)o not run deploy hooks: R

I could ask Ai but I am trying to follow your advice to help, if I start asking Ai then it may tell me to do things a different way to what you are saying and could confuse matters and totally confuse me.

As I say I am trying to learn, I learn best from Practical experience rather than reading literature.

Yes, you'll probably want to learn about linux file and directory permissions at some point.

R was correct. I even showed using that option in a prior post.

I prefer to learn from "doing" as well. But, reference material for the tools I use are excellent sources and guides. I say reference to distinguish that from random posts / blogs on the internet which requires some advanced knowledge to sort out good info from bad. For example, Certbot's docs (here) are reference material. Googling and reading random posts is not. You will see plenty of good info as well as bad so caution required. My experience with AI engines and tech info is mixed. I have gotten excellent guidance at times but also aggravating amounts of faulty info.

In this case the AI gave what looks like a working model. It looks more complex than need be although I am not a Jellyfin expert so perhaps that way is best. Still, it will require you to eventually know how to manage that setup. A Jellyfin forum or their docs was another way to have learned how to setup a system. There may even be step-by-step example given. But, I digress.

What was the result of running that reconfigure command? And, what is the result of the --dry-run command I showed?

2 Likes

The result of running the reconfigure command is this

(R)un deploy hooks/(D)o not run deploy hooks: R
The requested dns-dynu plugin does not appear to be installed
Ask for help or search for solutions at https://community.letsencrypt.org. See the logfile /var/log/letsencrypt/letsencrypt.log or re-run Certbot with -v for more details.

Here is the the let’s encrypt log
23:29:37,667:DEBUG:certbot._internal.main:certbot version: 4.0.0
2026-03-20 23:29:37,667:DEBUG:certbot._internal.main:Location of certbot entry point: /usr/bin/certbot
2026-03-20 23:29:37,667:DEBUG:certbot._internal.main:Arguments: ['--cert-name', 'xxxx.casacam.net', '--deploy-hook', '/home/chris/jellyfin/certbotscript/copy-jellyfin-certs.sh']
2026-03-20 23:29:37,667:DEBUG:certbot._internal.main:Discovered plugins: PluginsRegistry(PluginEntryPoint#manual,PluginEntryPoint#null,PluginEntryPoint#standalone,PluginEntryPoint#webroot)
2026-03-20 23:29:37,673:DEBUG:certbot._internal.log:Root logging level set at 30
2026-03-20 23:30:07,471:INFO:certbot._internal.storage:Attempting to parse the version 5.2.2 renewal configuration file found at /etc/letsencrypt/renewal/xxxx.casacam.net.conf with version 4.0.0 of Certbot. This might not work.
2026-03-20 23:30:07,472:DEBUG:certbot._internal.plugins.selection:Requested authenticator None and installer None
2026-03-20 23:30:07,473:DEBUG:certbot.configuration:Var server=https://acme-v02.api.letsencrypt.org/directory (set by user).
2026-03-20 23:30:07,473:DEBUG:certbot._internal.plugins.selection:Requested authenticator dns-dynu and installer None
2026-03-20 23:30:07,473:DEBUG:certbot._internal.plugins.selection:No candidate plugin
2026-03-20 23:30:07,473:DEBUG:certbot._internal.log:Exiting abnormally:
Traceback (most recent call last):
File "/usr/bin/certbot", line 33, in
sys.exit(load_entry_point('certbot==4.0.0', 'console_scripts', 'certbot')())
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
File "/usr/lib/python3/dist-packages/certbot/main.py", line 19, in main
return internal_main.main(cli_args)
~~~~~~~~~~~~~~~~~~^^^^^^^^^^
File "/usr/lib/python3/dist-packages/certbot/_internal/main.py", line 1872, in main
return config.func(config, plugins)
~~~~~~~~~~~^^^^^^^^^^^^^^^^^
File "/usr/lib/python3/dist-packages/certbot/_internal/main.py", line 1767, in reconfigure
installer, auth = plug_sel.choose_configurator_plugins(lineage_config, plugins, "certonly")
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3/dist-packages/certbot/_internal/plugins/selection.py", line 256, in choose_configurator_plugins
diagnose_configurator_problem("authenticator", req_auth, plugins)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3/dist-packages/certbot/_internal/plugins/selection.py", line 374, in diagnose_configurator_problem
raise errors.PluginSelectionError(msg)
certbot.errors.PluginSelectionError: The requested dns-dynu plugin does not appear to be installed
2026-03-20 23:30:07,476:ERROR:certbot._internal.log:The requested dns-dynu plugin does not appear to be installed

By the looks of it this didn’t work, please can you help?

Thanks

The reconfigure for the deploy-hook would not have broken that.

Is that what you used originally to get the certificate? At this stage I am going to want to see actual domain name. I will be able to learn things that are otherwise too time-consuming to work through.

Please show the unedited output of this. We can start here.
sudo certbot certificates

1 Like

Found the following certs:
Certificate Name: cjf.casacam.net
Serial Number: 5e12c124b85f4da7a9dca2043cf23369339
Key Type: ECDSA
Domains: cjf.casacam.net
Expiry Date: 2026-05-15 18:47:38+00:00 (VALID: 55 days)
Certificate Path: /etc/letsencrypt/live/cjf.casacam.net/fullchain.pem
Private Key Path: /etc/letsencrypt/live/cjf.casacam.net/privkey.pem

I really don’t know how I installed the certificates, sorry!

It appears something has broken with your Certbot setup since you last got a cert in Feb.

Would you now show the contents of this file? You can redact the account number if you wish

/etc/letsencrypt/renewal/cjf.casacam.net.conf

I have some other questions as your system looks generally misconfigured. Understanding what you have is needed to get the cert sorted out. I'm not sure we're the best forum for you to sort out these issues but let's see how it goes. We generally prefer to have a system that is generally working and well-behaved but that's not what I see :frowning:

Please answer the following as best you can

You have an nginx server replying to HTTPS requests to your domain. How does that fit with your Jellyfin server? Is it acting as a reverse proxy to it? Or, do you use a different subdomain for Jellyfin?

While HTTPS requests get a reply from nginx, HTTP requests (port 80) fail. Do you know why that happens? Does your ISP restrict inbound HTTP requests on port 80?

Do you expect requests to the "home" page of that domain to work? Because HTTPS (port 443) requests to your domain get redirected to HTTP. One, you shouldn't redirect from a secure protocol (HTTPS) to an unencrypted one (HTTP). And, as I just noted HTTP requests are failing to reach you. This means requests to your home page are failing.

2 Likes

version = 5.2.2
archive_dir = /etc/letsencrypt/archive/cjf.casacam.net
cert = /etc/letsencrypt/live/cjf.casacam.net/cert.pem
privkey = /etc/letsencrypt/live/cjf.casacam.net/privkey.pem
chain = /etc/letsencrypt/live/cjf.casacam.net/chain.pem
fullchain = /etc/letsencrypt/live/cjf.casacam.net/fullchain.pem
[renewalparams]
account =
authenticator = dns-dynu
dns_dynu_credentials = /etc/letsencrypt/dynu.ini
server = https://acme-v02.api.letsencrypt.org/directory
key_type = ecdsa
[acme_renewal_info]
ari_retry_after = 2026-03-21T09:53:31

I don't think I have a reverse proxy, it connects directly to my router
The Jellyfin server only accepts requests from HTTPS
The domain is a free one from Dynu using their dynamic dns service, so I don't own the whole domain I just have to use of the sub domain.

I don't know if the following is relevant

During the original setup I got this error message

~/certbot-venv/bin/certbot plugins ~/certbot-venv/bin/certbot renew --dry-run The 'certbot_dns_dynu.dns_dynu' plugin errored while loading: Duplicate plugin name dns-dynu from certbot-dns-dynu and certbot-dns-dynudns.. You may need to remove or update this plugin. The Certbot log will contain the full error details and this should be reported to the plugin developer.

I posted this error in to AI and it Said

That duplicate‑plugin error means you now have two different Dynu DNS plugins installed in your environment:

  • certbot-dns-dynu
  • certbot-dns-dynudns
    Both register the same name (dns-dynu), so Certbot refuses to load them.

:wrench: How to fix it

  1. Check what’s installed in your venv

source ~/certbot-venv/bin/activate
pip list | grep certbot

You’ll likely see both certbot-dns-dynu and certbot-dns-dynudns.

  1. Remove the duplicate Keep only one plugin. The correct one for Dynu is certbot-dns-dynu. Remove the other:

pip uninstall certbot-dns-dynudns

If you accidentally installed both, you can also remove both and reinstall just the correct one:

pip uninstall certbot-dns-dynu certbot-dns-dynudns
pip install certbot-dns-dynu

  1. Verify plugin availability

~/certbot-venv/bin/certbot plugins

You should now see only one dns-dynu entry. (however I cant remember if I did only see one entry at the time!)

  1. Test renewal again

~/certbot-venv/bin/certbot renew --dry-run

If I run the above command I get
The following error was encountered:
[Errno 13] Permission denied: '/var/log/letsencrypt/.certbot.lock'
Either run as root, or set --config-dir, --work-dir, and --logs-dir to writeable paths.

So I tried running it as

sudo ~/certbot-venv/bin/certbot plugins

and it looks as if the simulated renewals work fine

Saving debug log to /var/log/letsencrypt/letsencrypt.log


Processing /etc/letsencrypt/renewal/cjf.casacam.net.conf


Simulating renewal of an existing certificate for cjf.casacam.net
Waiting 60 seconds for DNS changes to propagate


Congratulations, all simulated renewals succeeded:
/etc/letsencrypt/live/cjf.casacam.net/fullchain.pem (success)

So I just assumed everything was working, so just left it at that.

I set this up in December 2025, and it renewed the certificate in February 2026, what is strange is that I haven't changed anything on the server between times, as everything seemed to be running ok, but it was just the fact in March 2026 the site reported the certificate had expired, which leads me to believe that the script was not copying the pem files to a location where Jellyfin has permission to read them.

If you need any further information please do not hesitate to let me know.

Thanks again for all your help.

An HTTPS request for your "home" page gets this reply. Note the "Server:" response header. That is an nginx server and is often used to reverse proxy to a JellyFin (per Jellyfin's docs).

curl -i -m7 https://cjf.casacam.net 

HTTP/1.1 302 Found
Server: nginx
Date: Sat, 21 Mar 2026 11:58:14 GMT
Location: web/

I was going to switch you off your broken DNS Challenge that used dns-dynu but if that is now working we can stay with that. An HTTP Challenge would likely have been simpler to setup. Although, it requires HTTP requests on port 80 to work which is why I asked about that.

Well, something is different for the reconfigure command to now fail with a missing DNS Challenge plugin.

Your Certbot renewal conf file shows Certbot version 5.2.2 . Yet, the log you showed after I asked you to run the reconfigure is below which shows Certbot v4.0.0.

Can you explain how you have two different versions running?

I am a little confused by what you have said. I asked you to use the reconfigure command and you said it failed because of the missing component. Then you said the --dry-run is working so all is well.

Yet, the renewal config file does not have the --deploy-hook included in it which it would if the reconfigure worked.

Let's keep it even simpler. What do these show right now?

sudo certbot --version
sudo certbot renew --dry-run

And, what exact URL do you use to connect to your Jellyfin? Because as I noted requests to your "home" page are failing right now.

1 Like

I can only assume from what you say is that I do have a reverse proxy, sorry I really haven't got a clue I just followed instructions that AI gave me.

I did not know that I have two versions of certbot running!

This is the output I get when running the two commands you asked me to run

sudo certbot --version
sudo certbot renew --dry-run

chris@CWPi5:~ $ sudo certbot --version
certbot 4.0.0
chris@CWPi5:~ $ sudo certbot renew --dry-run
Saving debug log to /var/log/letsencrypt/letsencrypt.log


Processing /etc/letsencrypt/renewal/cjf.casacam.net.conf


Failed to renew certificate cjf.casacam.net with error: The requested dns-dynu plugin does not appear to be installed


All simulated renewals failed. The following certificates could not be renewed:
/etc/letsencrypt/live/cjf.casacam.net/fullchain.pem (failure)


1 renew failure(s), 0 parse failure(s)
Ask for help or search for solutions at https://community.letsencrypt.org. See the logfile /var/log/letsencrypt/letsencrypt.log or re-run Certbot with -v for more details.

However when I run
sudo ~/certbot-venv/bin/certbot renew --dry-run

I get the following:

Saving debug log to /var/log/letsencrypt/letsencrypt.log


Processing /etc/letsencrypt/renewal/cjf.casacam.net.conf


Simulating renewal of an existing certificate for cjf.casacam.net
Waiting 60 seconds for DNS changes to propagate


Congratulations, all simulated renewals succeeded:
/etc/letsencrypt/live/cjf.casacam.net/fullchain.pem (success)

And when I run sudo ~/certbot-venv/bin/certbot --version

I get
certbot 5.2.2

So does this mean I have certbot 5.2.2 in a directory called certbot-venv/bin/ and another certbot somewhere else?

If so the certbot in certbot-venv/bin/ seems to be working but the other certbot doesnt

Could I just run the command you originally said (below)?
sudo ~/certbot-venv/bin/certbot reconfigure --cert-name cjf.casacam.net --deploy-hook "/home/chris/jellyfin/certbotscript/copy-jellyfin-certs.sh"

The address I use to access the jellyfin server is https://cjf.casacam.net

Thanks again for all your help, sorry for asking so many questions, I am learning slowly but still proceeding cautiously.

Yes, it looks like you installed the Debian apt version of Certbot and also a pip version. And, Certbot actually recommends using yet a different install method - the Snap install :slight_smile:

Your automated renewal is likely using the apt install as you did not complete the pip install properly. The Certbot instructions for that are here: Certbot Instructions

Before choosing a way forward though, did you require the pip version for some reason? Like did the DNS Challenge plugin require it?

Let's leave that question for later. Your automated Certbot renew command is likely using the apt version you have (4.0). We need to decide on one kind of Certbot install and build from that. The pip install requires more regular attention than the snap install method. I think snap install would be easier for you in the long run although there could be good reasons to use pip.

Ah, I make a mistake following your redirects and see that request working now.

Request to your "home" page redirects to web/ which works. But earlier I omitted the trailing slash and just use web which then redirected me to http://cjf.casacam.net/web/ which then failed because of a timeout using HTTP (port 80). This is what I saw when I said you should not redirect from HTTPS to HTTP (and you shouldn't). This likely won't be noticed by any actual user.

If you knew why HTTP (port 80) requests failed we could simplify your cert request with an HTTP Challenge. But, it is not essential if this is primarily for your own use or a limited audience.

We need to sort out your Certbot install first but I wanted to clarify my mistake from earlier.

1 Like

I'm not sure if I need pip version (I don't even know what the pip version is)! Likewise I'm not not sure what a DNS Challenge plugin is.

I think I turned off http in Jellyfin so that it only connects via HTTPS

Sorry for being so thick on this, I guess this is what happens if you just follow the advice of an AI bot, it mostly gets the job done but you don't really know how you got there.

This command is using a pip venv: sudo ~/certbot-venv ...

To get a cert you must pass a challenge. The two more common are an HTTP Challenge and a DNS Challenge. See official Let's Encrypt docs: Challenge Types - Let's Encrypt

A DNS Challenge works best when you can dynamically add/delete the needed challenge record using an API for your DNS provider. Certbot uses "plugins" for those.

The Certbot team offers a small number of plugins for very common DNS providers. You can also use third-party plugins which is what you need for your provider. See Certbot docs: User Guide — Certbot 5.5.0.dev0 documentation

Perhaps instructions for some third-party DNS plugin suggested using pip. Do you remember where you got the instructions for that plugin? You would have had to install something with the pip install of Certbot to get that plugin and have gotten a cert (and which allows --dry-run to work right now).

An HTTP Challenge is usually far easier to get working which is why I keep asking about HTTP connections to your system using port 80. Can you explain anything about why HTTP (port 80) may or may not be working?

Yeah, but, it looks like nginx is proxying to Jellyfin so nginx is the "thing" connecting to Jellyfin. A cert is used for an HTTPS connection. An HTTPS request from a browser actually connects with nginx first. nginx will then connect (proxy) to Jellyfin and can use HTTP (no cert) or HTTPS (cert). The cert you got is probably be used in both nginx and Jellyfin. And, using an expired cert in either place can cause failures. It isn't harmful to use HTTPS between nginx and Jellyfin but it isn't always required.

So: Browser->HTTPS->nginx->HTTP|HTTPS->Jellyfin

Can you show the output of this command? An uppercase T is essential. The output will be long. This may help determine if we can use the much simpler HTTP Challenge

sudo nginx -T
1 Like

Thanks for letting me know about pip and http Challenge.

I have run sudo nginx -T as requested and here's the output

chris@CWPi5:~ $ sudo nginx -T
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

# configuration file /etc/nginx/nginx.conf:
user www-data;
worker_processes auto;
worker_cpu_affinity auto;
pid /run/nginx.pid;
error_log /var/log/nginx/error.log;
include /etc/nginx/modules-enabled/*.conf;

events {
	worker_connections 768;
	# multi_accept on;
}

http {

	##
	# Basic Settings
	##

	sendfile on;
	tcp_nopush on;
	types_hash_max_size 2048;
	server_tokens off; # Recommended practice is to turn this off

	# server_names_hash_bucket_size 64;
	# server_name_in_redirect off;

	include /etc/nginx/mime.types;
	default_type application/octet-stream;

	##
	# SSL Settings
	##

	ssl_protocols TLSv1.2 TLSv1.3; # Dropping SSLv3 (POODLE), TLS 1.0, 1.1
	ssl_prefer_server_ciphers off; # Don't force server cipher order.

	##
	# Logging Settings
	##

	access_log /var/log/nginx/access.log;

	##
	# Gzip Settings
	##

	gzip on;

	# gzip_vary on;
	# gzip_proxied any;
	# gzip_comp_level 6;
	# gzip_buffers 16 8k;
	# gzip_http_version 1.1;
	# gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

	##
	# Virtual Host Configs
	##

	include /etc/nginx/conf.d/*.conf;
	include /etc/nginx/sites-enabled/*;
}


#mail {
#	# See sample authentication script at:
#	# http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript
#
#	# auth_http localhost/auth.php;
#	# pop3_capabilities "TOP" "USER";
#	# imap_capabilities "IMAP4rev1" "UIDPLUS";
#
#	server {
#		listen     localhost:110;
#		protocol   pop3;
#		proxy      on;
#	}
#
#	server {
#		listen     localhost:143;
#		protocol   imap;
#		proxy      on;
#	}
#}

# configuration file /etc/nginx/mime.types:
types {
    text/html                                        html htm shtml;
    text/css                                         css;
    text/xml                                         xml;
    image/gif                                        gif;
    image/jpeg                                       jpeg jpg;
    application/javascript                           js;
    application/atom+xml                             atom;
    application/rss+xml                              rss;

    text/mathml                                      mml;
    text/plain                                       txt;
    text/vnd.sun.j2me.app-descriptor                 jad;
    text/vnd.wap.wml                                 wml;
    text/x-component                                 htc;

    image/avif                                       avif;
    image/png                                        png;
    image/svg+xml                                    svg svgz;
    image/tiff                                       tif tiff;
    image/vnd.wap.wbmp                               wbmp;
    image/webp                                       webp;
    image/x-icon                                     ico;
    image/x-jng                                      jng;
    image/x-ms-bmp                                   bmp;

    font/woff                                        woff;
    font/woff2                                       woff2;

    application/java-archive                         jar war ear;
    application/json                                 json;
    application/mac-binhex40                         hqx;
    application/msword                               doc;
    application/pdf                                  pdf;
    application/postscript                           ps eps ai;
    application/rtf                                  rtf;
    application/vnd.apple.mpegurl                    m3u8;
    application/vnd.google-earth.kml+xml             kml;
    application/vnd.google-earth.kmz                 kmz;
    application/vnd.ms-excel                         xls;
    application/vnd.ms-fontobject                    eot;
    application/vnd.ms-powerpoint                    ppt;
    application/vnd.oasis.opendocument.graphics      odg;
    application/vnd.oasis.opendocument.presentation  odp;
    application/vnd.oasis.opendocument.spreadsheet   ods;
    application/vnd.oasis.opendocument.text          odt;
    application/vnd.openxmlformats-officedocument.presentationml.presentation
                                                     pptx;
    application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
                                                     xlsx;
    application/vnd.openxmlformats-officedocument.wordprocessingml.document
                                                     docx;
    application/vnd.wap.wmlc                         wmlc;
    application/wasm                                 wasm;
    application/x-7z-compressed                      7z;
    application/x-cocoa                              cco;
    application/x-java-archive-diff                  jardiff;
    application/x-java-jnlp-file                     jnlp;
    application/x-makeself                           run;
    application/x-perl                               pl pm;
    application/x-pilot                              prc pdb;
    application/x-rar-compressed                     rar;
    application/x-redhat-package-manager             rpm;
    application/x-sea                                sea;
    application/x-shockwave-flash                    swf;
    application/x-stuffit                            sit;
    application/x-tcl                                tcl tk;
    application/x-x509-ca-cert                       der pem crt;
    application/x-xpinstall                          xpi;
    application/xhtml+xml                            xhtml;
    application/xslt+xml                             xsl xslt;
    application/xspf+xml                             xspf;
    application/zip                                  zip;

    application/octet-stream                         bin exe dll;
    application/octet-stream                         deb;
    application/octet-stream                         dmg;
    application/octet-stream                         iso img;
    application/octet-stream                         msi msp msm;

    audio/midi                                       mid midi kar;
    audio/mpeg                                       mp3;
    audio/ogg                                        ogg;
    audio/x-m4a                                      m4a;
    audio/x-realaudio                                ra;

    video/3gpp                                       3gpp 3gp;
    video/mp2t                                       ts;
    video/mp4                                        mp4;
    video/mpeg                                       mpeg mpg;
    video/ogg                                        ogv;
    video/quicktime                                  mov;
    video/webm                                       webm;
    video/x-flv                                      flv;
    video/x-m4v                                      m4v;
    video/x-matroska                                 mkv;
    video/x-mng                                      mng;
    video/x-ms-asf                                   asx asf;
    video/x-ms-wmv                                   wmv;
    video/x-msvideo                                  avi;
}

# configuration file /etc/nginx/sites-enabled/default:
##
# You should look at the following URL's in order to grasp a solid understanding
# of Nginx configuration files in order to fully unleash the power of Nginx.
# https://www.nginx.com/resources/wiki/start/
# https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/
# https://wiki.debian.org/Nginx/DirectoryStructure
#
# In most cases, administrators will remove this file from sites-enabled/ and
# leave it as reference inside of sites-available where it will continue to be
# updated by the nginx packaging team.
#
# This file will automatically load configuration files provided by other
# applications, such as Drupal or Wordpress. These applications will be made
# available underneath a path with that package name, such as /drupal8.
#
# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.
##

# Default server configuration
#
server {
	listen 80 default_server;
	listen [::]:80 default_server;

	# SSL configuration
	#
	# listen 443 ssl default_server;
	# listen [::]:443 ssl default_server;
	#
	# Note: You should disable gzip for SSL traffic.
	# See: https://bugs.debian.org/773332
	#
	# Read up on ssl_ciphers to ensure a secure configuration.
	# See: https://bugs.debian.org/765782
	#
	# Self signed certs generated by the ssl-cert package
	# Don't use them in a production server!
	#
	# include snippets/snakeoil.conf;

	root /var/www/html;

	# Add index.php to the list if you are using PHP
	index index.html index.htm index.nginx-debian.html;

	server_name _;

	location / {
		# First attempt to serve request as file, then
		# as directory, then fall back to displaying a 404.
		try_files $uri $uri/ =404;
	}

	# pass PHP scripts to FastCGI server
	#
	#location ~ \.php$ {
	#	include snippets/fastcgi-php.conf;
	#
	#	# With php-fpm (or other unix sockets):
	#	fastcgi_pass unix:/run/php/php7.4-fpm.sock;
	#	# With php-cgi (or other tcp sockets):
	#	fastcgi_pass 127.0.0.1:9000;
	#}

	# deny access to .htaccess files, if Apache's document root
	# concurs with nginx's one
	#
	#location ~ /\.ht {
	#	deny all;
	#}
}


# Virtual Host configuration for example.com
#
# You can move that to a different file under sites-available/ and symlink that
# to sites-enabled/ to enable it.
#
#server {
#	listen 80;
#	listen [::]:80;
#
#	server_name example.com;
#
#	root /var/www/example.com;
#	index index.html;
#
#	location / {
#		try_files $uri $uri/ =404;
#	}
#}

# configuration file /etc/nginx/sites-enabled/jellyfin.conf:
server {
    listen 443 ssl;
    server_name cjf.casacam.net;

    ssl_certificate /etc/jellyfin/ssl/fullchain.pem;
    ssl_certificate_key /etc/jellyfin/ssl/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;

    location / {
        proxy_pass http://127.0.0.1:8096;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
server {
    listen 80;
    server_name cjf.casacam.net;
    return 301 https://$host$request_uri;

I have found the conversation I had with AI when asking it how to setup letsencrypt it looks like it tells you why certbot was setup as pip, this might make more sense to you than it does to me and hopefuly might give you more of an idea of what's going on here:

AI Told Me Correct command for Raspberry Pi OS (Debian/Ubuntu based)

sudo apt update
sudo apt install python3-certbot-dns-dynu

But I ran that command but go the error Error: Unable to locate package python3-certbot-dns-dynu

So It then said:

How to install the Dynu plugin on Raspberry Pi

Update pip and Certbot first:

sudo apt update
sudo apt install -y python3-pip
pip3 install --upgrade certbot

Install the Dynu plugin via pip:
pip3 install certbot-dns-dynu

When I tried this I got the message externally-managed-environment

That error is coming from Python’s packaging system on Raspberry Pi OS (and other Debian‑based distros). It means the system Python environment is “externally managed” by the OS, so pip3 install … refuses to put packages directly into /usr/lib/python3….

You have a few safe ways forward:

Option 1 — Use Debian packages (preferred if available)
On Raspberry Pi OS, not all Certbot DNS plugins are packaged. Since python3-certbot-dns-dynu isn’t in apt, this option isn’t viable for Dynu specifically.

Option 2 — Use a virtual environment (recommended)
Create a Python virtual environment just for Certbot plugins, then install the Dynu plugin there:

sudo apt install -y python3-venv
python3 -m venv ~/certbot-venv
source ~/certbot-venv/bin/activate
pip install --upgrade pip
pip install certbot certbot-dns-dynu

Now you can run Certbot from that venv:
~/certbot-venv/bin/certbot plugins
~/certbot-venv/bin/certbot renew --dry-run


What I recommend for you

Since you’re on Raspberry Pi OS and need the Dynu plugin, the virtual environment approach (Option 2) is the safest and most future‑proof. It keeps the plugin isolated, avoids breaking system packages, and lets you control updates.

Next step: run the python3 -m venv ~/certbot-venv commands I listed, then use ~/certbot-venv/bin/certbot renew --dry-run to confirm the plugin works.

I then got the error message

~/certbot-venv/bin/certbot plugins
~/certbot-venv/bin/certbot renew --dry-run
The 'certbot_dns_dynu.dns_dynu' plugin errored while loading: Duplicate plugin name dns-dynu from certbot-dns-dynu and certbot-dns-dynudns.. You may need to remove or update this plugin. The Certbot log will contain the full error details and this should be reported to the plugin developer.

So then AI Said

That duplicate‑plugin error means you now have two different Dynu DNS plugins installed in your environment:

    certbot-dns-dynu

    certbot-dns-dynudns

Both register the same name (dns-dynu), so Certbot refuses to load them.

How to fix it

1. Check what’s installed in your venv

source ~/certbot-venv/bin/activate
pip list | grep certbot

You’ll likely see both certbot-dns-dynu and certbot-dns-dynudns.

 Remove the duplicate
Keep only one plugin. The correct one for Dynu is certbot-dns-dynu. Remove the other:

pip uninstall certbot-dns-dynudns


If you accidentally installed both, you can also remove both and reinstall just the correct one:

pip uninstall certbot-dns-dynu certbot-dns-dynudns
pip install certbot-dns-dynu


Verify plugin availability
~/certbot-venv/bin/certbot plugins
You should now see only one dns-dynu entry.

I did only see one dns-dynu entry

Test renewal again
~/certbot-venv/bin/certbot renew --dry-run

This gave me the following error:
~/certbot-venv/bin/certbot renew --dry-run
The following error was encountered:
[Errno 13] Permission denied: '/var/log/letsencrypt/.certbot.lock'
Either run as root, or set --config-dir, --work-dir, and --logs-dir to writeable paths.

So it suggested
Run certbot in the venv as root (simplest)
sudo ~/certbot-venv/bin/certbot renew --dry-run

So then it said
Recommended for Raspberry Pi

(sudo ~/certbot-venv/bin/certbot renew --dry-run). It keeps everything in the standard /etc/letsencrypt and /var/log/letsencrypt locations, which is what the systemd timer expects.

Next step for you

Run: 
sudo ~/certbot-venv/bin/certbot renew --dry-run

If that succeeds, I can show you how to point the certbot.timer service at your venv’s binary so renewals happen automatically. Would you like me to walk you through updating the systemd unit so it uses your venv’s certbot instead of the system one?

So I said: Yes please show me how to point the certbot.timer service at my venv’s binary so renewals happen automatically.

Perfect — let’s wire your venv’s certbot into the systemd timer so renewals happen automatically. Right now, certbot.timer is scheduled twice daily, but it calls the system binary (/usr/bin/certbot). We’ll override the unit so it points to your venv’s ~/certbot-venv/bin/certbot.

Step‑by‑step

1. Create an override directory for the systemd service
sudo systemctl edit certbot.service

This opens an editor (usually nano or vim). Add the following lines:
[Service]
ExecStart=
ExecStart=/home/pi/certbot-venv/bin/certbot -q renew
    ⚠️ Replace /home/pi with your actual username/home path if different.

    The first ExecStart= line clears the existing command.

    The second sets it to your venv’s certbot binary.

Save and exit.

2. Reload systemd to apply changes

sudo systemctl daemon-reexec
sudo systemctl daemon-reload

3. Test the service manually
sudo systemctl start certbot.service
sudo systemctl status certbot.service --no-pager

You should see it run your venv’s certbot and exit cleanly.

I tried to test the service manually but got this message

sudo systemctl start certbot.service
sudo systemctl status certbot.service --no-pager
Job for certbot.service failed because the control process exited with error code.
See "systemctl status certbot.service" and "journalctl -xeu certbot.service" for details.
Ă— certbot.service - Certbot
     Loaded: loaded (/usr/lib/systemd/system/certbot.service; static)
    Drop-In: /etc/systemd/system/certbot.service.d
             └─override.conf
     Active: failed (Result: exit-code) since Wed 2025-12-17 11:48:45 GMT; 33ms ago
 Invocation: c8403933bc37464697deb0974cb2bf2f
TriggeredBy: â—Ź certbot.timer
       Docs: file:///usr/share/doc/python-certbot-doc/html/index.html
             https://certbot.eff.org/docs
    Process: 44018 ExecStart=/home/pi/certbot-venv/bin/certbot -q renew (code=exited, status=203/EXEC)
   Main PID: 44018 (code=exited, status=203/EXEC)
        CPU: 12ms

Dec 17 11:48:45 CWPi5 systemd[1]: Starting certbot.service - Certbot...
Dec 17 11:48:45 CWPi5 (certbot)[44018]: certbot.service: Unable to locate e…tory
Dec 17 11:48:45 CWPi5 (certbot)[44018]: certbot.service: Failed at step EXE…tory
Dec 17 11:48:45 CWPi5 systemd[1]: certbot.service: Main process exited, cod…EXEC
Dec 17 11:48:45 CWPi5 systemd[1]: certbot.service: Failed with result 'exit…de'.
Dec 17 11:48:45 CWPi5 systemd[1]: Failed to start certbot.service - Certbot.
Hint: Some lines were ellipsized, use -l to show in full.


AI Said

The status=203/EXEC error means systemd couldn’t execute the binary you pointed to. That usually happens if:

    The path in your override (ExecStart=/home/pi/certbot-venv/bin/certbot) is wrong (typo, wrong username, or the venv is in a different directory).

    The binary isn’t executable or doesn’t exist at that path.

    Systemd doesn’t expand ~, so you must use the full absolute path.


🔧 Fix step‑by‑step

1. Confirm the actual path to your venv’s certbot
bash

ls -l /home/pi/certbot-venv/bin/certbot

If your username isn’t pi, adjust accordingly (e.g. /home/chris/...).

This returned: 

-rwxrwxr-x 1 chris chris 204 Dec 16 15:15 /home/chris/certbot-venv/bin/certbot

2. Make sure it’s executable
bash

chmod +x /home/pi/certbot-venv/bin/certbot

When Entering the above no response was given it just gave me a prompt below

3. Edit the override again with the correct absolute path
bash

sudo systemctl edit certbot.service

Replace with:
ini

[Service]
ExecStart=
ExecStart=/home/pi/certbot-venv/bin/certbot -q renew

When running the command sudo systemctl edit certbot.service this is the file it opened, it looks correct

/etc/systemd/system/certbot.service.d/.#override.conf5fc1024ffa7a95ec           
### Editing /etc/systemd/system/certbot.service.d/override.conf
### Anything between here and the comment below will become the contents of the>

[Service]
ExecStart=
ExecStart=/home/chris/certbot-venv/bin/certbot -q renew

### Edits below this comment will be discarded


### /usr/lib/systemd/system/certbot.service
# [Unit]
# Description=Certbot
# Documentation=file:///usr/share/doc/python-certbot-doc/html/index.html
# Documentation=https://certbot.eff.org/docs
# [Service]
# Type=oneshot
# ExecStart=/usr/bin/certbot -q renew --no-random-sleep-on-renew
# PrivateTmp=true



4. Reload systemd and test
bash

sudo systemctl daemon-reload
sudo systemctl start certbot.service
sudo systemctl status certbot.service --no-pager

yeah, so it looks like that AI said to use pip for that DNS Challenge third-party plugin. I'd like to avoid both and I don't agree with it that for you the pip install is simpler or easiest to manage. An HTTP Challenge would be far easier. I have literally worked on thousands of threads on this community helping people get and use certificates. Click on my id and check my stats :slight_smile:

Interestingly that nginx is proxying to Jellyfin using http - not httpS. Which really is sensible given your nginx is (probably) running on your host and talking to Jellyfin running in a container on that same machine. No real reason to use HTTPS between those two services.

Part of the problem is that nginx isn't using the cert files as produced by Certbot. It is using a copy of them. Which isn't necessary at all and isn't even recommended.

So, there's two main things to sort out here. One, get nginx using the proper cert file names. And, two, get your Certbot automation working ideally by switching to HTTP Challenge. We need to see sudo certbot renew --dry-run working to know the renewal will work.

If HTTP Challenge isn't possible for some reason we could fix your pip install by following instructions I referred earlier. But, that still leaves you with you managing the pip version and the more complex DNS Challenge.

I am running out of time for a bit but might post more later today. Or, perhaps some other volunteer will take it up.

1 Like

@cdub Oh, and in the meantime check your router and any port forwarding or NAT rules. Did you add one/some for port 443? If so, add similar ones for port 80. That is, if you see a forward for port 443 to 443 on your server then make another for port 80 going to port 80 on that same local IP.

1 Like

Thanks for all your help with this, I really do appreciate your time and efforts in trying to sort this out.

I think the certificate is being renewed through the pip installed version of certbot as it was automatically renewed on 16th February, obviously I won’t for sure till around 30 days before the certificate is due for renewal to see if it renews again.

Running
sudo ~/certbot-venv/bin/certbot renew --dry-run

Gives me the message
Congratulations, all simulated renewals succeeded:
/etc/letsencrypt/live/cjf.casacam.net/fullchain.pem (success)

So going on the basis I think the certificate is being renewed automatically, it’s just the script to copy fullchain.pem and privkey.pem to a directory that Jellyfin can read those files from, is not running when the certbot renew is taking place.

Is there any way I could schedule a task to run the script named copy-jellyfin-certs.sh you asked me to move to /home/chris/jellyfin/certbotscript which runs the following commands

#!/bin/sh
cp /etc/letsencrypt/live/xxxx.casacam.net/fullchain.pem /etc/jellyfin/ssl/
cp /etc/letsencrypt/live/xxxx.casacam.net/privkey.pem /etc/jellyfin/ssl/
chown root:jellyfin /etc/jellyfin/ssl/.pem
chmod 640 /etc/jellyfin/ssl/.pem
docker restart jellyfin

If I could just schedule this to run once a day on its own then this could possibly be a simple way round the problem, I know this might not be the the most graceful way of doing this but I know you time is precious.