Running multiple self-hosted services on one server creates a problem: they all need different ports. Nextcloud on 8080, Jellyfin on 8096, Grafana on 3000 — and you’re expected to remember all of them. Worse, none of them have SSL unless you set it up manually.

Nginx Proxy Manager (NPM) fixes this by giving you a clean web UI to manage reverse proxy hosts, automatically provision SSL certificates from Let’s Encrypt, and control access — all without touching a single Nginx config file.

It’s the most beginner-friendly reverse proxy option in the self-hosting world, and this guide will get you from zero to fully configured in about 15 minutes.

What is Nginx Proxy Manager?

Nginx Proxy Manager is a Docker-based application that wraps Nginx with an admin interface. Instead of writing server blocks and location directives by hand, you point and click.

Key features:

  • Web-based UI for managing proxy hosts
  • Automatic SSL via Let’s Encrypt with one-click renewal
  • Access lists for basic authentication and IP restrictions
  • Redirection hosts for URL forwarding
  • Stream proxying for TCP/UDP services (databases, game servers)
  • Custom Nginx config when you need more control
  • Multi-user support with role-based permissions

Prerequisites

Before starting, make sure you have:

  • A Linux server (Ubuntu, Debian, or any distro with Docker support)
  • Docker and Docker Compose installed
  • A domain name pointing to your server’s public IP (for SSL)
  • Ports 80 and 443 open on your firewall/router

If you don’t have Docker installed yet, run:

curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER

Log out and back in for the group change to take effect.

Step 1: Create the Docker Compose File

Create a directory for Nginx Proxy Manager and set up your compose file:

mkdir -p ~/nginx-proxy-manager && cd ~/nginx-proxy-manager

Create docker-compose.yml:

version: "3.8"

services:
  npm:
    image: jc21/nginx-proxy-manager:latest
    container_name: nginx-proxy-manager
    restart: unless-stopped
    ports:
      - "80:80"       # HTTP
      - "443:443"     # HTTPS
      - "81:81"       # Admin UI
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
    environment:
      - TZ=America/New_York

That’s it. No database configuration needed — NPM uses SQLite by default, which is perfect for home lab use.

Optional: Use MariaDB for Larger Deployments

If you’re proxying 50+ services or want better performance, add MariaDB:

version: "3.8"

services:
  npm:
    image: jc21/nginx-proxy-manager:latest
    container_name: nginx-proxy-manager
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "81:81"
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
    environment:
      - TZ=America/New_York
      - DB_MYSQL_HOST=db
      - DB_MYSQL_PORT=3306
      - DB_MYSQL_USER=npm
      - DB_MYSQL_PASSWORD=your_secure_password
      - DB_MYSQL_NAME=npm
    depends_on:
      - db

  db:
    image: mariadb:10.11
    container_name: npm-db
    restart: unless-stopped
    environment:
      - MYSQL_ROOT_PASSWORD=your_root_password
      - MYSQL_DATABASE=npm
      - MYSQL_USER=npm
      - MYSQL_PASSWORD=your_secure_password
    volumes:
      - ./mysql:/var/lib/mysql

For most home lab setups, stick with the SQLite version. It’s simpler and performs well for dozens of proxy hosts.

Step 2: Start the Container

docker compose up -d

Wait about 30 seconds for the container to initialize, then check the logs:

docker logs nginx-proxy-manager

You should see Nginx starting and the admin interface becoming available.

Step 3: Access the Admin Panel

Open your browser and navigate to:

http://YOUR_SERVER_IP:81

Log in with the default credentials:

You’ll be immediately prompted to change these. Do it now — use a strong password and your real email address. The email is used for Let’s Encrypt certificate notifications.

Step 4: Add Your First Proxy Host

