Purpose

Handles a SSL (HTTPS) and points subdomains (e.g. recipes.sydneysu.dev) to the right container automatically.

Sequence Diagram

sequenceDiagram
    autonumber
    actor User as User Browser
    participant DNS as Cloudflare
    participant VPS as DigitalOcean VPS (NPM)
    participant App as App Containers

    Note over User, DNS: User visits recipes.sydneysu.dev
    User->>DNS: 1. Fetch IP for Subdomain
    DNS-->>User: 2. Return Droplet IP

    rect rgb(40, 44, 52)
        Note right of VPS: Traffic hits Port 443 (HTTPS)
        User->>VPS: 3. Request + Host Header
        VPS->>App: 4. Internal Proxy to Port 9000
        App-->>VPS: 5. App Response
        VPS-->>User: 6. Encrypted Data
    end

    Note over User, VPS: User tries Admin Panel (vps.sydneysu.dev)
    User-XVPS: 7. Direct Port 81 access (BLOCKED by UFW)
    
    User->>VPS: 8. Request via Port 443
    VPS->>VPS: 9. Internal Proxy to Admin (Port 81)
    VPS-->>User: 10. Secure Admin Access

NPM vs. Alternatives

Fundamental Difference

  • Cloudflare (DNS): Tells the internet’s traffic which IP address to go to
  • NPM (Proxy Manager): Directs traffic to the correct Docker container once they arrive
FeatureCloudflare (DNS Only)NPM (Reverse Proxy)
RoutingCan only send traffic to one IP.Can route traffic to unlimited apps on different internal ports.
User ExperienceRequires “Ugly URLs” like site.com:9000.Enables “Clean URLs” like recipes.sydneysu.dev.
SSL (Security)Encrypts traffic to the “edge” (Cloudflare).Encrypts traffic all the way to your Droplet (needed for .dev).
Port ManagementAll ports would have to be open to the world.Closes all ports except 80/443; everything else stays hidden.

The “One IP” Problem

Since the DigitalOcean Droplet only has one public IP address, Cloudflare can’t distinguish between apps. If you point vps.sydneysu.dev and recipes.sydneysu.dev both to the droplet IP, the Droplet wouldn’t know which request belongs to which container. NPM reads the “Host Header” to make that decision.

Port Hiding & Security

Docker apps usually run on “high ports” (9000, 8080, etc.).

  • Without NPM: Would have to open every single one of those ports in the Firewall (UFW), increasing “Attack Surface.”
  • With NPM: Only open Port 443. NPM sits on that port and “hands off” traffic internally via the Loopback (127.0.0.1), keeping your app ports invisible to the public internet.

Automated SSL for .dev

The .dev domain has HSTS (Strict HTTPS) hard-coded into browsers. NPM provides a centralized dashboard to request, renew, and apply Let’s Encrypt certificates to all subdomains with one click, rather than configuring SSL inside every individual Docker container.

Steps

1. Create a dedicated directory for the project

mkdir ~/nginx-proxy-manage
cd ~/nginx-proxy-manager

2. Create docker-compose.yml

services:
  app:
    image: 'jc21/nginx-proxy-manager:latest'
    restart: unless-stopped
    ports:
      - '80:80'   # Public HTTP
      - '81:81'   # Admin Web Port (Temporary)
      - '443:443' # Public HTTPS
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt

3. Launch

docker compose up -d

4. Setup Network & DNS

  • Cloudflare:
    • Add a DNS Record:
      • Type: A
      • Name: vps
      • IPv4 Address: <droplet ipv4 address>
      • DNS Only proxy status to allow non-standard port traffic (Port 81)
  • Firewall:
    • Opened ports for the web and admin panel
    • sudo ufw allow 80/tcp
    • sudo ufw allow 81/tcp
    • sudo ufw allow 443/tcp

5. Setup Security

To secure the admin panel, route it through itself to enable SSL and then lock the public door

  1. Create a new Nginx admin account at http://<droplet-ip>:81
  2. Add a new proxy host
    • Domain: vps.sydneysu.dev
    • Forward IP: 127.0.0.1 (Loopback)
    • Forward Port: 81
    • Block common Exploits
    • SSL: Request new Let’s Encrypt certificate + Force SSL
  3. Hardening:
    • Once https://vps.sydneysu.dev is verified, close the insecure public port
    • sudo ufw deny 81/tcp

6. Edit docker-compose.yml

services:
  app:
    image: 'jc21/nginx-proxy-manager:latest'
    restart: unless-stopped
    ports:
      - '80:80'   
      - '127.0.0.1:81:81'   # only listen to internal requests
      - '443:443' 
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt

Technical Summary

  • Reverse Proxy: NPM sits on ports 80/443 and directs traffic based on the subdomain
  • Loopback (127.0.0.1): Used to allow internal communication between the Proxy and the Admin panel, bypassing the external firewall
  • HSTS Awareness: Since .dev domains require HTTPS, the proxy is essential for accessing internal HTTP services