Chapter 8 of Be Your Own Cloud explains why everything after it sits behind a reverse proxy, and how a service that’s never on the internet still gets a real HTTPS certificate. Every service tutorial on this site assumes it’s done. This page is the setup in order, with one certificate that covers every service, a way to test before Pi-hole is running, and how to add each service from here on.
Before you start
- A server with Docker, from Installing Ubuntu Server and Docker and Compose.
- A domain you own. The examples use
home.yourdomain.com, as the book does. Nothing under it will ever be public. - That domain’s DNS at a provider Nginx Proxy Manager supports. Chapter 8 suggests Cloudflare, which is free for this; this page uses it. Moving the DNS means changing the domain’s nameservers at the registrar where you bought it — Cloudflare shows you the two to use when you add the domain.
1. The folder, the network, the proxy
sudo mkdir -p /opt/stacks/npm
cd /opt/stacks/npm
The compose file, as printed:
services:
npm:
image: 'jc21/nginx-proxy-manager:2.16.0'
container_name: npm
restart: unless-stopped
ports:
- '80:80'
- '443:443'
- '81:81' # admin UI
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
networks:
- proxy
networks:
proxy:
external: true The shared network, once, before the first start:
docker network create proxy docker compose up -d
docker compose logs -f npm
The first start takes a minute or two while it sets itself up. When the log settles, press Ctrl+C.
2. The admin account — straight away
Open http://192.168.1.50:81. You get a Welcome! page that creates the admin account. As Chapter 8 says, do it now: until it exists, anyone on your network who opens port 81 first gets to create it. Use a long random password from your password manager.
Then turn on two-factor: open the user menu at the top right, choose Two-Factor Auth, and scan the code with your authenticator app. This panel controls where every service’s traffic goes, so it deserves the same protection as your email.
3. A Cloudflare token that can only edit DNS
In Cloudflare, go to My Profile → API Tokens → Create Token and use the Edit zone DNS template. Under Zone Resources, choose Include → Specific zone → your domain. Continue, then Create Token, and copy it — Cloudflare shows it once.
That’s the narrowest token that works: it can change DNS records on one domain and nothing else. Chapter 8’s troubleshooting box comes back to this — a read-only token, or one for the wrong domain, is the usual reason a certificate request fails.
4. One certificate for every service
Chapter 8 gets its certificates through the DNS challenge. Because of that, you can ask for a wildcard certificate — *.home.yourdomain.com — once, and pick it for every service from then on. That’s what “your DNS-challenge certificate from Chapter 8” means in this site’s service tutorials.
In Nginx Proxy Manager, open Certificates, choose Add Certificate, then Let’s Encrypt via DNS:
| Field | Value |
|---|---|
| Domain Names | *.home.yourdomain.com |
| DNS Provider | Cloudflare |
| Credentials File Content | replace the example token with yours: dns_cloudflare_api_token= followed by the token |
| Propagation Seconds | leave empty; if the request times out, try 60 |
| Key Type | leave the default |
Save, and give it a minute or two. Let’s Encrypt checks a temporary DNS record that Nginx Proxy Manager creates through the token, then removes. When it’s done, the certificate appears in the list with an Expires date.
Nginx Proxy Manager warns that the credentials are kept in plain text in its database. That’s the other reason for a token that can only touch DNS on one domain. The database lives in /opt/stacks/npm/data, which Chapter 18’s backup takes in full, into an encrypted repository.
5. Your first proxy host: the admin page itself
Something to test with, and a nicer address for the admin page. Hosts → Proxy Hosts → Add Proxy Host:
| Tab | Setting | Value |
|---|---|---|
| Details | Domain Names | npm.home.yourdomain.com |
| Details | Scheme | http |
| Details | Forward Hostname / IP | npm |
| Details | Forward Port | 81 |
| Details | Block Common Exploits | on |
| SSL | SSL Certificate | your *.home.yourdomain.com certificate |
| SSL | Force SSL, HTTP/2 Support | on |
npm is the container’s name. It’s on the proxy network, so Docker resolves the name there — the same way every service that joins that network is reached.
6. Test it before Pi-hole exists
Chapter 8 makes these names work at home with local DNS records in Pi-hole — but that’s Chapter 10. You don’t have to wait to know the proxy and certificate work. From a laptop on your network:
curl -sI --resolve npm.home.yourdomain.com:443:192.168.1.50 https://npm.home.yourdomain.com
--resolve tells curl the address for this one request, so no DNS is involved. Windows 10 and 11 have curl too — type curl.exe in PowerShell. An HTTP/2 200 back, with no certificate error, means the proxy, the certificate and the forward all work.
To open it in a browser before Pi-hole is running, you can add a temporary line to the laptop’s hosts file (/etc/hosts on Linux and Mac, C:\Windows\System32\drivers\etc\hosts on Windows), and take it out once Pi-hole answers instead:
192.168.1.50 npm.home.yourdomain.com
7. When Pi-hole is running
Chapter 8’s records, added in Pi-hole under Settings → Local DNS Records:
One record per subdomain, all pointing at the server. Added once Chapter 10's Pi-hole is running.
vault.home.yourdomain.com -> 192.168.1.50
nextcloud.home.yourdomain.com -> 192.168.1.50
# Same IP for all of them -- Nginx Proxy Manager on that one machine
# is what routes each subdomain to the right container behind it. Or one wildcard record that covers them all — see One record for every subdomain in the Pi-hole tutorial.
8. Adding each service from here on
Every service follows one of two patterns. If its compose file joins the proxy network, forward to its container name — it doesn’t publish a port on the server at all. If it publishes a port on the server instead, forward to the server’s IP and that port.
| Service | Forward Hostname / IP | Forward Port | Settings |
|---|---|---|---|
| Vaultwarden | vaultwarden | 80 | Vaultwarden |
| Nextcloud AIO | 192.168.1.50 | 11000 | Nextcloud AIO |
| Immich | 192.168.1.50 | 2283 | Immich |
| Jellyfin | 192.168.1.50 | 8096 | Jellyfin |
| The Sanitiser | sanitizer | 5000 | Local AI |
| Open WebUI | 192.168.1.50 | 8090 | Local AI |
Each tutorial’s table has the rest — Websockets, upload limits, the Advanced tab (the gear icon at the right-hand end of the proxy host’s tabs) — for that service.
Checks worth knowing
Who’s on the shared network — anything the proxy forwards to by name has to be listed:
docker network inspect proxy --format '{{range .Containers}}{{.Name}} {{end}}'
What the server is listening on — you should see 80, 443 and 81 from the proxy, plus any ports your services publish:
sudo ss -tlnp
Two problems Chapter 8’s troubleshooting box doesn’t cover:
- 502 Bad Gateway. The proxy can’t reach what you forwarded to. The forward hostname is a container that isn’t on
proxy(check the command above), or the port is wrong. Forwarding by container name means the port the app listens on inside its container —80for Vaultwarden. - A certificate warning on one name only. A wildcard covers one level:
immich.home.yourdomain.com, but notphotos.immich.home.yourdomain.com. Keep to one level underhome.
Renewals and updates
Let’s Encrypt certificates are short-lived, and Nginx Proxy Manager renews them itself, through the same token. Look at the Expires date on the Certificates page a month or so from now: if it has moved on, renewal works. If it hasn’t, Renew Certificate in the certificate’s menu shows the error.
The book pins jc21/nginx-proxy-manager:2.16.0. Before you change that, read the release notes for every version in between — recent ones have updated Certbot, which can affect DNS-challenge renewals (Chapter 27) — and make sure last night’s backup ran (Chapter 18).
Official documentation
- Nginx Proxy Manager and its releases
- Certbot’s Cloudflare DNS plugin — the credentials format
- Cloudflare: creating an API token
- Let’s Encrypt: challenge types — why DNS-01 is the one that needs no open port
Stuck on a step? Get in touch.