Let’s say you’re running Portainer on port 9443 and want to access it at portainer.yourdomain.com.

  1. Click HostsProxy HostsAdd Proxy Host
  2. Fill in the Details tab:
    • Domain Names: portainer.yourdomain.com
    • Scheme: https (Portainer uses HTTPS by default)
    • Forward Hostname/IP: YOUR_SERVER_IP (or container name if on same Docker network)
    • Forward Port: 9443
    • Block Common Exploits: ✅ Enable
    • Websockets Support: ✅ Enable (many services need this)
  3. Click the SSL tab:
    • SSL Certificate: Request a New SSL Certificate
    • Force SSL: ✅ Enable
    • HTTP/2 Support: ✅ Enable
    • HSTS Enabled: ✅ Enable
    • Email Address: Your email for Let’s Encrypt
    • Agree to Terms of Service:
  4. Click Save

NPM will automatically request an SSL certificate from Let’s Encrypt and configure the proxy. Within a few seconds, https://portainer.yourdomain.com will be live with a valid SSL certificate.

Step 5: Docker Networking (The Right Way)

Using server IP addresses works, but it’s better to put NPM and your services on the same Docker network. This keeps traffic internal and lets you use container names instead of IPs.

Create a shared network:

docker network create proxy

Update your NPM docker-compose.yml:

services:
  npm:
    image: jc21/nginx-proxy-manager:latest
    container_name: nginx-proxy-manager
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "81:81"
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
    networks:
      - proxy
      - default

networks:
  proxy:
    external: true

Then in any service you want to proxy (e.g., Nextcloud), add:

networks:
  - proxy

Now in NPM, you can use the container name (like nextcloud) as the Forward Hostname instead of an IP address. If the container restarts with a different IP, everything still works.

Step 6: Set Up Access Lists

Access lists let you restrict who can reach certain services. This is useful for admin panels you don’t want exposed to the internet.

  1. Go to Access ListsAdd Access List
  2. Name: Admin Only
  3. Under the Authorization tab, add username/password combinations
  4. Under the Access tab, add allowed IP ranges (e.g., 192.168.1.0/24 for your local network)
  5. Save

Now when editing a proxy host, you can assign this access list under the Details tab. Anyone outside your allowed IPs will get a 403, and anyone inside will need the username/password.

Step 7: Redirection Hosts

Want yourdomain.com to redirect to www.yourdomain.com? Or redirect an old URL to a new one?

  1. Go to HostsRedirection HostsAdd Redirection Host
  2. Domain Names: yourdomain.com
  3. Forward Scheme: https
  4. Forward Domain: www.yourdomain.com
  5. HTTP Code: 301 (permanent redirect)
  6. Preserve Path: ✅ Enable
  7. Set up SSL the same as proxy hosts

This is also useful for creating short-link-style redirects for services with long URLs.

Step 8: Custom SSL Certificates

While Let’s Encrypt works for most cases, you might want to use wildcard certificates or certificates from another provider.

Wildcard Certificates with DNS Challenge

For *.yourdomain.com certificates, you’ll need DNS challenge support. NPM supports this for many DNS providers:

  1. Go to SSL CertificatesAdd SSL CertificateLet’s Encrypt
  2. Domain Names: *.yourdomain.com and yourdomain.com
  3. Enable DNS Challenge:
  4. DNS Provider: Select yours (Cloudflare, DigitalOcean, Route53, etc.)
  5. Credentials File Content: Add your API token

For Cloudflare, the credentials look like:

dns_cloudflare_api_token=your_cloudflare_api_token

Generate a Cloudflare API token with Zone:DNS:Edit permissions for your domain.

Once you have a wildcard certificate, you can reuse it for every proxy host under that domain — no more waiting for individual cert provisioning.

Step 9: Stream Proxying (TCP/UDP)

Need to proxy non-HTTP traffic? NPM can handle TCP and UDP streams. This is useful for:

  • Database connections (MySQL, PostgreSQL)
  • Game servers (Minecraft, Valheim)
  • SSH on custom ports
  • DNS services

