emby.wiki · DOCUMENTATION
Reverse Proxy for Emby (Linux)
Configure an Emby reverse proxy, HTTPS, and automatic renewal on Linux using SSH, Nginx, and Certbot.
This guide is for users with a newly purchased Linux cloud server who want to configure an Nginx reverse proxy and HTTPS for Emby from the command line. It uses SSH, the system package manager, systemd, Nginx, and Certbot throughout, without a web control panel.
Prerequisite This guide covers the reverse proxy and HTTPS, not installing Emby or building a media library. A new server can act as the Nginx entry point, but a working Emby origin must already be available. For a single-server deployment, install and start Emby first. For two servers, confirm that Emby works on the other server first.
1. Deployment architecture#
Clients connect to https://emby.example.com. Nginx receives the connection on port 443, handles TLS, and forwards requests over HTTP to Emby on port 8096.
| Deployment | Client entry point | Nginx to Emby | Use case |
|---|---|---|---|
| Single server | Public Nginx domain, HTTPS 443 | http://127.0.0.1:8096 | Emby and Nginx run on the same host |
| Two servers | Public Nginx domain, HTTPS 443 | http://10.0.0.20:8096 | The hosts connect over a trusted private network |
Public access normally needs only TCP 80, 443, and the management SSH port, which defaults to 22. Port 80 provides HTTP redirects and Let's Encrypt validation; port 443 serves clients. Restrict Emby port 8096 to the local host or private network rather than opening it to the entire Internet by default.
If traffic between the servers crosses the Internet, establish a trusted encrypted tunnel first or use an HTTPS upstream with certificate verification. HTTPS between the client and Nginx does not automatically encrypt HTTP between Nginx and Emby.
2. Parameters and command conventions#
| Placeholder | Meaning | Example |
|---|---|---|
YOUR_DOMAIN | Domain pointing to Nginx, without a scheme, path, or port | emby.example.com |
EMBY_HOST | Emby address reachable from Nginx | 127.0.0.1 or 10.0.0.20 |
EMBY_PORT | Actual Emby HTTP listening port | 8096 |
YOUR_EMAIL | Email address for certificate account notices | admin@example.com |
Single-server example:
YOUR_DOMAIN = emby.example.com
EMBY_HOST = 127.0.0.1
EMBY_PORT = 8096
Two-server example:
YOUR_DOMAIN = emby.example.com
EMBY_HOST = 10.0.0.20
EMBY_PORT = 8096
Replace placeholders before running commands The assignments above describe parameters; they are not shell commands. Manually replace
YOUR_DOMAIN,EMBY_HOST,EMBY_PORT, andYOUR_EMAILthroughout this guide. The shell will not expand them automatically. By contrast,$host,$scheme, and$http_upgradein Nginx are native Nginx variables and must remain unchanged.
Except for SSH login commands, run commands on the Nginx server unless they explicitly say “Emby server.” Commands include sudo; you can omit it after switching to root with sudo -i. Run only the installation commands for your distribution.
3. Log in and identify the system#
From a local terminal, log in using the username, IP address, and authentication method supplied by your cloud provider:
ssh root@SERVER_IP
If the provider supplies a regular user, use the appropriate command:
ssh ubuntu@SERVER_IP
ssh debian@SERVER_IP
For a custom SSH port or a specific private key, for example:
ssh -p 2222 -i ~/.ssh/YOUR_PRIVATE_KEY ubuntu@SERVER_IP
Check your identity after login. A regular user can obtain administrative access with:
whoami
sudo -i
Identify the distribution, kernel, CPU architecture, and network:
cat /etc/os-release
uname -a
uname -m
ip addr
ip route
For Connection timed out, check the public IP, SSH port, cloud security group, and system firewall. For Permission denied, first check the username, password or SSH private key, and whether the server permits that authentication method. Common initial usernames include root, ubuntu, debian, rocky, and ec2-user; follow your provider's instructions.
Keep management access available Keep the current SSH window open when changing firewall rules, and confirm that you can log in from a second terminal. If SSH uses another port such as 2222, replace port 22 in subsequent commands with the actual port.
4. Verify the Emby origin#
Do not configure Nginx until the Emby origin works. Confirm that the origin responds to HTTP requests before configuring the reverse proxy.
Single server#
curl -I http://127.0.0.1:8096/
curl -v http://127.0.0.1:8096/
sudo ss -lntp | grep 8096
The HTTP response may be a page or a normal redirect; the root path does not have to return 200. What matters is a successful connection and a response from Emby. If HEAD requests are unsupported, use the GET request from curl -v and a browser test.
The listening address may be 127.0.0.1:8096 or 0.0.0.0:8096. The former allows local access only. The latter listens on every IPv4 interface, so firewall rules must still restrict public access.
Two servers#
Test from the Nginx server:
curl -v http://EMBY_HOST:EMBY_PORT/
nc -vz EMBY_HOST EMBY_PORT
Install missing diagnostic tools using the relevant package manager. nc is an additional TCP check; curl is the main test.
| System | Install curl and nc |
|---|---|
| Debian / Ubuntu | sudo apt update, then sudo apt install -y curl netcat-openbsd |
| Rocky / AlmaLinux / RHEL / CentOS Stream / Fedora | sudo dnf install -y curl nmap-ncat |
| openSUSE | sudo zypper install curl netcat-openbsd |
| Arch Linux | sudo pacman -Syu curl openbsd-netcat |
If the origin is unreachable, first run sudo ss -lntp | grep 8096 on the Emby server. No output may mean that the service is stopped or the port has changed. If it listens only on 127.0.0.1, the other server cannot connect. A separate origin must listen on a reachable private address and accept connections only from Nginx. See section 16 for source restrictions.
5. Configure DNS and IPv4 / IPv6#
Create an A record in your domain's DNS settings pointing to the Nginx server's public IPv4, not Emby's private IP. For example:
Record type:A
Hostname: emby
Target IPv4: 203.0.113.10
Final domain:emby.example.com
203.0.113.10 is a documentation address; replace it with your actual public IP. If your DNS provider offers a proxy or CDN toggle, use “DNS only” for this direct-access setup. Adding another proxy changes the request path, certificate checks, and source IP handling.
getent ahosts YOUR_DOMAIN
dig +short YOUR_DOMAIN A
dig +short YOUR_DOMAIN AAAA
If dig is missing, install the appropriate tools:
| System | Command |
|---|---|
| Debian / Ubuntu | sudo apt install -y dnsutils |
| RPM-based systems | sudo dnf install -y bind-utils |
| openSUSE | sudo zypper install bind-utils |
| Arch Linux | sudo pacman -S bind |
Do not publish an AAAA record unless IPv6 actually works. An incorrect AAAA can send some devices to the wrong address and break Let's Encrypt validation. IPv6 requires a correct AAAA record, reachable server IPv6, IPv6 security group and firewall rules, and an IPv6 Nginx listener.
6. Allow traffic in the cloud security group#
An open system firewall does not mean the cloud platform allows traffic. Check inbound rules in the server's cloud security group or cloud firewall:
| TCP port | Purpose | Suggested source |
|---|---|---|
| 22, or the actual SSH port | SSH management | Restrict to your management IP wherever possible |
| 80 | HTTP and HTTP-01 validation | Public sources required for the service |
| 443 | HTTPS | Public sources required for the service |
Add corresponding IPv6 rules if you provide IPv6 service. If the server is behind a router, forward ports 80 and 443 to Nginx. Do not add port 8096 to rules allowing every public address.
7. Install Nginx#
Debian / Ubuntu#
sudo apt update
sudo apt install -y nginx curl ca-certificates
sudo systemctl enable --now nginx
If a new server needs system updates, run sudo apt upgrade -y only after considering the changes and possible reboot. A full system upgrade is not a required reverse proxy configuration step.
Rocky / AlmaLinux / RHEL / CentOS Stream#
sudo dnf install -y nginx curl ca-certificates
sudo systemctl enable --now nginx
Use sudo dnf update -y when a system update is needed. On RHEL, package availability depends on subscriptions and enabled repositories. Check official repository availability if a package cannot be found.
Fedora#
sudo dnf install -y nginx curl ca-certificates
sudo systemctl enable --now nginx
Use sudo dnf upgrade --refresh -y when system updates are needed.
openSUSE#
sudo zypper refresh
sudo zypper install nginx curl ca-certificates
sudo systemctl enable --now nginx
Arch Linux#
Avoid refreshing Arch repositories without upgrading the system:
sudo pacman -Syu nginx curl ca-certificates
sudo systemctl enable --now nginx
Check the installation#
command -v nginx
sudo nginx -v
sudo nginx -t
sudo systemctl status nginx --no-pager -l
curl -I http://127.0.0.1/
A default Welcome page is normal immediately after installation; the Emby proxy is not configured yet. For nginx: command not found, check that installation succeeded and that the command directory is in PATH. If the service fails to start, inspect:
sudo journalctl -u nginx -n 100 --no-pager
sudo journalctl -xeu nginx
Section 18 covers port conflicts and configuration errors. Do not terminate unidentified processes or delete other sites to resolve a conflict.
8. Configure the system firewall#
Check which firewall the system already uses, and apply only the rules suited to that environment. Do not blindly layer UFW and firewalld onto a server with existing rules.
UFW#
If UFW is missing on Debian / Ubuntu:
sudo apt install -y ufw
Allow the actual SSH port before enabling the firewall, then allow HTTP and HTTPS:
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose
Replace port 22 first if SSH uses another port. For IPv6, check the IPv6 setting in /etc/default/ufw and the IPv6 rules in the status output. After enabling UFW, verify a new SSH login from another terminal before closing the old connection.
firewalld#
If the package is missing on an RPM-based system:
sudo dnf install -y firewalld
On openSUSE, use sudo zypper install firewalld. If a firewall already exists, confirm the current SSH policy before enabling a new service. A new environment using the default SSH service can run:
sudo systemctl enable --now firewalld
sudo firewall-cmd --get-active-zones
Select the actual zone containing the public network interface. The following example uses public; replace every occurrence if your interface uses another zone:
sudo firewall-cmd --zone=public --add-service=ssh
sudo firewall-cmd --zone=public --permanent --add-service=ssh
sudo firewall-cmd --zone=public --permanent --add-service=http
sudo firewall-cmd --zone=public --permanent --add-service=https
sudo firewall-cmd --reload
sudo firewall-cmd --zone=public --list-all
For a custom SSH port, add both runtime and permanent rules such as --add-port=2222/tcp first; the default ssh service alone is insufficient. Before remotely enabling firewalld, confirm that the initial zone allows management access and prepare a recovery route through the cloud console.
9. Check SELinux#
Rocky, AlmaLinux, RHEL, CentOS Stream, and Fedora may have SELinux enabled:
getenforce
If the result is Enforcing, allow Nginx's web service domain to connect to upstream services:
sudo setsebool -P httpd_can_network_connect 1
getsebool httpd_can_network_connect
The check should include:
httpd_can_network_connect --> on
If local curl reaches Emby but Nginx returns 502 with permission errors in its log, check SELinux. Keep its protections enabled rather than disabling SELinux temporarily or permanently to bypass the issue.
10. Configure and test the HTTP reverse proxy#
Verify forwarding from Nginx to Emby before adding TLS. This separates origin, proxy, and certificate problems.
Choose a configuration file and make a backup#
Inspect the existing configuration first:
sudo nginx -T
sudo cp -a /etc/nginx /root/nginx-backup-$(date +%Y%m%d-%H%M%S)
Debian / Ubuntu:
sudo nano /etc/nginx/sites-available/emby
Rocky / AlmaLinux / RHEL / CentOS Stream / Fedora:
sudo nano /etc/nginx/conf.d/emby.conf
Package configurations on openSUSE and Arch can differ. Inspect the include directives inside http { ... } in /etc/nginx/nginx.conf first. Choose a directory already included in the http context. If none is suitable, create /etc/nginx/conf.d and add this line once inside the existing http { ... } block:
include /etc/nginx/conf.d/*.conf;
Then edit /etc/nginx/conf.d/emby.conf. Do not replace the entire nginx.conf with this site's configuration, or include a file containing map inside a server or location block.
Install nano if needed: sudo apt install -y nano on Debian / Ubuntu, sudo dnf install -y nano on RPM-based systems, sudo zypper install nano on openSUSE, or sudo pacman -S nano on Arch. In nano, save with Ctrl+O and Enter, then exit with Ctrl+X.
Complete HTTP configuration#
Replace the three placeholders before saving:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
listen [::]:80;
server_name YOUR_DOMAIN;
client_max_body_size 0;
location / {
proxy_pass http://EMBY_HOST:EMBY_PORT;
proxy_http_version 1.1;
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;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_buffering off;
proxy_request_buffering off;
}
}
For a single server, use server_name emby.example.com; and proxy_pass http://127.0.0.1:8096;. Enclose an IPv6 upstream address in brackets, for example proxy_pass http://[fd00::20]:8096;.
map belongs in the http context and must be defined only once globally. Reuse an existing identical $connection_upgrade map instead of defining it again. On a system without IPv6 support, remove the [::] listeners and do not publish AAAA records; make the same changes in the final configuration below.
Enable, test, and reload#
Debian / Ubuntu also needs a symlink:
sudo ln -s /etc/nginx/sites-available/emby /etc/nginx/sites-enabled/emby
ls -l /etc/nginx/sites-enabled/
If you see File exists, check whether the existing link points to the correct file rather than forcibly overwriting it. On other distributions, saving in the correct included directory is sufficient.
Test after every change:
sudo nginx -t
Run the following only after seeing syntax is ok and test is successful:
sudo systemctl reload nginx
Always follow “edit → nginx -t → reload.” Routine configuration changes do not need repeated restarts. If the service is not running, fix startup problems and start it first.
Test HTTP#
curl -I http://YOUR_DOMAIN/
curl -v http://YOUR_DOMAIN/
Before DNS takes effect, specify the Host locally on Nginx:
curl -v -H "Host: YOUR_DOMAIN" http://127.0.0.1/
This proves only local virtual host matching and forwarding. Also test domain access from an external network. Confirm that the Emby page loads in a browser. HTTP is only an intermediate test; perform normal login and playback acceptance tests after HTTPS is ready.
If the Welcome page appears, first check DNS, server_name, and whether the configuration is enabled. The default Debian / Ubuntu symlink is /etc/nginx/sites-enabled/default. Remove it only after confirming that no other site on the server depends on it:
sudo rm /etc/nginx/sites-enabled/default
Run sudo nginx -t successfully before reloading afterward. Removing the default site is optional; do not delete other configurations.
11. WebSocket and streaming parameters#
The complete configuration already includes these directives; do not add duplicate copies.
| Directive | Purpose |
|---|---|
proxy_http_version 1.1; | Explicitly use HTTP/1.1 for upstream communication |
Upgrade, Connection, and map | Forward WebSocket upgrades for live status and persistent connections |
proxy_read_timeout 3600s; | Timeout between successive reads from the upstream |
proxy_send_timeout 3600s; | Timeout between successive writes to the upstream |
proxy_buffering off; | Pass upstream responses promptly without normal proxy response buffering |
proxy_request_buffering off; | Stream request bodies instead of fully buffering them before forwarding |
3600 seconds does not mean a video can play for only one hour or that the entire request has that duration limit. These timeouts primarily control waits between read or write operations. Investigate interrupted playback using logs, origin load, and network checks rather than endlessly increasing timeouts.
Normal Range requests pass through the proxy normally. Without a specific problem, do not rewrite Range or If-Range: incorrect changes can break seeking and partial requests. Do not blindly add shared caching for Emby APIs, login responses, or media requests.
12. Configure Emby#
Open Dashboard → Network in Emby and use the equivalent names shown by your version:
| Setting | Suggested value |
|---|---|
| External Domain | YOUR_DOMAIN, for example emby.example.com |
| Public HTTPS Port | 443 |
| Secure Connection Mode | Handled by reverse proxy |
| Local HTTP Port | Keep the actual origin port, normally 8096 |
Do not include https://, a path, or a port in External Domain. If configuring the public HTTP port, this guide uses port 80; it does not change Emby's local listener. TLS terminates at Nginx, so this setup does not require copying Nginx's private key to Emby.
For remote access, allow remote connections for both the server and the relevant users. If your version offers a setting to inspect proxy headers for the real client IP, confirm that it suits your proxy environment. Restrict the sources allowed to reach 8096 so clients cannot bypass Nginx and forge those headers. Disable unnecessary automatic port mapping to prevent a router from exposing Emby's port.
The public address on port 443 will be unavailable until HTTPS is configured. Complete the following steps, test the final HTTPS address, and apply settings as the interface requires.
13. Verify ACME and obtain a certificate#
Create the webroot and a test file#
After the HTTP proxy works, run:
sudo mkdir -p /var/www/letsencrypt/.well-known/acme-challenge
echo test | sudo tee /var/www/letsencrypt/.well-known/acme-challenge/test
Inside the existing port 80 server block, add this alongside location /:
location ^~ /.well-known/acme-challenge/ {
root /var/www/letsencrypt;
default_type text/plain;
try_files $uri =404;
}
Keep the existing map and Emby proxy configuration. This root maps requests to /var/www/letsencrypt/.well-known/acme-challenge/; do not repeat .well-known in the root path.
sudo nginx -t
After a successful test:
sudo systemctl reload nginx
curl -fsS http://YOUR_DOMAIN/.well-known/acme-challenge/test
The response must be:
test
Also request this URL from a network outside the server. Local access alone does not prove public validation works. HTTP-01 must retrieve the challenge file publicly on port 80. Login pages, challenge screens, incorrect proxy routes, and site-wide redirects must not intercept this path.
On SELinux systems, investigate file permissions and security contexts if access to the directory is denied. To label a custom webroot explicitly, use:
sudo semanage fcontext -a -t httpd_sys_content_t '/var/www/letsencrypt(/.*)?'
sudo restorecon -Rv /var/www/letsencrypt
If the same rule exists already, modify it with semanage fcontext -m rather than adding a duplicate. RPM-based systems typically provide semanage in policycoreutils-python-utils. Do not make the webroot or private key directory writable by everyone.
Install Certbot#
Debian / Ubuntu:
sudo apt update
sudo apt install -y certbot
Fedora and RPM repositories that provide Certbot:
sudo dnf install -y certbot
openSUSE:
sudo zypper install certbot
Arch Linux:
sudo pacman -S certbot
Rocky and AlmaLinux may need their supported EPEL repository; RHEL also involves subscription repositories. If the package cannot be found, choose the appropriate method for your distribution using the official Certbot installation instructions. Do not add unknown repositories or mix multiple Certbot installations.
Obtain the certificate and confirm its paths#
The simplest interactive method is:
sudo certbot certonly \
--webroot \
-w /var/www/letsencrypt \
-d YOUR_DOMAIN
Alternatively, provide your email explicitly and run this after reading and accepting Let's Encrypt's terms of service:
sudo certbot certonly \
--webroot \
-w /var/www/letsencrypt \
-d YOUR_DOMAIN \
-m YOUR_EMAIL \
--agree-tos
Choose one method; do not request the same certificate repeatedly. certonly obtains the certificate but does not configure Nginx HTTPS for you. After success, inspect the actual paths reported by Certbot:
sudo certbot certificates
sudo ls -l /etc/letsencrypt/live/YOUR_DOMAIN/
They are usually:
/etc/letsencrypt/live/YOUR_DOMAIN/fullchain.pem
/etc/letsencrypt/live/YOUR_DOMAIN/privkey.pem
If a certificate with the same name already exists, its directory may have a suffix such as -0001. Use the real paths reported by Certbot in the final configuration rather than guessing from the domain name.
If issuance fails#
Request the test file again and confirm it still returns test:
curl -v http://YOUR_DOMAIN/.well-known/acme-challenge/test
dig +short YOUR_DOMAIN A
dig +short YOUR_DOMAIN AAAA
sudo ss -lntp | grep ':80'
sudo tail -n 100 /var/log/nginx/error.log
Use sudo ufw status verbose or sudo firewall-cmd --zone=public --list-all according to your firewall, and check the cloud security group. For 403, first inspect read permissions, SELinux, and access rules. For 404, inspect the webroot, virtual host, and path. For timeouts, inspect DNS, networking, and port 80.
Check /var/log/letsencrypt/letsencrypt.log. If an AAAA record exists, IPv6 must serve the correct validation content too; do not assume the validator always falls back to IPv4. Fix the cause before retrying to avoid issuance rate limits. Section 18 covers general TLS diagnostics.
14. Final HTTPS configuration and verification#
Complete configuration#
After obtaining the certificate, replace the contents of the Emby site file created in this guide. Do not append this after the old servers with the same name or overwrite other sites. Keep one global map; omit the duplicate below if a shared configuration already defines it.
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
listen [::]:80;
server_name YOUR_DOMAIN;
location ^~ /.well-known/acme-challenge/ {
root /var/www/letsencrypt;
default_type text/plain;
try_files $uri =404;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name YOUR_DOMAIN;
ssl_certificate /etc/letsencrypt/live/YOUR_DOMAIN/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/YOUR_DOMAIN/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
client_max_body_size 0;
location / {
proxy_pass http://EMBY_HOST:EMBY_PORT;
proxy_http_version 1.1;
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;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_buffering off;
proxy_request_buffering off;
}
}
Replace the domain, upstream, and certificate paths. fullchain.pem contains the server certificate and intermediate chain. privkey.pem is private: do not publish it or make it readable by everyone. client_max_body_size 0 removes the request body limit; set an appropriate limit such as 1g if your upload policy requires one.
The ACME path remains on HTTP; other HTTP requests return 301. Do not add a blanket return 301 at the HTTP server's top level that intercepts validation requests. TLS 1.3 requires suitable Nginx and OpenSSL support; use current packages maintained by your distribution.
Test and load#
sudo nginx -t
Only after success, run:
sudo systemctl reload nginx
curl -I http://YOUR_DOMAIN/
Confirm that normal HTTP paths return 301 with an HTTPS Location, while the ACME test path still returns test:
curl -fsS http://YOUR_DOMAIN/.well-known/acme-challenge/test
curl -I https://YOUR_DOMAIN/
curl -v https://YOUR_DOMAIN/
The HTTPS root may return 200 or a normal Emby redirect. Skipping certificate verification does not count as a successful acceptance test.
Check the TLS handshake, certificate chain, and hostname:
openssl s_client \
-connect YOUR_DOMAIN:443 \
-servername YOUR_DOMAIN \
-verify_hostname YOUR_DOMAIN \
-verify_return_error
Confirm the certificate matches the domain, is within its validity period, and shows Verify return code: 0 (ok). Exit with Ctrl+C. Also access https://YOUR_DOMAIN from a browser and mobile client on an external network, checking login, images, subtitles, playback, and seeking.
15. Automatic renewal and certificate loading#
Check renewal and existing scheduled jobs#
sudo certbot renew --dry-run
systemctl list-timers --all | grep -i certbot
systemctl list-unit-files | grep -i certbot
A successful dry run means current validation and renewal broadly work. It does not prove that scheduling is enabled or that Nginx will automatically load renewed certificates. Some packages use cron; check Certbot entries in /etc/cron.d/ too.
Timer names vary by installation method, for example certbot.timer or certbot-renew.timer. Confirm the actual timer exists before enabling it. If certbot.timer exists:
sudo systemctl enable --now certbot.timer
If no automated job exists, add one using the official instructions for your installation method rather than creating multiple duplicate renewal jobs.
Reload Nginx after successful renewal#
This guide uses certonly --webroot. Configure a deployment hook to test the configuration and reload Nginx after a successful renewal:
sudo mkdir -p /etc/letsencrypt/renewal-hooks/deploy
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh > /dev/null <<'EOF'
#!/bin/sh
set -eu
nginx -t
systemctl reload nginx
EOF
sudo chmod 755 /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
sudo /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
Ensure this filename does not overwrite an existing different hook. Reuse an existing equivalent hook. Certbot versions supporting --run-deploy-hooks can also test:
sudo certbot renew --dry-run --run-deploy-hooks
If that option is unsupported, retain the normal dry run and test the hook above separately. Renewal requires port 80, the ACME location, correct DNS, and a readable webroot to remain available.
16. Two-server deployment and upstream protection#
Private addresses in this example:
Nginx IP = 10.0.0.10
Emby IP = 10.0.0.20
Emby HTTP Port = 8096
Confirm that the hosts actually share a routable private network. Writing IP addresses from different cloud providers as 10.0.0.x does not create a private network between them.
Check the listener on the Emby server:
sudo ss -lntp | grep 8096
Emby must listen on a reachable private interface, not only loopback. With UFW, allow Nginx's private address:
sudo ufw allow from 10.0.0.10 to any port 8096 proto tcp
sudo ufw status numbered
This allow rule does not remove an existing public allow rule. Confirm UFW is enabled, the default inbound policy is appropriate, and no old rule still opens 8096 to any source. Carefully remove incorrect rules using their actual numbers without affecting SSH. The cloud security group must also allow 8096 only from Nginx's actual source address.
With firewalld, add a source restriction in the actual zone containing Emby's private interface. This example uses internal; check interface bindings and zone rules first:
sudo firewall-cmd --get-active-zones
sudo firewall-cmd --zone=internal --permanent --add-rich-rule='rule family="ipv4" source address="10.0.0.10/32" port port="8096" protocol="tcp" accept'
sudo firewall-cmd --reload
sudo firewall-cmd --zone=internal --list-all
If the zone already accepts all inbound traffic or has broad rules for 8096, adding this rule alone does not restrict access. Adjust the existing policy accordingly.
Verify back on the Nginx server:
curl -v http://10.0.0.20:8096/
nc -vz 10.0.0.20 8096
Change the upstream to proxy_pass http://10.0.0.20:8096;, then test with nginx -t and reload. Do not expose 8096 publicly for convenience.
17. Security and maintenance#
- Open only necessary public entry points: HTTP 80, HTTPS 443, and SSH restricted to management sources where possible.
- Keep Emby reachable only through loopback on a single server where possible. For separate servers, allow only trusted Nginx addresses. Check exposure over both IPv4 and IPv6.
- Use strong passwords, maintained packages, and appropriate Emby user permissions. Normal playback users do not need unnecessary administrator or media deletion permissions.
- Keep SELinux and the firewall enabled. Back up Nginx configurations before changes, and test before every reload.
- Never publish certificate private keys. Do not share complete diagnostic logs containing accounts, tokens, or sensitive request parameters.
- If adding a CDN, another proxy, or a tunnel, reassess certificate validation, timeouts, real IP trust, and proxy headers. This baseline configuration assumes Nginx is the first public proxy.
- Regularly check scheduled renewal results and the validity of the certificate loaded by Nginx, and test extended playback.
18. Troubleshooting#
502 Bad Gateway#
Check the upstream from Nginx first. Run listener checks on the server hosting Emby:
curl -v http://EMBY_HOST:EMBY_PORT/
sudo ss -lntp | grep 8096
sudo nginx -T | grep -n proxy_pass
sudo tail -n 100 /var/log/nginx/error.log
Replace 8096 if Emby's port changed. For Connection refused, first check the service, listening address, and port. For connection timeouts, check the upstream network and access rules. For Permission denied on SELinux systems, inspect:
getenforce
getsebool httpd_can_network_connect
504 Gateway Timeout#
Inspect origin response time and load:
time curl -v http://EMBY_HOST:EMBY_PORT/
top
free -h
df -h
sudo tail -f /var/log/nginx/error.log
For separate servers, run resource checks on Emby too. Distinguish a stuck origin, CPU transcoding bottleneck, unavailable GPU, insufficient disk space or I/O waits, upstream networking, and proxy timeouts. df -h shows capacity, not I/O performance. Do not set timeouts to several days to hide performance problems.
413 Request Entity Too Large#
Check the effective client_max_body_size:
sudo nginx -T | grep -n client_max_body_size
client_max_body_size 0;
Value 0 removes the request body limit. To impose a limit, use a value such as client_max_body_size 1g;. Test before reloading after changes. If another proxy precedes Nginx, check its limits too.
WebSocket failures#
sudo nginx -T | grep -n -E 'Upgrade|Connection|proxy_http_version'
Confirm the active location includes HTTP/1.1, Upgrade, and Connection, with one valid map. Browser developer tools can show whether the WebSocket request upgrades successfully to 101. A working page does not prove live connections work.
Playback disconnects after a while#
sudo tail -f /var/log/nginx/error.log
Confirm that proxy_read_timeout 3600s;, proxy_send_timeout 3600s;, and proxy_buffering off; take effect. Also check Emby's transcoding CPU, GPU, disk I/O, upstream bandwidth, and client network. If only seeking fails, inspect Range responses and custom header or cache rules; remove unverified rewrites first.
DNS failures or only some devices cannot connect#
dig +short YOUR_DOMAIN A
dig +short YOUR_DOMAIN AAAA
getent ahosts YOUR_DOMAIN
curl -4 -I https://YOUR_DOMAIN/
curl -6 -I https://YOUR_DOMAIN/
Require curl -6 success only if working IPv6 is configured. Check that A and AAAA point to the same service entry point, DNS caches have updated, and both address families are allowed through. Do not leave an AAAA record pointing to an old server.
HTTPS, certificate, or TLS errors#
For issuance failures, follow section 13 to check ACME, DNS, and port 80. If HTTPS fails after configuration, inspect the port 443 listener, certificate hostname, validity period, system time, chain, and actual paths:
sudo certbot certificates
sudo nginx -T | grep -n -E 'ssl_certificate|listen.*443|server_name'
timedatectl status
Missing certificate paths, incorrect private key permissions, or a certificate/key mismatch cause nginx -t to fail. If renewal succeeded but browsers still receive the old certificate, check the deployment hook and whether Nginx reloaded successfully. Do not “fix” errors by disabling certificate verification.
Syntax errors, duplicate listeners, and the Welcome page#
sudo nginx -t
sudo nginx -T
sudo nginx -T | grep -n "listen 80"
sudo nginx -T | grep -n default_server
For unknown directive, check spelling, module support, and directive context. map directive is not allowed here means the map is in the wrong context. For duplicate listen, check repeated declarations or conflicting options inside the same server; multiple ordinary sites sharing port 80 is normal. For duplicate default server, check repeated default_server assignments for the same address and port.
For the Welcome page, check domain matching, includes, and Debian / Ubuntu enablement symlinks. Handle the default site carefully as described in section 10. Do not publish sensitive information from nginx -T output.
Port 80 / 443 conflicts#
sudo ss -lntp | grep -E ':80\b|:443\b'
sudo lsof -iTCP:80 -sTCP:LISTEN
sudo lsof -iTCP:443 -sTCP:LISTEN
Install lsof if needed, for example with sudo apt install -y lsof or sudo dnf install -y lsof. Common listeners include Apache, httpd, Caddy, another Nginx installation, and Docker containers. Check:
sudo systemctl status apache2 --no-pager -l
sudo systemctl status httpd --no-pager -l
Run checks appropriate to your distribution. Identify the service and other sites before changing ports or stopping services. Avoid kill -9 or blindly disabling a working entry point.
Nginx logs#
sudo tail -f /var/log/nginx/access.log
sudo tail -f /var/log/nginx/error.log
sudo tail -n 100 /var/log/nginx/error.log
sudo journalctl -u nginx -n 100 --no-pager
If the site defines separate access_log or error_log files, use those paths. Exit tail -f with Ctrl+C. Correlate request times, status codes, and upstream errors to locate the fault.
19. Final checklist#
- Linux distribution, SSH user, and actual management port confirmed
- Emby responds on 8096 or the actual origin port
- DNS A record points to Nginx
- AAAA checked; incorrect or unused records removed
- Cloud security group allows 80 / 443 and necessary SSH access
- System firewall allows 80 / 443 and a new SSH login works
- Nginx installed and enabled at startup
- Original configuration backed up; other sites preserved
- nginx -t succeeds
- HTTP reverse proxy works
- SELinux configured appropriately for the environment
- WebSocket headers and map work
- Emby External Domain is correct
- Public HTTPS Port = 443
- Secure Connection Mode = Handled by reverse proxy
- ACME challenge accessible from an external network
- HTTPS certificate obtained; actual certificate paths configured
- Normal HTTP requests redirect to HTTPS; ACME path preserved
- HTTPS hostname, validity, and certificate chain verified
- Emby login works
- Posters and images work
- Subtitles work
- Video playback works
- Seeking works
- Mobile client works
- Extended playback works
- certbot renew --dry-run succeeds
- Automatic renewal job confirmed enabled
- Post-renewal configuration test and Nginx reload hook verified
- Emby 8096 has no unnecessary public exposure, including IPv6
Official references: Nginx proxy module, Emby network settings, Let's Encrypt challenge types, Let's Encrypt IPv6 support, and Certbot usage and renewal.

