Leveraging Reverse Proxying


In this article, we will:

  • Place portainer, cockpit and nginx proxy manager (well at least the admin interface) behind the reverse proxy for ssl termination/ip whitelisting on all our admin interfaces.

Leveraging the Reverse Proxy

Now we can do something cool. With all of our services in the reverse proxy network, we can now get our management interfaces protected! Even the reverse proxy! This is fantastic as it allows us to tunnel all of our admin interfaces behind validated SSL and protect them with IP whitelisting.

Let’s make 3 more subdomains in our DNS:

  • portainer.<your-domain>.<tld>
  • npm.<your-domain>.<tld>
  • <your-docker-host>.<your-domain>.<tld> (this guide uses cbdocker02 as it’s hostname)

in OPNsense, since they’re all the same IP we can do so using aliases

  • In the Nginx Proxy Manager, create a proxy host to npm.<your-domain>.<tld> on http, pointing to nginx-proxy-manager on port 81. Set the access list to local subnets (management interfaces should not be exposed to the web, except with great discretion).

  • Under SSL, set your wildcard SSL certificate (or generate a new let’s encrypt certificate). Press save.

  • Now let’s do it all again for portainer.<yourdomain>.<tld> note the fact that we’re proxying to an https endpoint this time.

  • Also add or generate your SSL certificate and save.

  • Same thing for <your-host>.<your-domain>.<tld> for cockpit (on port 9090).

Note that we are reverse proxying to the same domain address (since it should resolve back to our host IP). We are also setting the Scheme to HTTPS, same as portainer.

  • Don’t forget the SSL

Putting Portainer on the Reverse Proxy

Oops, we missed a step. Even though we are reverse proxying to portainer, portainer isn’t on the same network! Let’s fix that. in cockpit, navigate to /mnt/containers/portainer and update the docker-compose.yaml. Let’s comment out the port exposure at the same time:

      image: portainer/portainer-ce:latest
      container_name: portainer
      restart: always
      privileged: true
        - /mnt/containers/portainer/container-data/data:/data:Z
        - /var/run/docker.sock:/var/run/docker.sock:Z
#      ports:
#        - 9443:9443
        - reverseproxy-nw

    external: true

You can see me do that below using mc and micro:

At the start of the animation, you can see me uncheck use internal editor. This tells mc to default to micro as the editor.

Testing our Setup

Let’s now verify that you can access https://portainer.<yourdomain>.<tld> , https://<your-host>.<your-domain>.<tld> and https://npm.<yourdomain>.<tld>. You can see me do that in the animation below:

Success! We are now proxying all of our web admin interfaces behind nginx proxy manager, and restricting those interfaces to local IPs.

The eagle eyed of you may notice the ‘login with oauth’ prompt for portainer. I may have been reading a bit ahead when creating this animation. We’ll get there!

Removing the Port Exposures

Now log into portainer, and comment out the port exposure for nginx-proxy-manager. Now our only method of accessing the nginx-proxy-manager admin portal and portainer is through the reverse proxy.

Note that we did not comment out the port exposure for cockpit. This is because if we screw up the reverse proxy, we want a way to get back into our host on port 9090. This is referred to as an out of band access method.


Always. Be. Hustling Documenting.

Update gitea to reflect our updated nginx proxy manager configuration:

and portainer:

Moving On

Alright, we are making good progress. We have most of the tools we need to create a stable, secure docker environment.

One critical part is missing though. We aren’t backing up our data! We need backups. Let’s address that with Backups up Docker.