Go to HostsStreamsAdd Stream:

  • Incoming Port: The port users will connect to
  • Forward Host: The server/container running the service
  • Forward Port: The actual service port
  • TCP/UDP Forwarding: Select the protocol

Securing Nginx Proxy Manager

Move the Admin Panel Off Port 81

Exposing the admin panel on port 81 is convenient but not ideal. Options:

Option 1: Proxy NPM through itself. Create a proxy host for npm.yourdomain.com pointing to 127.0.0.1:81, then block port 81 in your firewall:

sudo ufw deny 81

Option 2: Bind port 81 to localhost only:

ports:
  - "80:80"
  - "443:443"
  - "127.0.0.1:81:81"

Then access via SSH tunnel: ssh -L 81:localhost:81 user@server

Enable Fail2Ban

Add brute-force protection by mounting the NPM log directory and configuring Fail2Ban:

sudo apt install fail2ban

Create /etc/fail2ban/filter.d/npm.conf:

[Definition]
failregex = ^<HOST> .* 4\d\d
ignoreregex =

Create /etc/fail2ban/jail.d/npm.conf:

[npm]
enabled = true
filter = npm
logpath = /path/to/nginx-proxy-manager/data/logs/proxy-host-*_access.log
maxretry = 5
bantime = 3600
findtime = 600

Troubleshooting

“502 Bad Gateway”

This means NPM can reach Nginx but Nginx can’t reach your service.

  • Check the service is running: docker ps to verify the container is up
  • Check the port: Make sure you’re using the correct internal port
  • Check the scheme: Some services use HTTPS internally (like Portainer)
  • Check networking: If using container names, make sure both containers are on the same Docker network

SSL Certificate Fails to Provision

  • DNS not pointing to your server: Run dig yourdomain.com and verify the A record
  • Ports 80/443 blocked: Let’s Encrypt needs to reach your server on these ports
  • Rate limiting: Let’s Encrypt has rate limits — if you’ve requested too many certs recently, wait an hour

“504 Gateway Timeout”

The backend service is too slow to respond.

Add custom Nginx config to the proxy host under the Advanced tab:

proxy_read_timeout 300s;
proxy_connect_timeout 300s;
proxy_send_timeout 300s;

Admin Panel Not Loading

If port 81 isn’t responding:

docker logs nginx-proxy-manager
docker restart nginx-proxy-manager

Check if another service is using port 81: sudo lsof -i :81

Tips and Best Practices

  1. Always enable “Block Common Exploits” — it adds basic protection against common attack patterns
  2. Enable Websockets Support by default — most modern apps need it and it doesn’t hurt if they don’t
  3. Use Docker networks instead of IP addresses — more reliable and secure
  4. Back up the data directory — it contains your certs, configs, and database
  5. Set up wildcard certs if you have more than 3-4 services — saves time and avoids rate limits
  6. Keep NPM updated: docker compose pull && docker compose up -d
  7. Monitor certificate expiry — NPM auto-renews, but check the SSL Certificates page occasionally

Backing Up Nginx Proxy Manager

Your entire NPM configuration lives in the data and letsencrypt directories. Back them up:

tar -czf npm-backup-$(date +%Y%m%d).tar.gz data letsencrypt

To restore, extract the archive into a fresh NPM directory and start the container. Everything — proxy hosts, SSL certs, access lists — comes back exactly as it was.

Conclusion

Nginx Proxy Manager takes the most intimidating part of self-hosting — reverse proxies and SSL — and makes it approachable. With a few clicks, you get proper domain names and HTTPS for all your services.

Once it’s running, adding a new service takes about 30 seconds: create the proxy host, point it at the container, request an SSL cert, done. It’s the kind of infrastructure that makes everything else easier.

Next steps:

  • Set up Authelia for single sign-on across all your proxied services
  • Configure Uptime Kuma to monitor your proxy hosts
  • Explore Traefik if you want automatic service discovery with Docker labels

Your services deserve real URLs and real certificates. Nginx Proxy Manager makes that trivially easy.