guide · 5 min read
Setting up a reverse proxy
Readable names and HTTPS instead of six ports to remember: Nginx Proxy Manager, Let's Encrypt, and the “Internal Error” waiting for everyone.
By your third service, you will hit a problem nobody warns you about.
Jellyfin listens on 8096, Immich on 2283, Vaultwarden on 8080. You remember
them at first. Then you add a fourth, you get a port wrong, and you go digging
through docker-compose.yml to find which was which. And when someone else in
the house asks for the photos address, you read out 192.168.1.42:2283 —
twice, because the first attempt did not work.
Worse: all of it travels in clear text. A password manager reached over HTTP on your own network is still a password manager handing out its session unencrypted.
A reverse proxy fixes both at once. One entry point, readable names, HTTPS everywhere:
photos.example.com → 192.168.1.42:2283
movies.example.com → 192.168.1.42:8096
vault.example.com → 192.168.1.42:8080
This guide uses Nginx Proxy Manager: it is nginx with a web interface on top. Let's Encrypt certificates take three clicks and renew themselves. Budget thirty minutes the first time.
What you need
- Ubuntu Server with Docker — see Installing Docker.
- A domain name. It is the one unavoidable cost in this guide: roughly ten euros a year. Without a domain there is no Let's Encrypt certificate, and so no HTTPS.
- A way to reach your server from outside, if that is what you want: a Cloudflare tunnel or a forwarded port.
And if you want to expose nothing at all, this guide still pays off: readable names and HTTPS on your own network is most of the comfort already.
Step 1 — Free up ports 80 and 443
The proxy has to be alone on them. If something already holds them, move it first:
sudo ss -tlnp | grep -E ':(80|443)\s'
No output means nothing is listening and you can carry on. An apache2 or
nginx line means a web server came along with something else, and it has to
go (sudo systemctl disable --now apache2).
Step 2 — Start the proxy
In a directory of its own, a docker-compose.yml:
services:
npm:
image: jc21/nginx-proxy-manager:2.15.1
restart: unless-stopped
ports:
- '80:80' # public HTTP
- '443:443' # public HTTPS
- '81:81' # admin interface
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
docker compose up -d
The version is pinned, and that is not a detail: latest on the component
carrying all your traffic means a surprise update can take every service down
at once. Updating your containers covers
when and how to move up a version.
Step 3 — First login
Go to http://192.168.1.42:81 — 81, not 80. Port 80 serves public
traffic; the admin interface lives beside it.
The first account is in the documentation, so everyone knows it:
[email protected]
changeme
The interface asks you to replace it immediately. Actually do it — this admin page can issue a certificate for any subdomain and point any service anywhere.
Step 4 — Point your domain here
At your registrar (or in Cloudflare, if the domain is delegated there), create one record per service:
| Type | Name | Value |
|---|---|---|
| A | photos |
your public IP |
| A | movies |
your public IP |
Or, simpler, a wildcard: a * record sending every subdomain to the same
address. You will never come back here to add a service again.
If you go through a Cloudflare tunnel, the tunnel's own records do this job and your public IP never appears.
Check it took effect before going further — DNS propagation takes anywhere from minutes to hours:
dig +short photos.example.com
An address means you are good. Nothing at all means wait: asking for a certificate now would fail without the error saying why.
Step 5 — Your first host
Under Hosts → Proxy Hosts → Add Proxy Host:
- Domain Names:
photos.example.com - Scheme:
http - Forward Hostname / IP: the server's local IP
- Forward Port:
2283 - Block Common Exploits: ticked
Then the SSL tab:
- SSL Certificate: Request a new SSL Certificate
- Force SSL: ticked — otherwise the service stays reachable in clear text
- HTTP/2 Support: ticked
- Your email address, and accept the Let's Encrypt terms
Save. Ten seconds later https://photos.example.com answers, with a padlock.
The error coming your way: "Internal Error"
That is what Nginx Proxy Manager says when Let's Encrypt could not verify the domain. It never says why. The three causes, most common first:
- DNS does not point here yet. Check again with
dig. - Port 80 does not reach the proxy. Let's Encrypt validates by connecting on port 80 from outside — even if you only ever intend to serve HTTPS. Without that port forwarded, validation fails.
- Your ISP blocks port 80. This is common. Switch to DNS validation: in the SSL tab, tick Use a DNS Challenge and pick your provider. No port is needed, and it is the only way to get a wildcard certificate.
Local only, exposing nothing
You can have names and HTTPS without a single packet coming in:
- Buy a real domain, but point its records at the server's private IP
(
192.168.1.42). This is allowed, and invisible from outside. - Use DNS validation for the certificate: Let's Encrypt then never needs to reach your machine.
The result: https://photos.example.com works at home and nowhere else. That
is often exactly what you wanted.
What it does not do
A reverse proxy is not authentication. It encrypts transport and tidies up addresses; it stops nobody from opening the page. What protects a service is its own password — and not exposing it at all when nothing requires it.
Do not put the proxy's own interface (port 81) behind a public subdomain. It is the key to everything else.
Next
Video
Lawrence Systems walks through Nginx Proxy Manager and its SSL handling — worth watching if you would rather see the interface before committing to it.
Nothing is sent to YouTube until you press play. This site sets no cookies of its own.Watch on YouTube