How to configure containerized PgAdmin + Nginx Reverse Proxy

Image generated via ChatGPT

I recently set up a rock-solid NGINX configuration for my environment — which includes a Raspberry Pi, DDNS client, and a Namecheap-registered domain — and successfully tested it with Portainer. Everything worked smoothly, but when I copied and tweaked my reverse proxy setup for my PostgreSQL and PgAdmin services, things started to go sideways.

In this next blog post, I dive into the unexpected problems I ran into when adapting my NGINX setup for PostgreSQL and PgAdmin — and share exactly how I tackled and solved each hurdle. If you’re curious about troubleshooting real-world reverse proxy issues and want some practical fixes, you’ll find all the details here!

Issue #1: Rate limit

To protect your services from repeated access attempts or brute-force attacks, you can implement rate limiting in your NGINX setup by adding directives within the http {} block of your nginx.conf file. Here’s a typical example of how you would do this for a small setup that is intended to be used by a group of people:

    # RATE LIMIT AGAINST BOTS
# Defines a zone named 'mylimit' based on client IP, 10MB size, allowing 20 requests per second.
# Allows a burst of 30 requests beyond the rate before rejecting.
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=20r/s;

To set the burst size for rate limiting in each of your reverse-proxied subdomains, you can add the following directive inside the location / {} block of your NGINX configuration:

    # Allows a burst of 30 requests.
# 'nodelay' ensures requests within the burst are processed immediately.
limit_req zone=mylimit burst=30 nodelay;

Problem

Even with just a single database, PgAdmin sends separate requests for every table, columns, and UI elements, each relies on the PostgreSQL backend. This quickly caused NGINX to hit the rate limit just from loading my database — so much so that even basic things like icons and the user menu in the top-right corner failed to load.

2025/08/07 09:08:03 [error] 24#24: *47 limiting requests, excess: 30.300 by zone "mylimit", client: [REDACTED]

To avoid this, increase the burst to 100+, I set it to 200:

    limit_req zone=mylimit burst=200 nodelay;

Issue #2: PgAdmin behind reverse-proxy

After resolving the rate-limiting issue, I found another problem. Every time I wanted to interact with the backend PostgeSQL in a way to view/browse the data in the <iframe> on the right hand side, I saw this error:

Error you can easily face when PgAdmin runs behind a reverse-proxy

This error indicates that pgAdmin is preventing itself from being embedded within an <iframe> or similar element on another website due to security measures, specifically X-Frame-Options or Content Security Policy (CSP) headers. This is a security feature designed to prevent clickjacking attacks. To still have such and similar protections to all my other services, I kept my proxy.conf intact:

# Timeout if the real server is dead
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;

# Proxy Connection Settings
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
proxy_connect_timeout 240;
proxy_headers_hash_bucket_size 128;
proxy_headers_hash_max_size 1024;
proxy_http_version 1.1;
proxy_read_timeout 240;
#proxy_redirect http:// $scheme://;
#proxy_redirect http:// https://;
proxy_send_timeout 240;

# Proxy Cache and Cookie Settings
proxy_cache_bypass $cookie_session;
proxy_no_cache $cookie_session;

# Proxy Header Settings
proxy_set_header Connection "upgrade";
proxy_set_header Early-Data $ssl_early_data;
proxy_set_header Host $host;
proxy_set_header Proxy "";
proxy_set_header Upgrade $http_upgrade;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Ssl on;
proxy_set_header X-Real-IP $remote_addr;
# this is to not send this back more than once, as our nginx will send it once already
# this is set in ssl_lazarus.conf as add_header X-Content-Type-Options "nosniff" always;
proxy_hide_header X-Content-Type-Options;

However, you can override settings in the reverse-proxy configuration for the subdomain itself. So, after loading proxy.conf, I added additional proxy header changes resulting in the following location / block:

  location / {
# Applies the 'mylimit' zone.
# Allows a burst of 30 requests.
# 'nodelay' ensures requests within the burst are processed immediately.
limit_req zone=mylimit burst=200 nodelay;
#include common proxy settings
include /etc/nginx/proxy.conf;
#override some proxy settings ah
proxy_set_header Upgrade $http_upgrade; # Redundant if in proxy.conf but safe
proxy_set_header Connection "upgrade"; # Redundant if in proxy.conf but safe
proxy_redirect off; # directive tells Nginx to not rewrite the "Location" header in the backend's response.
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header X-Frame-Options;
add_header X-Frame-Options "ALLOW-FROM https://pgadmin.YOUR_DOMAIN.tld";
# proxy_hide_header Content-Security-Policy;

proxy_pass $pgadmin;
access_log on;
access_log /var/log/nginx/access_pgadmin.log;
error_log on;
error_log /var/log/nginx/error_pgadmin.log;
}

The key is basically to hide the original X-Frame-Option, the overriding it by setting our pgadmin subdomain to be allowed.

These settings will already allow you to run your PgAdmin behind a reverse-proxy. No further changes are need, nor any specific ENV variable is needed to be passed to your PgAdmin container.

The original post can be found here: https://cslev.medium.com/how-to-configure-containerized-pgadmin-nginx-reverse-proxy-d63f44ea4d3c