emby.wiki · DOCUMENTATION
Reverse Proxy for Emby (Windows Server)
Configure an Emby reverse proxy, HTTPS, startup, and automatic renewal on Windows Server using PowerShell, Nginx, win-acme, and Task Scheduler.
This guide starts with a newly installed Windows Server and uses administrator PowerShell, Nginx for Windows, Windows Firewall, win-acme, and Task Scheduler to configure an Emby reverse proxy. Installing Emby and building a media library are outside its scope. A working Emby origin must be ready before configuring the entry server.
1. Limitations of Nginx for Windows#
NGINX still considers its Windows version beta. Connection handling, performance, and scalability have limitations. Although multiple workers can start, the official documentation states that only one actually performs work, and UDP / QUIC is unsupported. Windows Server can run the proxy in this guide, but Linux is generally more suitable for sustained production workloads with high concurrency or traffic.
The official Windows ZIP contains a console application; it does not automatically register a Windows service. This guide uses Task Scheduler for startup, win-acme's scheduled task for automatic renewal, and a configuration test before reloading after renewal.
Download the latest mainline release recommended by the official NGINX Windows documentation. The win-acme example queries the latest official release dynamically rather than pinning an old version. Check the NGINX Windows documentation and win-acme system requirements at deployment time. Do not keep using old packages on an unsupported Windows system.
2. Parameters and administrator PowerShell#
Log in through RDP or an already configured SSH service, then open Windows PowerShell with Run as administrator. The file-writing and network diagnostic commands below support Windows PowerShell 5.1 and do not require a GUI control panel.
whoami
net session
Get-ComputerInfo |
Select-Object WindowsProductName, WindowsVersion, OsArchitecture
whoami shows the current identity but does not prove the window is elevated. If net session returns Access is denied., reopen PowerShell as administrator. If it reports that the Server service is stopped, that result alone cannot establish your privilege level. Check the current token directly:
$Identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$Principal = [Security.Principal.WindowsPrincipal]::new($Identity)
$Principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
The result should be True. Run systeminfo if you need more system details. The download examples use x64 ZIP packages; confirm that OsArchitecture is 64-bit and check current software requirements.
Use these parameters. The domain must not include a scheme, path, or port:
| Parameter | Meaning | Single-server example | Two-server example |
|---|---|---|---|
YOUR_DOMAIN | Domain pointing to the public Nginx entry point | emby.example.com | emby.example.com |
EMBY_HOST | Emby address reachable from Nginx | 127.0.0.1 | 10.0.0.20 |
EMBY_PORT | Emby HTTP port | 8096 | 8096 |
YOUR_EMAIL | ACME account contact email | Your valid email address | Your valid email address |
Set actual values in the same administrator PowerShell session:
$YOUR_DOMAIN = 'emby.example.com'
$EMBY_HOST = '127.0.0.1'
$EMBY_PORT = 8096
$YOUR_EMAIL = 'admin@example.com'
Replace the domain and email with your own. For separate servers, change $EMBY_HOST to a real private address such as 10.0.0.20. Set these variables again whenever you reopen PowerShell. Braces in ${EMBY_HOST} prevent PowerShell from interpreting the following colon in a URI as scope syntax.
The Nginx templates use YOUR_DOMAIN, EMBY_HOST, and EMBY_PORT, explicitly substituted using .Replace(). Preserve native Nginx variables $host, $remote_addr, $http_upgrade, and $request_uri unchanged.
3. Verify the Emby origin#
Confirm that Emby works before configuring Nginx. Adding a reverse proxy cannot fix an unresponsive origin.
For a single server, test port 8096 and HTTP first:
Test-NetConnection 127.0.0.1 -Port 8096
Invoke-WebRequest -Uri 'http://127.0.0.1:8096/' -UseBasicParsing
TcpTestSucceeded should be True; HTTP should return an Emby page or a normal redirect. Substitute the actual port if it differs.
For two servers, use your configured parameters on the Windows Nginx server:
Test-NetConnection $EMBY_HOST -Port $EMBY_PORT
Invoke-WebRequest -Uri "http://${EMBY_HOST}:$EMBY_PORT/" -UseBasicParsing
The following listener check examines only the machine running it. If Emby runs on another Windows server, run it on that origin:
$Connections = Get-NetTCPConnection -LocalPort 8096 -State Listen `
-ErrorAction SilentlyContinue
$Connections | Select-Object LocalAddress, LocalPort, State, OwningProcess
$PortProcessIds = @($Connections.OwningProcess | Sort-Object -Unique)
if ($PortProcessIds.Count -gt 0) {
Get-Process -Id $PortProcessIds
}
If there is no listener, check whether Emby started or its port changed. 127.0.0.1 permits local access only; a separate origin must listen on a reachable private address. 0.0.0.0 listens on all IPv4 interfaces and requires firewall source restrictions. Do not assign to $PID or $Pid: PowerShell variable names are case-insensitive, and $PID is an automatic system variable.
4. DNS and port checks#
The A record must point to the Windows Nginx server's public IPv4, not Emby's private address. If your DNS provider offers a proxy or CDN toggle, use “DNS only” for this direct-access setup. Enabling a proxy requires additional checks of the request path, streaming restrictions, and source IP handling.
Resolve-DnsName $YOUR_DOMAIN
Resolve-DnsName $YOUR_DOMAIN -Type A
Resolve-DnsName $YOUR_DOMAIN -Type AAAA -ErrorAction SilentlyContinue
Remove incorrect AAAA records if working IPv6 is unavailable. They can cause clients to prefer the wrong address and interfere with ACME validation. IPv6 service requires reachability, firewall and security group rules, and corresponding Nginx IPv6 listeners.
Before installation, check listeners and processes on 80 / 443:
$Listeners = Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue |
Where-Object { $_.LocalPort -in 80, 443 }
$Listeners | Select-Object LocalAddress, LocalPort, OwningProcess
$PortProcessIds = @($Listeners.OwningProcess | Sort-Object -Unique)
if ($PortProcessIds.Count -gt 0) {
Get-Process -Id $PortProcessIds
}
Common occupants include IIS, Apache, an old Nginx instance, or another web server. Identify their purpose before adjusting ports or services; do not simply kill the process. PID 4 / System may indicate HTTP.sys. Use netsh http show servicestate to help identify the actual service.
Get-Service W3SVC -ErrorAction SilentlyContinue
Do not disable W3SVC directly if this server hosts IIS websites. Run the following only after confirming IIS is no longer needed and stopping it will not affect existing sites:
Stop-Service W3SVC
Set-Service W3SVC -StartupType Disabled
5. Download and install Nginx#
Check the current mainline Windows ZIP version on the official NGINX download page and replace CURRENT_VERSION below. Avoid third-party repackaged downloads.
$NginxVersion = 'CURRENT_VERSION'
if ($NginxVersion -eq 'CURRENT_VERSION') {
throw 'Set NginxVersion to the current official Windows mainline version.'
}
if (Test-Path 'C:\nginx') {
throw 'C:\nginx already exists. Inspect and back it up before continuing.'
}
New-Item -ItemType Directory -Path 'C:\Temp' -Force | Out-Null
Invoke-WebRequest -UseBasicParsing `
-Uri "https://nginx.org/download/nginx-$NginxVersion.zip" `
-OutFile "C:\Temp\nginx-$NginxVersion.zip"
Expand-Archive -Path "C:\Temp\nginx-$NginxVersion.zip" `
-DestinationPath 'C:\' -ErrorAction Stop
Move-Item -Path "C:\nginx-$NginxVersion" -Destination 'C:\nginx' `
-ErrorAction Stop
Get-ChildItem 'C:\nginx'
The result should contain C:\nginx\nginx.exe and the conf, html, and logs directories. These commands intentionally do not overwrite an existing directory. Back up configuration and certificates separately when upgrading an existing instance.
If an older Windows PowerShell download reports Could not create SSL/TLS secure channel, enable TLS 1.2 for the current session and retry; do not skip certificate verification:
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Use forward slashes in Nginx Windows paths, for example C:/nginx/html and C:/nginx/certs/emby.example.com-chain.pem. Windows filesystem paths in PowerShell can still use backslashes.
Test and start the default configuration:
Set-Location 'C:\nginx'
.\nginx.exe -t
if ($LASTEXITCODE -ne 0) { throw 'Nginx configuration test failed.' }
.\nginx.exe
if ($LASTEXITCODE -ne 0) { throw 'Nginx start failed.' }
Get-Process nginx
tasklist /fi "imagename eq nginx.exe"
Invoke-WebRequest -Uri 'http://127.0.0.1/' -UseBasicParsing
The default page should be accessible locally. Usually a master and a worker appear; two nginx.exe processes alone do not prove duplicate startup. If startup fails, check the error logs in section 17.
6. Windows Firewall and cloud security groups#
Create HTTP and HTTPS inbound rules in administrator PowerShell. These examples are for a new server. Check for existing rules with the same names before repeating them:
New-NetFirewallRule -Name 'EmbyNginxHTTP' `
-DisplayName 'Nginx HTTP 80' -Direction Inbound `
-Protocol TCP -LocalPort 80 -Action Allow -Profile Any
New-NetFirewallRule -Name 'EmbyNginxHTTPS' `
-DisplayName 'Nginx HTTPS 443' -Direction Inbound `
-Protocol TCP -LocalPort 443 -Action Allow -Profile Any
Get-NetFirewallRule -DisplayName 'Nginx HTTP 80', 'Nginx HTTPS 443' |
Select-Object Name, DisplayName, Enabled, Direction, Action, Profile
Get-NetConnectionProfile
Profile Any applies these specific port rules to Domain, Private, and Public profiles; it does not disable the firewall. If existing rules are bound to an incorrect profile and should apply to the current network, modify them:
Set-NetFirewallRule -DisplayName 'Nginx HTTP 80' -Profile Any
Set-NetFirewallRule -DisplayName 'Nginx HTTPS 443' -Profile Any
Check cloud security groups and cloud firewalls independently. A Windows Firewall allow rule does not guarantee public reachability.
| Inbound TCP port | Purpose | Sources |
|---|---|---|
| 80 | HTTP and HTTP-01 validation | Public sources required for the service |
| 443 | HTTPS | Public sources required for the service |
| 3389, or the actual RDP port | Remote management | Restrict to your management IP / VPN where possible |
| 22, or the actual SSH port | Only if SSH is enabled | Restrict to your management IP / VPN where possible |
Do not leave management ports unrestricted to the entire Internet or open public 8096 by default. If the server uses NAT or sits behind a router, forward 80 / 443 to Nginx. Check IPv6 rules when serving IPv6. Do not troubleshoot by disabling the whole Windows Firewall.
7. Configure the HTTP reverse proxy#
Back up the configuration before writing C:\nginx\conf\nginx.conf. The complete configuration includes the ACME path from the start to avoid repeated edits.
Use a single-quoted here-string @' ... '@ so PowerShell does not expand Nginx variables such as $host. Windows PowerShell 5.1 writes a BOM with Set-Content -Encoding UTF8; this guide explicitly writes UTF-8 without BOM through .NET for compatibility across PowerShell versions. The closing '@ must be alone on an unindented line.
$BackupPath = 'C:\nginx\conf\nginx.conf.' + (Get-Date -Format 'yyyyMMdd-HHmmss') + '.bak'
Copy-Item 'C:\nginx\conf\nginx.conf' $BackupPath -ErrorAction Stop
$NginxConfig = @'
worker_processes 1;
error_log logs/error.log;
pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
access_log logs/access.log;
sendfile on;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name YOUR_DOMAIN;
client_max_body_size 0;
location /.well-known/acme-challenge/ {
root C:/nginx/html;
default_type text/plain;
try_files $uri =404;
}
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;
}
}
}
'@
$NginxConfig = $NginxConfig.Replace('YOUR_DOMAIN', $YOUR_DOMAIN)
$NginxConfig = $NginxConfig.Replace('EMBY_HOST', $EMBY_HOST)
$NginxConfig = $NginxConfig.Replace('EMBY_PORT', [string]$EMBY_PORT)
[IO.File]::WriteAllText('C:\nginx\conf\nginx.conf', $NginxConfig,
[Text.UTF8Encoding]::new($false))
& 'C:\nginx\nginx.exe' -t -p C:/nginx/ -c conf/nginx.conf
if ($LASTEXITCODE -ne 0) { throw 'Nginx configuration test failed. Do not reload.' }
& 'C:\nginx\nginx.exe' -s reload -p C:/nginx/ -c conf/nginx.conf
if ($LASTEXITCODE -ne 0) { throw 'Nginx reload failed. Check the error log.' }
The test should report syntax is ok and test is successful. Nonzero native command exit codes do not necessarily throw PowerShell exceptions, so these examples explicitly check $LASTEXITCODE. -p C:/nginx/ fixes the prefix and -c conf/nginx.conf fixes the configuration path, avoiding incorrect files when commands run from another directory.
For every later change, follow back up and edit → successful nginx -t → reload. If testing fails, fix the configuration or restore the backup before reloading.
Management commands follow. Stop commands are only for occasions when shutdown is actually needed:
# Graceful shutdown: stop accepting new connections and finish existing work.
& 'C:\nginx\nginx.exe' -s quit -p C:/nginx/ -c conf/nginx.conf
# Immediate shutdown: may interrupt active playback.
& 'C:\nginx\nginx.exe' -s stop -p C:/nginx/ -c conf/nginx.conf
Use reload for normal changes without stopping first. During manual startup, manage Nginx under the same administrator identity. Section 14 covers management after migration to a SYSTEM startup task.
8. Verify HTTP and WebSocket#
Test-NetConnection $YOUR_DOMAIN -Port 80
Invoke-WebRequest -Uri "http://$YOUR_DOMAIN/" -UseBasicParsing
Before DNS takes effect, test the virtual host locally using Host:
Invoke-WebRequest -Uri 'http://127.0.0.1/' `
-Headers @{ Host = $YOUR_DOMAIN } -UseBasicParsing
After local success, access http://emby.example.com from another external device. Local access alone does not prove that the cloud security group and public entry point work.
The full configuration includes the required WebSocket settings:
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
Upgrade and Connection are hop-by-hop headers that require explicit forwarding. The map sends upgrade for Upgrade requests and close for normal requests. Inspect actual WebSocket connections in browser developer tools under Network / WS; a successful upgrade typically returns 101 Switching Protocols. A working HTTP page does not prove WebSocket works.
Streaming settings also include:
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_buffering off;
proxy_request_buffering off;
These timeouts mainly limit waits between successive read or write operations, not the total video duration. Disabling buffering supports streaming. Do not rewrite Range requests without a specific need. Test login, posters, subtitles, playback, and seeking separately, then test extended playback.
9. Emby settings#
Check the following under Network in Emby's dashboard. Names may vary by version and language; follow the current interface and official Emby network documentation.
| Setting | Target value |
|---|---|
| External Domain | Actual YOUR_DOMAIN, for example emby.example.com |
| Public HTTPS Port | 443 |
| Secure Connection Mode | Handled by reverse proxy or the equivalent translation |
| Local HTTP port | Actual Emby listener, for example 8096 |
External Domain must not include https://, a path, or a port. TLS terminates at Nginx, with HTTP upstream traffic over localhost or a trusted private network. If your Emby version provides proxy trust or proxy header settings, trust only the actual proxy addresses rather than arbitrary public sources. After saving, restart Emby if your version requires it and check the origin again.
10. Prepare HTTPS and ACME#
Windows uses the native ACME client win-acme. It supports command-line operation, filesystem HTTP validation, PEM output, and post-renewal scripts, making it suitable for Nginx certificates.
Create the webroot challenge directory and test file:
New-Item -ItemType Directory `
-Path 'C:\nginx\html\.well-known\acme-challenge' -Force | Out-Null
[IO.File]::WriteAllText('C:\nginx\html\.well-known\acme-challenge\test',
'test', [Text.Encoding]::ASCII)
$ChallengeResponse = Invoke-WebRequest `
-Uri "http://$YOUR_DOMAIN/.well-known/acme-challenge/test" `
-UseBasicParsing
$ChallengeResponse.StatusCode
$ChallengeResponse.Content
Section 7 already includes this location; do not replace the full configuration with an incomplete fragment:
location /.well-known/acme-challenge/ {
root C:/nginx/html;
default_type text/plain;
try_files $uri =404;
}
root appends the request URI to C:/nginx/html, so the webroot parameter must be C:\nginx\html, not the acme-challenge subdirectory itself.
Also request the same test URL from an external network. It must return HTTP 200 and test, not an Emby login page, 404, or interception page. Check DNS, AAAA, port 80 rules, and port forwarding.
Do not request a certificate if the challenge test fails. This guide uses HTTP-01, which requires public TCP 80 access to the challenge file. The final HTTPS configuration preserves this path for automatic renewal.
11. Install and configure win-acme#
Get win-acme from its official website and official GitHub Releases. The getting-started documentation recommends the x64 trimmed ZIP for most 64-bit users, extracted to a permanent directory such as C:\Program Files\win-acme.
This script queries the latest release and matches the current official x64 trimmed ZIP naming pattern. If the pattern changes, no asset matches, or multiple assets match, it stops and lists filenames. Check the official Releases rather than arbitrarily selecting the first file.
$Release = Invoke-RestMethod `
-Uri 'https://api.github.com/repos/win-acme/win-acme/releases/latest'
$Assets = @($Release.assets | Where-Object {
$_.name -match '^win-acme\..*\.x64\.trimmed\.zip$'
})
if ($Assets.Count -ne 1) {
$Release.assets | Select-Object name
throw 'Cannot select one official x64 trimmed ZIP. Check the release assets.'
}
$Asset = $Assets[0]
$Release.tag_name
$Asset.name
if (Test-Path 'C:\Program Files\win-acme\wacs.exe') {
throw 'win-acme already exists. Inspect its configuration before upgrading.'
}
New-Item -ItemType Directory -Path 'C:\Temp' -Force | Out-Null
New-Item -ItemType Directory -Path 'C:\Program Files\win-acme' -Force | Out-Null
Invoke-WebRequest -Uri $Asset.browser_download_url -UseBasicParsing `
-OutFile 'C:\Temp\win-acme.zip'
Expand-Archive -Path 'C:\Temp\win-acme.zip' `
-DestinationPath 'C:\Program Files\win-acme' -ErrorAction Stop
Test-Path 'C:\Program Files\win-acme\wacs.exe'
New-Item -ItemType Directory -Path 'C:\nginx\certs' -Force | Out-Null
Test-Path should return True. Do not install win-acme in a temporary directory that may be cleaned up. The certificate directory is outside the webroot so private keys cannot be served over HTTP. Inspect permissions with icacls C:\nginx\certs: unrelated users must not read private keys or modify certificates. Administrators and the SYSTEM task identity need appropriate access.
Register the reload script before first issuance#
Create the script first. This content uses ASCII and the fixed paths from this guide:
@'
@echo off
"C:\nginx\nginx.exe" -t -p C:/nginx/ -c conf/nginx.conf
if errorlevel 1 exit /b 1
"C:\nginx\nginx.exe" -s reload -p C:/nginx/ -c conf/nginx.conf
if errorlevel 1 exit /b 1
exit /b 0
'@ | Set-Content -Path 'C:\nginx\reload-nginx.cmd' -Encoding ASCII
Issuance and renewal call this script through the installation script plugin. Creating a .cmd file alone does not make win-acme run it. Explicitly configure --installation script --script ... in the issuance command.
Issue and store PEM files#
Confirm that Nginx is running, the challenge file is publicly accessible, and all four parameters are set, then run:
Set-Location 'C:\Program Files\win-acme'
.\wacs.exe `
--source manual `
--host $YOUR_DOMAIN `
--validationmode http-01 `
--validation filesystem `
--webroot 'C:\nginx\html' `
--store pemfiles `
--pemfilespath 'C:\nginx\certs' `
--pemfilesname $YOUR_DOMAIN `
--installation script `
--script 'C:\nginx\reload-nginx.cmd' `
--emailaddress $YOUR_EMAIL `
--accepttos
if ($LASTEXITCODE -ne 0) { throw 'win-acme failed. Inspect its output and logs.' }
Get-ChildItem 'C:\nginx\certs'
--accepttos accepts the selected ACME service's terms. Read them and confirm acceptance before running the command. These parameters were checked against the official win-acme CLI, filesystem validation, PEM store, and installation script documentation.
--pemfilesname fixes the output prefix to the actual domain. Current PEM store naming is below; the actual Get-ChildItem results are authoritative:
| File | Contents | Nginx use |
|---|---|---|
YOUR_DOMAIN-crt.pem | Site certificate | Leaf only; not a replacement for the full chain |
YOUR_DOMAIN-key.pem | Private key | ssl_certificate_key |
YOUR_DOMAIN-chain.pem | Site certificate and intermediate chain | ssl_certificate |
YOUR_DOMAIN-chain-only.pem | Intermediate chain without the site certificate | Not used as ssl_certificate in this guide |
For example, the real files may be emby.example.com-chain.pem and emby.example.com-key.pem. Confirm both exist before writing the HTTPS configuration. If first issuance succeeds but the installation script fails, fix the script or Nginx state, then check renewal management to ensure the renewal item was saved completely. Do not repeatedly issue certificates blindly.
12. Final HTTPS Nginx configuration#
This full template includes the HTTP-01 path, HTTPS redirects for other HTTP requests, TLS 1.2 / 1.3, complete proxy headers, WebSocket, and streaming settings. Certificate names use the prefix confirmed above.
Confirm the files exist, back up the HTTP configuration, and write the configuration without BOM:
$ChainPath = "C:\nginx\certs\$YOUR_DOMAIN-chain.pem"
$KeyPath = "C:\nginx\certs\$YOUR_DOMAIN-key.pem"
if (-not (Test-Path $ChainPath) -or -not (Test-Path $KeyPath)) {
throw 'Certificate chain or private key is missing. Check actual PEM filenames.'
}
$BackupPath = 'C:\nginx\conf\nginx.conf.http.' + (Get-Date -Format 'yyyyMMdd-HHmmss') + '.bak'
Copy-Item 'C:\nginx\conf\nginx.conf' $BackupPath -ErrorAction Stop
$NginxConfig = @'
worker_processes 1;
error_log logs/error.log;
pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
access_log logs/access.log;
sendfile on;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name YOUR_DOMAIN;
location /.well-known/acme-challenge/ {
root C:/nginx/html;
default_type text/plain;
try_files $uri =404;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
server_name YOUR_DOMAIN;
ssl_certificate C:/nginx/certs/YOUR_DOMAIN-chain.pem;
ssl_certificate_key C:/nginx/certs/YOUR_DOMAIN-key.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;
}
}
}
'@
$NginxConfig = $NginxConfig.Replace('YOUR_DOMAIN', $YOUR_DOMAIN)
$NginxConfig = $NginxConfig.Replace('EMBY_HOST', $EMBY_HOST)
$NginxConfig = $NginxConfig.Replace('EMBY_PORT', [string]$EMBY_PORT)
[IO.File]::WriteAllText('C:\nginx\conf\nginx.conf', $NginxConfig,
[Text.UTF8Encoding]::new($false))
& 'C:\nginx\nginx.exe' -t -p C:/nginx/ -c conf/nginx.conf
if ($LASTEXITCODE -ne 0) { throw 'HTTPS configuration test failed. Do not reload.' }
& 'C:\nginx\nginx.exe' -s reload -p C:/nginx/ -c conf/nginx.conf
if ($LASTEXITCODE -ne 0) { throw 'Nginx reload failed. Check the error log.' }
If the real certificate prefix differs from the domain, change both the file checks and the two ssl_certificate paths; changing only one place is insufficient. For IPv6, add listen [::]:80; and listen [::]:443 ssl; in their respective server blocks after checking reachability and firewall rules.
client_max_body_size 0 removes Nginx's request body limit for Emby features. Set a reasonable limit if your upload policy requires one. Fix domain, certificate chain, or system time problems if certificate validation fails rather than permanently ignoring certificate errors.
13. Verify HTTPS#
Test-NetConnection $YOUR_DOMAIN -Port 443
Invoke-WebRequest -Uri "https://$YOUR_DOMAIN/" -UseBasicParsing
Disable automatic redirect following when checking HTTP redirects and read the status and Location. This method works in Windows PowerShell 5.1 without hiding redirect or network errors using -ErrorAction SilentlyContinue:
$Request = [Net.HttpWebRequest]::Create("http://$YOUR_DOMAIN/")
$Request.AllowAutoRedirect = $false
$Response = $Request.GetResponse()
try {
[int]$Response.StatusCode
$Response.Headers['Location']
} finally {
$Response.Close()
}
Expect 301 with a Location pointing to the actual HTTPS domain. Request the challenge file again; it must still return HTTP 200 and test, without redirecting to Emby:
Invoke-WebRequest `
-Uri "http://$YOUR_DOMAIN/.well-known/acme-challenge/test" `
-UseBasicParsing
Test from an external browser and mobile Emby client: login, posters, subtitles, direct playback, transcoding, seeking, WebSocket, persistent connections, and extended playback. The browser should receive a valid certificate for the correct domain with its complete chain. Finish external tests before going live; TCP 443 reachability alone does not prove playback works.
14. Start Nginx automatically at boot#
Use a system startup task so Nginx runs without an administrator login. First create a startup script with a configuration test and an existing-process check to avoid starting another master:
@'
@echo off
tasklist /fi "imagename eq nginx.exe" /nh | find /i "nginx.exe" >nul
if not errorlevel 1 exit /b 0
"C:\nginx\nginx.exe" -t -p C:/nginx/ -c conf/nginx.conf
if errorlevel 1 exit /b 1
"C:\nginx\nginx.exe" -p C:/nginx/ -c conf/nginx.conf
if errorlevel 1 exit /b 1
exit /b 0
'@ | Set-Content -Path 'C:\nginx\start-nginx.cmd' -Encoding ASCII
if (Get-ScheduledTask -TaskName 'Nginx' -ErrorAction SilentlyContinue) {
throw 'A task named Nginx already exists. Inspect it before replacing.'
}
$Action = 'C:\nginx\start-nginx.cmd'
schtasks.exe /Create /TN 'Nginx' /SC ONSTART /RU SYSTEM /RL HIGHEST /TR $Action
if ($LASTEXITCODE -ne 0) { throw 'Nginx startup task creation failed.' }
schtasks.exe /Query /TN 'Nginx' /V /FO LIST
The command does not use /F to overwrite an existing task by default. The process check assumes this is the only Nginx installation on the machine. Multiple instances require distinct prefixes, PID paths, and explicit instance checks.
Migrate the manual instance to SYSTEM#
Schedule a maintenance window, then gracefully quit from the administrator session that started Nginx. Extended playback connections can delay shutdown. Confirm all relevant processes have exited before running the task:
& 'C:\nginx\nginx.exe' -s quit -p C:/nginx/ -c conf/nginx.conf
if ($LASTEXITCODE -ne 0) { throw 'Graceful shutdown failed. Inspect before proceeding.' }
Get-Process nginx -ErrorAction SilentlyContinue
If the process check still returns output, wait for connections to finish and check again. Do not start duplicates or forcibly kill processes. Once no related processes remain, run:
schtasks.exe /Run /TN 'Nginx'
if ($LASTEXITCODE -ne 0) { throw 'Cannot start the Nginx task.' }
Get-Process nginx -ErrorAction SilentlyContinue
Get-ScheduledTask -TaskName 'Nginx' | Get-ScheduledTaskInfo
Invoke-WebRequest -Uri "https://$YOUR_DOMAIN/" -UseBasicParsing
Scheduled task startup is asynchronous. If the process has not appeared yet, inspect the task result and error log instead of repeatedly running it. A completed startup script does not mean the background Nginx process stopped; check the processes and HTTP response too.
Manual reload under SYSTEM#
Coordinate Windows control commands with the running instance's identity and privileges. The startup task and default win-acme renewal task both use SYSTEM. If an administrator's direct -s reload gets access denied, create a manually invoked SYSTEM reload task without a trigger, calling the test script from section 11:
if (Get-ScheduledTask -TaskName 'Nginx Reload' -ErrorAction SilentlyContinue) {
throw 'Nginx Reload already exists. Inspect it before replacing.'
}
$ReloadAction = New-ScheduledTaskAction -Execute 'C:\Windows\System32\cmd.exe' `
-Argument '/c C:\nginx\reload-nginx.cmd' -WorkingDirectory 'C:\nginx'
$ReloadPrincipal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' `
-LogonType ServiceAccount -RunLevel Highest
Register-ScheduledTask -TaskName 'Nginx Reload' -Action $ReloadAction `
-Principal $ReloadPrincipal -Description 'Validate and reload this Nginx instance'
Start-ScheduledTask -TaskName 'Nginx Reload'
Get-ScheduledTask -TaskName 'Nginx Reload' | Get-ScheduledTaskInfo
After task completion, check LastTaskResult; success should be 0. Check HTTPS again. This task does not schedule renewal; win-acme still calls the installation script for renewal. After an actual server reboot, verify Nginx works without login. Creating the task alone does not constitute a successful boot test.
15. Automatic certificate renewal and Nginx reload#
After the first successful certificate creation, win-acme creates a Scheduled Task that checks periodically and renews only when needed. It uses SYSTEM by default. Its paths depend on the installation directory, so do not move or delete that directory casually.
$RenewalTasks = Get-ScheduledTask | Where-Object TaskName -Like '*win-acme*'
$RenewalTasks | Select-Object TaskName, TaskPath, State
$RenewalTasks | ForEach-Object {
$_.Principal | Select-Object UserId, RunLevel
$_.Actions | Select-Object Execute, Arguments, WorkingDirectory
$_ | Get-ScheduledTaskInfo
}
schtasks.exe /Query | Select-String 'win-acme'
Confirm the task exists, is enabled, points to the permanent program directory, and has an appropriate recent result. If it is missing, do not invent a renewal task with uncertain parameters; use the officially supported rebuilding method:
& 'C:\Program Files\win-acme\wacs.exe' --setuptaskscheduler
if ($LASTEXITCODE -ne 0) { throw 'Cannot create or update the win-acme scheduled task.' }
Section 11 registered --installation script --script C:\nginx\reload-nginx.cmd; renewal should retain it. The correct sequence is update PEM files → installation script → nginx -t → reload after success. Updating certificate files alone does not make a running Nginx instance load them.
Verify renewal and the script#
First run the Nginx Reload task from section 14 manually to verify that SYSTEM can read configuration and certificates, test, and reload. Then check the error log. You can also run in an administrator session:
& 'C:\Program Files\win-acme\wacs.exe' --list
& 'C:\Program Files\win-acme\wacs.exe' --renew --verbose
--renew processes only renewals that are due. New certificates may be skipped, so an error-free command does not prove issuance, file updates, and hooks all ran.
If a complete diagnostic run is necessary, obtain the renewal ID with --list, substitute it for YOUR_RENEWAL_ID, and use the officially supported forced renewal:
& 'C:\Program Files\win-acme\wacs.exe' --renew --id YOUR_RENEWAL_ID --force --verbose
This performs a real renewal workflow, potentially using cached data or the production ACME service. It is not an unlimited dry run. Avoid frequent forced issuance that can hit rate limits. If the manually invoked installation script fails because of identity differences, first check saved PEM files and logs, then load them through the SYSTEM reload task instead of repeatedly requesting certificates. Ultimately verify that the default SYSTEM renewal task completes the entire workflow.
Logs are available in Windows Event Viewer and the Log directory for the relevant ACME service under %ProgramData%\win-acme\. Check PEM modification times, script exit results, and the validity period actually received by an external browser. Preserve HTTP-01 and public port 80 after first issuance.
16. Two-server deployment#
Example: Windows Nginx private address 10.0.0.10, Emby private address 10.0.0.20, HTTP port 8096. Set $EMBY_HOST = '10.0.0.20' on the entry server, then regenerate and test the configuration.
Test-NetConnection 10.0.0.20 -Port 8096
Invoke-WebRequest -Uri 'http://10.0.0.20:8096/' -UseBasicParsing
The upstream becomes:
proxy_pass http://10.0.0.20:8096;
If the origin is also Windows, allow the entry server's private IP on port 8096 on the Emby server:
New-NetFirewallRule -Name 'EmbyFromNginx' `
-DisplayName 'Emby 8096 from Nginx' -Direction Inbound `
-Protocol TCP -LocalPort 8096 -RemoteAddress 10.0.0.10 `
-Action Allow -Profile Any
Get-NetFirewallRule -Name 'EmbyFromNginx' | Get-NetFirewallAddressFilter
Get-NetFirewallRule -Name 'EmbyFromNginx' | Get-NetFirewallPortFilter
A narrow Allow rule does not cancel an existing broad Allow rule. Inspect the existing Emby installation rules, other 8096 rules, and the origin's cloud security group. Narrow them after confirming their purpose so public clients cannot bypass Nginx. Do not add a conflicting “block all 8096” rule to override the source exception.
For a Linux origin, follow section 16 of the Linux guide to restrict sources. A separate Emby origin cannot listen only on 127.0.0.1; on a single server, prefer keeping it reachable only locally.
HTTP between two hosts is suitable only for a trusted private network. Across the public Internet, establish an encrypted tunnel first or use an HTTPS upstream with certificate verification. Client-to-Nginx TLS does not automatically encrypt Nginx-to-Emby HTTP.
17. Troubleshooting#
Nginx startup or configuration test fails#
& 'C:\nginx\nginx.exe' -t -p C:/nginx/ -c conf/nginx.conf
Get-Content 'C:\nginx\logs\error.log' -Tail 100
To watch live, use the following and stop with Ctrl+C:
Get-Content 'C:\nginx\logs\error.log' -Tail 30 -Wait
If no error log is created, check Windows Event Viewer, task results, directory permissions, and startup dependencies. If unknown directive occurs on the first line despite apparently correct text, inspect the BOM. Regenerate using the no-BOM method in this guide rather than using > to write default UTF-16 text into Nginx configuration.
CreateFile() failed / certificate cannot load#
Test-Path 'C:\nginx\conf\nginx.conf'
Test-Path "C:\nginx\certs\$YOUR_DOMAIN-chain.pem"
Test-Path "C:\nginx\certs\$YOUR_DOMAIN-key.pem"
Get-ChildItem 'C:\nginx\certs'
icacls 'C:\nginx\certs'
Check filenames, the actual issued domain, forward slashes in Nginx paths, the fixed -p prefix, and SYSTEM directory permissions. Do not move private keys into html or grant Everyone full control to resolve permissions.
bind() on 80 / 443 fails#
$Listeners = Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue |
Where-Object { $_.LocalPort -in 80, 443 }
$Listeners | Select-Object LocalAddress, LocalPort, OwningProcess
$PortProcessIds = @($Listeners.OwningProcess | Sort-Object -Unique)
if ($PortProcessIds.Count -gt 0) { Get-Process -Id $PortProcessIds }
Identify IIS, HTTP.sys, an old instance, or another service before taking action. Check whether both manual startup and the boot task ran. Do not hide the cause through repeated startup attempts or killing listeners.
502 Bad Gateway#
Test the origin from the Nginx server first:
Test-NetConnection $EMBY_HOST -Port $EMBY_PORT
Invoke-WebRequest -Uri "http://${EMBY_HOST}:$EMBY_PORT/" -UseBasicParsing
Get-Content 'C:\nginx\logs\error.log' -Tail 100
Check Emby's process, listening address, private routing, origin firewall, and proxy_pass. Connection refused usually points to a missing listener or incorrect port; for timeouts, inspect the network path and rules.
504 Gateway Timeout and resource usage#
Measure-Command {
Invoke-WebRequest -Uri "http://${EMBY_HOST}:$EMBY_PORT/" -UseBasicParsing
}
Get-Process | Sort-Object CPU -Descending |
Select-Object -First 10 Name, Id, CPU, WorkingSet64
Get-CimInstance Win32_OperatingSystem |
Select-Object TotalVisibleMemorySize, FreePhysicalMemory
The CPU field from Get-Process is cumulative processor time, not live CPU percentage. Both memory fields use KiB. Root page timing does not represent transcoding or full-video performance. Use Task Manager / Resource Monitor and Emby transcoding logs to inspect CPU, GPU, RAM, disk I/O, networking, and actual playback requests. Do not endlessly increase timeouts.
Playback disconnects, subtitle problems, or seeking failures#
Confirm the final configuration retains proxy_read_timeout 3600s, proxy_send_timeout 3600s, proxy_buffering off, and proxy_request_buffering off. Check Emby transcoding, GPU, disk I/O, public bandwidth, client networking, and intermediary CDN limits. Inspect actual request statuses and errors. Do not add arbitrary Range rewrites. For 413, check effective client_max_body_size and set it according to upload policy.
WebSocket failures#
Check HTTP/1.1, Upgrade, Connection, and map from section 8, including their correct http / location contexts. Inspect whether real WS requests upgrade successfully. If the page works but live status does not, first investigate WebSocket and intermediary proxies.
Firewall: local access works but public access fails#
Get-NetFirewallRule -DisplayName 'Nginx HTTP 80', 'Nginx HTTPS 443' |
Select-Object DisplayName, Enabled, Direction, Action, Profile
Get-NetConnectionProfile
Get-NetFirewallRule -DisplayName 'Nginx HTTP 80', 'Nginx HTTPS 443' |
Get-NetFirewallPortFilter
Continue checking cloud security groups, NAT, public IP, A / AAAA records, IPv6 listeners, and the external client network. Explicit block rules or organizational policies can override local Allow rules. Inspect effective rules rather than disabling the entire firewall.
ACME validation, renewal, or reload failures#
Request the challenge file again and confirm public port 80 is open and the path is neither authenticated nor proxied to Emby. Check the win-acme task's path and account, PEM timestamps, saved installation script, script test results, and Nginx identity. If certificates update but browsers receive an old certificate, check whether reload succeeded and whether the correct entry server is being accessed.
18. Final Windows checklist#
- Administrator PowerShell privileges confirmed
- Emby responds on 8096 or its actual port
- DNS A points to the Windows Nginx public entry point
- AAAA is correct, or no incorrect record exists when IPv6 is unavailable
- Cloud security group allows required TCP 80 / 443
- Windows Firewall allows required TCP 80 / 443
- RDP / SSH management sources reasonably restricted
- No conflicting service occupies 80 / 443
- Official Nginx installed in C:\nginx
- Configuration backed up; single-quoted here-string and UTF-8 without BOM used
- Nginx paths use forward slashes
- nginx.exe -t succeeds before reload
- HTTP proxy tested locally and externally
- WebSocket map, HTTP/1.1, Upgrade, and Connection work
- Emby External Domain uses the actual domain without a scheme
- Public HTTPS Port = 443
- Secure Connection Mode = Handled by reverse proxy or equivalent translation
- ACME challenge file publicly accessible over HTTP
- win-acme issued PEM files; actual filenames confirmed
- Nginx uses the site certificate plus full chain and matching private key
- HTTPS hostname and chain verification succeed
- Normal HTTP redirects to HTTPS; challenge path remains accessible
- Login, images, subtitles, and playback work
- Seeking, direct playback, and transcoding work
- Mobile Emby client works
- WebSocket, persistent connections, and extended playback work
- Nginx startup task created and verified after an actual reboot
- win-acme renewal task exists, is enabled, and uses the correct paths
- Installation script registered; renewal tests nginx -t before reload
- SYSTEM can read certificates and configuration and perform reload
- Certificate validity received externally checked after renewal
- Emby 8096 has no unnecessary public exposure
- Private keys outside webroot; unrelated users cannot modify configuration or scripts
Deployment references: NGINX Windows, NGINX WebSocket, win-acme getting started, win-acme automatic renewal, Microsoft schtasks create, Microsoft New-NetFirewallRule, and PowerShell character encoding.

