Running Docker on Windows WSL2/Ubuntu (Part 3) – Nginx Proxy Manager Setup

Learn how to install and configure Nginx Proxy Manager (NPM) on Windows WSL2/Ubuntu with Docker. Replace Synology DSM’s reverse proxy, issue free Let’s Encrypt SSL certificates, and securely publish containers using a unified edge network.

Running Docker on Windows WSL2/Ubuntu (Part 3) – Nginx Proxy Manager Setup

🐳INtroduction: Why Nginx Proxy Manager?

If you’ve used Synology DSM’s built-in reverse proxy, you already know the drill: central management of domains, SSL certificates, and routing to services running inside your network. But DSM is limited, and as we move to WSL2 + Docker Engine, we need something more flexible.

That’s where Nginx Proxy Manager (NPM) comes in.

  • 🔒 Built-in Let’s Encrypt SSL support
  • 🌍 Clean GUI interface for reverse proxy rules
  • 🔄 Easy per-container configuration (no need to manage one giant wildcard cert)
  • ⚡ Works seamlessly with Docker, networks, and edge routing

Think of NPM as your new “DSM reverse proxy,” but better.


STEP 1: The Networking Concept

Before diving into the installation, let’s outline how networking will work in this project:

  • edge network (external) – a shared Docker network that NPM lives on. Every container that should be publicly accessible will also join this network.
  • internal/private networks – containers can still have their own internal networks for DBs, cron jobs, or worker processes. These stay hidden from the outside.
  • dual-network containers – for example, Jellyfin will connect to both its internal media network and the edge network. That way, only the HTTP/HTTPS service is visible to NPM.
  • ports hidden – instead of exposing ports like :9000, everything goes through NPM and gets a clean subdomain (https://app.mydomain.com).

This design mimics what DSM used to do for us, but with more flexibility and isolation.


STEP 2: NPM Installation with Docker Compose

Let’s set up NPM. Create a folder just for it:

mkdir -p ~/docker/npm
cd ~/docker/npm
mkdir data letsencrypt

Here’s the docker-compose.yml:

services:
  npm-edge:
    image: jc21/nginx-proxy-manager:latest
    container_name: npm-edge
    restart: unless-stopped
    environment:
      - TZ=Asia/Seoul  # to your location
    ports:
      - "80:80"     # HTTP
      - "443:443"   # HTTPS
      - "81:81"     # NPM Admin UI (temporary)
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
    networks:
      - edge

networks:
  edge:
    external: true

Networking Setup for NPM

Before deploying Nginx Proxy Manager, let’s prepare the network it will use.
Instead of letting Docker Compose auto-generate an isolated network for each project, we’ll create a shared external bridge network called edge.

Run this once:

docker network create edge

Why not let Compose create networks automatically?

  • External + Reusable – Declaring edge as external: true in docker-compose.yml means multiple Compose projects can reuse it. NPM, Portainer, Ghost, Jellyfin—each can connect to the same network seamlessly.
  • Avoid Duplication – Auto-created networks are project-scoped. That makes cross-project communication messy.
  • Predictable Service Discovery – With a single edge network, containers can find each other by container name without hacks or manual linking.

👉 In short: by creating edge once, we ensure a consistent, reliable networking layer for every container that will sit behind Nginx Proxy Manager.

Hit it up:

docker compose up -d

STEP 3: First Login

Open the NPM admin interface in your browser:

http://<your-wsl-ip-or-domain>:81

Default credentials:

  • Emailadmin@example.com
  • Passwordchangeme

👉 Change the email/password immediately and set your timezone.

👉 You can set timezone either via the TZ variable in docker-compose.yml or inside the NPM UI (Settings → General). Make sure both match for consistency.


STEP 4: First SSL Certificate

NPM can issue free SSL certs using Let’s Encrypt. Let’s create your first certificate:

  1. Navigate to SSL Certificates → Add SSL Certificate.
  2. Choose Request a new SSL Certificate (HTTP-01 Challenge).
  3. Enter your domain:
    • If you’re using DuckDNS: wody.duckdns.org
    • If you own a domain: mydomain.com
  4. Enable options:
    • Force SSL
    • HTTP/2
    • (HSTS later – keep it disabled for now)

Hit save, and NPM will automatically fetch and install the certificate.

👉 You can set timezone either via the TZ variable in docker-compose.yml or inside the NPM UI (Settings → General). Make sure both match for consistency.


🔧 Troubleshooting SSL (Common Issues)

Sometimes the first SSL request fails. Don’t worry — here are the most common causes and fixes:

  1. Port Forwarding Not Set Up
    Let’s Encrypt must reach your server on ports 80 (HTTP) and 443 (HTTPS).
    • Check your router → forward both ports to your WSL IP (e.g., 192.168.0.121).
    • Make sure Windows Firewall isn’t blocking them.
  2. Public IP Mismatch
    If your domain doesn’t point to your public IP, validation will fail.
    • For DuckDNS: ensure the IP is updated (duckdns.org provides an update URL or cron script).
    • For personal domains: verify your DNS A-record points to your ISP’s public IP.
  3. ISP Port Blocking
    Some ISPs block port 80. If so, Let’s Encrypt HTTP-01 validation won’t work.
    • Possible workaround: use a VPN tunnel or Cloudflare proxy.
    • Or, switch to DNS-01 challenge (NPM supports it) which bypasses port checks.
  4. Temporary Rate Limits
    Let’s Encrypt has rate limits if you retry too often.
    • Wait a few hours before retrying.
    • Test with Staging Environment option in NPM to avoid hitting production limits.

👉 Once you confirm ports, DNS, and DuckDNS updates are all working, SSL requests usually succeed on the next try.


STEP 5: Accessing NPM via Subdomain

Why did we install Nginx Proxy Manager in the first place?
The very first reason was simple: get rid of those ugly port numbers and replace them with clean, SSL-secured subdomains.

So let’s put NPM to its first real test:

  1. Go to Hosts → Proxy Hosts → Add Proxy Host
  2. Fill in:
    • Domain Namesnpm.wody.duckdns.org (or npm.mydomain.com)
    • Forward Hostname/IPnpm-edge
    • Forward Port81
    • Schemehttp
  3. Under SSL:
    • Select your newly created certificate
    • Enable Force SSL and HTTP/2
    • Leave HSTS disabled for now
  4. Save.

Now you can log into NPM at:
👉 https://npm.wody.duckdns.org

Once this works:

  • Go back to your docker-compose.yml
  • Comment out / remove the 81:81 port mapping
  • (Optional) Re-enable HSTS for stricter HTTPS enforcement

🛠️ Adding Useful Custom Headers

NPM allows you to inject Custom Nginx Configuration per host. This is powerful for security and compatibility.
Here are some recommended headers you can add under Advanced → Custom Nginx Configuration:

# Security Headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; img-src *; media-src *; frame-ancestors 'self'" always;

# Performance
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

🔒 Why these matter:

  • X-Frame-Options / X-Content-Type-Options → protect against clickjacking & MIME sniffing
  • Referrer-Policy → limits sensitive referrer data leaks
  • Content-Security-Policy (CSP) → reduces XSS risks (you can customize for your app)
  • Forwarded headers → ensure apps know the real client IP & protocol

👉 These tweaks make your setup not only cleaner but also more secure and production-ready.


STEP 6: Operating NPM Effectively

A few best practices to keep NPM running smoothly:

  • 🔐 Access Control – Use Access Lists in NPM to restrict sensitive apps (e.g., Portainer).
  • 🔒 HSTS (later) – Once you confirm everything works, enable HSTS for stricter security.
  • 🗂 Certificates per subdomain – Unlike DSM where you batch everything, NPM lets you request SSL certs individually. Add them only as needed.
  • ⚡ Backups – The ./data and ./letsencrypt folders contain all config and certificates. Keep them backed up.

Final Thoughts

At this point, NPM is acting as the gateway to your home server. Every new container you install later (Jellyfin, Ghost, Portainer, etc.) will:

  • Join the edge network
  • Be assigned a unique subdomain via NPM
  • Receive its own SSL certificate from Let’s Encrypt

This setup feels a lot like Synology DSM’s reverse proxy, but with more control, better isolation, and a modern SSL workflow.

👉 Unlike DSM, you’re no longer locked into a closed ecosystem. You can run any container, use any domain provider, and scale far beyond the limits of NAS hardware.

If you’ve followed along this far, you should now have a solid grasp of the core concept. And more importantly—you now have the foundation to launch, split, secure, and experiment with as many containers as you want.

But we’re not done yet. There are still a few cases and tools that will make your home server experience smoother. In the next part, we’ll explore Portainer, a powerful GUI for managing Docker. With Portainer, you’ll get the convenience of a dashboard while still keeping all the flexibility of your WSL-based Docker setup.


👉 Next up in Part 4: Installing Portainer for a Better Docker GUI Experience