Docker & DevOps

Nginx Proxy Manager in Docker: HTTPS for many self-hosted apps

Run Nginx Proxy Manager in Docker, add proxy hosts for each app and issue free Let's Encrypt certificates from a single web UI.

IntermediateLinux server3 min read

Nginx Proxy Manager in Docker gives you a web interface that creates reverse proxy hosts and issues Let's Encrypt certificates for each one. You point a subdomain at your VPS, add a proxy host in the UI, and HTTPS is handled for you. This guide covers the Compose file, the first proxy host, certificate issuance and the errors you are most likely to hit.

Before you start

You need:

  • A Linux VPS running Ubuntu 22.04/24.04, Debian 12 or a similar release, with root or sudo access.
  • A domain you control. Create an A record for each app, for example app.example.com → 203.0.113.10. Let's Encrypt validates over HTTP on port 80, so DNS must resolve before you request a certificate.
  • Ports 80 and 443 free on the host. If Nginx, Apache or another web server is already listening there, stop and disable it first.
  • Docker and the Compose plugin installed.

Warning: Stopping an existing web server will take any sites it hosts offline. Check what is running with sudo ss -tlnp | grep -E ':80|:443' before you continue.

Install Docker and the Compose plugin

On Ubuntu, install Docker from the official repository so you get a current version:

Bash
sudo apt update
sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Confirm both tools respond:

Bash
docker --version
docker compose version

On Debian 12, swap ubuntu for debian in the repository URL and the GPG download URL. The rest is the same.

Run Nginx Proxy Manager in Docker

Create a directory for the stack so the data survives container rebuilds:

Bash
sudo mkdir -p /opt/npm
cd /opt/npm

Create /opt/npm/docker-compose.yml:

YAML
services:
  npm:
    image: jc21/nginx-proxy-manager:latest
    container_name: npm
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "81:81"
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
    environment:
      - TZ=UTC
    extra_hosts:
      - "host.docker.internal:host-gateway"

The extra_hosts line lets the container reach services running directly on the host by the name host.docker.internal. Start the stack:

Bash
sudo docker compose up -d
sudo docker compose logs -f

Wait for the log line saying the backend is listening, then press Ctrl+C to stop following the logs. Open http://203.0.113.10:81 in your browser.

The first login uses admin@example.com with the password changeme. Change both immediately under the user menu — the admin panel is on a plain HTTP port.

Tip: Restrict port 81 to your own IP at the firewall, or reach it through an SSH tunnel with ssh -L 8181:127.0.0.1:81 deploy@203.0.113.10 and browse to http://localhost:8181.

Add a proxy host

In the UI, go to Hosts → Proxy Hosts → Add Proxy Host.

  • Domain Names: app.example.com
  • Scheme: http (NPM terminates TLS and talks to your app in plain HTTP on the private network)
  • Forward Hostname / IP: the address of your app
  • Forward Port: the port your app listens on, for example 3000
  • Block Common Exploits: on
  • Websockets Support: on if the app uses them

For the forward hostname, pick the right target:

  • App in another container on the same Compose network: use the service name, for example app.
  • App running directly on the host: use host.docker.internal.
  • App on a different machine: use its private IP, for example 10.0.0.15.

Make sure the app binds to 0.0.0.0 and not only 127.0.0.1, otherwise the container cannot reach it.

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.