← Self-Hosting · Foundations 3 of 8 · Replaces: remembering IP:port numbers
What You'll Build
Right now each app lives at an address like http://192.168.1.50:2283. A reverse proxy
is a single front door on ports 80/443 that looks at the name you typed and forwards you to the right app:
photos.home.example.com ─┐
vault.home.example.com ─┼─► Nginx Proxy Manager (:443) ─► Immich :2283
jellyfin.home.example.com ─┘ ─► Vaultwarden :8082
─► Jellyfin :8096
It also handles HTTPS certificates from Let's Encrypt automatically. Some apps — Vaultwarden, Nextcloud, and phone camera uploads — need HTTPS to work properly.
Ports: 80, 443, and 81 (admin) · Time: 45 minutes · Difficulty: Medium
What You Need
- The server from Foundations 1, with nothing else using ports 80 or 443.
- A domain name (about $10/year from any registrar). This guide uses
example.com. - Your domain's DNS managed by Cloudflare (free), for the private option. Other DNS providers work too; the menu names differ.
Step 1: Private or Public?
| Private (recommended) | Public | |
|---|---|---|
| Who can reach your apps | Devices at home, and your devices on Tailscale | Anyone on the internet (behind each app's login) |
| Router port forwarding | None | Forward 80 and 443 to the server |
| How certificates are issued | DNS challenge (Cloudflare API token) | HTTP challenge |
| Risk | Low | Higher — every app must be kept updated |
This guide builds the private setup. A box at the end covers the public differences.
Step 2: Start Nginx Proxy Manager
mkdir -p ~/stacks/npm && cd ~/stacks/npm
nano compose.yaml
services:
npm:
image: jc21/nginx-proxy-manager:latest
container_name: npm
restart: unless-stopped
ports:
- "80:80" # HTTP (redirects to HTTPS)
- "443:443" # HTTPS (all your apps come in here)
- "81:81" # the admin web page
environment:
TZ: "America/New_York"
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
docker compose up -d
sudo ufw allow 80,443/tcp # let browsers on your network reach it
Open http://192.168.1.50:81. Current versions show a first-run screen where you create your
admin account. (Older versions used admin@example.com / changeme — if you see a
login page instead, use those and change them immediately.)
Step 3: Point Your Names at the Server
For a private setup, a wildcard DNS record points every *.home.example.com name at your server's local IP. It only works on your home network (and over Tailscale with subnet routing), which is exactly what we want.
- In Cloudflare: your domain → DNS → Records → Add record.
- Type A, Name
*.home, IPv4 address192.168.1.50, Proxy status DNS only (grey cloud) → Save.
Some routers block DNS answers that point to local IPs ("DNS rebind protection"). If names don't resolve at home, add the same wildcard as a local DNS record in Pi-hole or AdGuard Home instead.
Step 4: Get a Wildcard Certificate (DNS Challenge)
- In Cloudflare: My Profile → API Tokens → Create Token → template Edit zone DNS → Zone Resources: your domain → Create. Copy the token (it's shown once).
- In Nginx Proxy Manager: SSL Certificates → Add SSL Certificate → Let's Encrypt.
- Domain names:
*.home.example.comandhome.example.com. - Turn on Use a DNS Challenge, provider Cloudflare, and replace the sample token in the credentials box with yours:
dns_cloudflare_api_token=YOUR_TOKEN - Agree to the terms → Save. After a minute or two the certificate appears with a green status.
Let's Encrypt proves you own the domain by checking a DNS record, so your server never has to be reachable from the internet.
Step 5: Add Your First Proxy Host
Let's put Portainer behind the proxy as a test:
- Hosts → Proxy Hosts → Add Proxy Host.
- Domain Names:
portainer.home.example.com - Scheme:
https(Portainer itself uses HTTPS) · Forward Hostname/IP:192.168.1.50· Forward Port:9443 - Tick Block Common Exploits and Websockets Support.
- SSL tab: choose your
*.home.example.comcertificate, tick Force SSL and HTTP/2 Support → Save.
Open https://portainer.home.example.com — padlock, no warning. Every app guide on this site ends with the name and port to add here.
| Setting | Most apps | Exceptions |
|---|---|---|
| Scheme | http | Portainer (9443), Nextcloud AIO interface (8080) use https |
| Websockets | Always tick it | — |
| Big uploads | — | Immich / Nextcloud / Paperless: in the Advanced tab add client_max_body_size 0; |
Going Public Instead (Optional)
Only do this if you need friends or family to reach an app without Tailscale. Anything you publish is attacked by bots within hours; keep those apps updated and use strong passwords with two-factor authentication.
- Create a normal (non-wildcard) A record, e.g.
photos.example.com, pointing at your public IP (search "what is my IP"). - In your router, forward TCP 80 and 443 to
192.168.1.50. Never forward 81. - In NPM, request the certificate without the DNS challenge (the HTTP challenge works now that port 80 is reachable).
- Under Access Lists, you can restrict an app to certain IPs or add a password in front of it.
Troubleshooting
- "Internal Error" when requesting a certificate — the Cloudflare token is wrong or lacks Edit zone DNS for that domain. Create a new token.
- 502 Bad Gateway — NPM can't reach the app: check the IP, the port, and
httpvshttps. Is the app running (docker ps)? - Name doesn't resolve — run
nslookup photos.home.example.com. If it fails, see the DNS rebind note in Step 3. - Port 80 already in use — something else (often Apache, or Pi-hole's web UI) is on port 80. Stop it or move it to another port.
- Uploads fail at around 1 MB — add
client_max_body_size 0;in the proxy host's Advanced tab.
What's Next?
Reach all of this from your phone, anywhere, without opening ports:
Foundations 4: Remote Access with Tailscale or WireGuard →