Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -185,14 +185,14 @@ ENV PYTHONPATH=/pgadmin4
# Copy in the code and docs
COPY --from=app-builder /pgadmin4/web /pgadmin4
COPY --from=docs-builder /pgadmin4/docs/en_US/_build/html/ /pgadmin4/docs
COPY pkg/docker/run_pgadmin.py pkg/docker/gunicorn_config.py /pgadmin4/
COPY pkg/docker/run_pgadmin.py /pgadmin4/
COPY pkg/docker/entrypoint.sh /entrypoint.sh

# License files
COPY LICENSE /pgadmin4/LICENSE

# Configure everything in one RUN step
RUN /venv/bin/python3 -m pip install --no-cache-dir gunicorn==23.0.0 && \
RUN /venv/bin/python3 -m pip install --no-cache-dir granian==2.7.2 && \
find / -type d -name '__pycache__' -exec rm -rf {} + && \
useradd -r -u 5050 -g root -s /sbin/nologin pgadmin && \
mkdir -p /run/pgadmin /var/lib/pgadmin && \
Expand Down
27 changes: 15 additions & 12 deletions pkg/docker/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -178,25 +178,28 @@ if [ -z "${PGADMIN_DISABLE_POSTFIX}" ]; then
sudo /usr/sbin/postfix start
fi

# Get the session timeout from the pgAdmin config. We'll use this (in seconds)
# to define the Gunicorn worker timeout
TIMEOUT=$(cd /pgadmin4 && /venv/bin/python3 -c 'import config; print(config.SESSION_EXPIRATION_TIME * 60 * 60 * 24)')

# NOTE: currently pgadmin can run only with 1 worker due to sessions implementation
# Using --threads to have multi-threaded single-process worker

if [ -n "${PGADMIN_ENABLE_SOCK}" ]; then
BIND_ADDRESS="unix:/run/pgadmin/pgadmin.sock"
BIND_ARGS="--uds /run/pgadmin/pgadmin.sock"
else
BIND_ARGS="--host ${PGADMIN_LISTEN_ADDRESS:-[::]} --port ${PGADMIN_LISTEN_PORT:-80}"
if [ -n "${PGADMIN_ENABLE_TLS}" ]; then
BIND_ADDRESS="${PGADMIN_LISTEN_ADDRESS:-[::]}:${PGADMIN_LISTEN_PORT:-443}"
else
BIND_ADDRESS="${PGADMIN_LISTEN_ADDRESS:-[::]}:${PGADMIN_LISTEN_PORT:-80}"
BIND_ARGS="--host ${PGADMIN_LISTEN_ADDRESS:-[::]} --port ${PGADMIN_LISTEN_PORT:-443}"
fi
fi

if [ -n "${PGADMIN_ENABLE_TLS}" ]; then
exec /venv/bin/gunicorn --limit-request-line "${GUNICORN_LIMIT_REQUEST_LINE:-8190}" --timeout "${TIMEOUT}" --bind "${BIND_ADDRESS}" -w 1 --threads "${GUNICORN_THREADS:-25}" --access-logfile "${GUNICORN_ACCESS_LOGFILE:--}" --keyfile /certs/server.key --certfile /certs/server.cert -c gunicorn_config.py run_pgadmin:app
if [ "${GUNICORN_ACCESS_LOGFILE:--}" = "-" ]; then
ACCESS_LOG_ARGS="--access-log"
else
exec /venv/bin/gunicorn --limit-request-line "${GUNICORN_LIMIT_REQUEST_LINE:-8190}" --limit-request-fields "${GUNICORN_LIMIT_REQUEST_FIELDS:-100}" --limit-request-field_size "${GUNICORN_LIMIT_REQUEST_FIELD_SIZE:-8190}" --timeout "${TIMEOUT}" --bind "${BIND_ADDRESS}" -w 1 --threads "${GUNICORN_THREADS:-25}" --access-logfile "${GUNICORN_ACCESS_LOGFILE:--}" -c gunicorn_config.py run_pgadmin:app
ACCESS_LOG_ARGS="--no-access-log"
fi
Comment thread
Smartappli marked this conversation as resolved.

TLS_ARGS=""
if [ -n "${PGADMIN_ENABLE_TLS}" ]; then
TLS_ARGS="--ssl-keyfile /certs/server.key --ssl-certificate /certs/server.cert"
Comment on lines +201 to +203
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and inspect the actual file
find . -name "entrypoint.sh" -path "*/docker/*" | head -5

Repository: pgadmin-org/pgadmin4

Length of output: 90


🏁 Script executed:

# Check if Granian is referenced elsewhere in the entrypoint
rg "granian|tls|ssl|TLS|SSL" pkg/docker/entrypoint.sh -i -A 2 -B 2 | head -80

Repository: pgadmin-org/pgadmin4

Length of output: 1198


🌐 Web query:

Granian 2.7.2 --ssl-protocol-min default value documentation

💡 Result:

In Granian 2.7.2, the CLI option:

  • --ssl-protocol-min [tls1.2|tls1.3]

has a default value of tls1.3 (env var GRANIAN_SSL_PROTOCOL_MIN). [1]


🌐 Web query:

Gunicorn SSL certificate keyfile default TLS version

💡 Result:

  • keyfile (private key) default: None (no key configured unless you set it). (docs.gunicorn.org)
  • certfile default: None. (docs.gunicorn.org)
  • Gunicorn “default TLS version” (ssl_version) default: ssl.PROTOCOL_TLS (configured in Gunicorn as _SSLMethod.PROTOCOL_TLS). This means Gunicorn will negotiate the highest protocol version supported by both client and server, with the effective enabled/disabled versions depending on your Python + OpenSSL build and defaults. (docs.gunicorn.org)

How to explicitly control it:

  • Set a minimum (common choice): ssl_version = "TLSv1_2" (Gunicorn example also notes this as minimum TLS 1.2 for HTTP/2). (gunicorn.org)
  • For finer control (Gunicorn ≥ 20.2): use ssl_context and set context.minimum_version (e.g., TLS 1.3). (docs.gunicorn.org)

Citations:


🏁 Script executed:

# Check if there are any existing GRANIAN_* environment variables in the codebase to confirm naming convention
rg "GRANIAN_" pkg/docker/ -i

Repository: pgadmin-org/pgadmin4

Length of output: 46


🏁 Script executed:

# Check Python and OpenSSL versions in the Dockerfile
rg "FROM|python|openssl" pkg/docker/Dockerfile -i -A 1 | head -30

Repository: pgadmin-org/pgadmin4

Length of output: 125


🏁 Script executed:

# Find the Dockerfile
find . -name "Dockerfile*" -type f | head -10

Repository: pgadmin-org/pgadmin4

Length of output: 76


🏁 Script executed:

# Check the environment variable pattern used in entrypoint.sh
rg "^\s*\$\{[A-Z_]+:-" pkg/docker/entrypoint.sh | head -10

Repository: pgadmin-org/pgadmin4

Length of output: 46


🏁 Script executed:

# Check environment variable patterns in entrypoint.sh
rg "\$\{" pkg/docker/entrypoint.sh | head -20

Repository: pgadmin-org/pgadmin4

Length of output: 46


🏁 Script executed:

# Examine the Dockerfile to understand the base image and Python version
head -30 ./Dockerfile

Repository: pgadmin-org/pgadmin4

Length of output: 860


🏁 Script executed:

# Get all environment variable patterns with defaults in entrypoint.sh
grep -E '\$\{[A-Z_]+:-' pkg/docker/entrypoint.sh

Repository: pgadmin-org/pgadmin4

Length of output: 860


🏁 Script executed:

# Look for the section with TLS and surrounding context to understand the env var pattern
sed -n '190,210p' pkg/docker/entrypoint.sh

Repository: pgadmin-org/pgadmin4

Length of output: 769


Don't silently raise the TLS floor to 1.3.

Granian 2.7.2 defaults --ssl-protocol-min to tls1.3, while Gunicorn's ssl.PROTOCOL_TLS defaults to negotiating TLS 1.2+. Because this branch only adds --ssl-keyfile/--ssl-certificate, TLS-enabled containers now reject TLS 1.2 clients without any opt-in. If that isn't intentional, set an explicit minimum or surface it as a configurable migration knob.

🛠️ Suggested change
 TLS_ARGS=""
 if [ -n "${PGADMIN_ENABLE_TLS}" ]; then
-    TLS_ARGS="--ssl-keyfile /certs/server.key --ssl-certificate /certs/server.cert"
+    TLS_ARGS="--ssl-keyfile /certs/server.key --ssl-certificate /certs/server.cert --ssl-protocol-min ${GRANIAN_SSL_PROTOCOL_MIN:-tls1.2}"
 fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
TLS_ARGS=""
if [ -n "${PGADMIN_ENABLE_TLS}" ]; then
TLS_ARGS="--ssl-keyfile /certs/server.key --ssl-certificate /certs/server.cert"
TLS_ARGS=""
if [ -n "${PGADMIN_ENABLE_TLS}" ]; then
TLS_ARGS="--ssl-keyfile /certs/server.key --ssl-certificate /certs/server.cert --ssl-protocol-min ${GRANIAN_SSL_PROTOCOL_MIN:-tls1.2}"
fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/docker/entrypoint.sh` around lines 201 - 203, The TLS branch that sets
TLS_ARGS when PGADMIN_ENABLE_TLS is present currently only adds
--ssl-keyfile/--ssl-certificate and relies on defaults that may raise the
minimum protocol to TLS1.3; update entrypoint.sh so TLS_ARGS also explicitly
sets the minimum protocol (e.g., add --ssl-protocol-min tls1.2) or read a new
env var (e.g., PGADMIN_SSL_MIN_PROTOCOL) to allow opt-in upgrades, ensuring the
change is applied where TLS_ARGS is constructed and used.

fi

# Keep the existing environment variables for backward compatibility.
exec /venv/bin/granian --interface wsgi --workers 1 --blocking-threads "${GUNICORN_THREADS:-25}" ${ACCESS_LOG_ARGS} ${TLS_ARGS} ${BIND_ARGS} run_pgadmin:app
Comment on lines +206 to +207
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "entrypoint.sh" -type f | head -20

Repository: pgadmin-org/pgadmin4

Length of output: 90


🏁 Script executed:

cat -n pkg/docker/entrypoint.sh | sed -n '200,215p'

Repository: pgadmin-org/pgadmin4

Length of output: 495


🌐 Web query:

Granian 2.7.2 backpressure blocking-threads difference WSGI

💡 Result:

In Granian 2.7.2, nothing changed about backpressure or blocking-threads specifically—the 2.7.2 patch release is about ASGI websocket handling/metrics fixes (e.g., rejection flow, Django Channels hang) and dependency bumps. [1]

What “backpressure” is (and why it matters for WSGI)

Granian’s backpressure is a per-worker safeguard that stops the worker accept-loop from taking more connections when “too much work” is already queued for Python. Think of it as a server-level secondary backlog (in addition to the OS listen backlog). [2]

Key WSGI nuance: because it acts at the connection accept level, it limits connections, not individual HTTP requests; lots of keep-alive connections (common behind reverse proxies) can “consume” backpressure capacity. [2]

What “blocking-threads” is (and how it differs)

Granian’s blocking threads are the threads in a worker that interact with the Python interpreter (i.e., execute your app and contend for the GIL). [2]

Granian explicitly notes:

  • On async interfaces (ASGI/RSGI), this setting is effectively fixed to 1 (interop mainly happens via the asyncio event loop holding the GIL). [2]
  • On WSGI (sync), it becomes the maximum number of Python-executing threads per worker (i.e., your WSGI thread pool). [2]

Practical difference for WSGI

  • --blocking-threads = “How many requests can run Python code concurrently (per worker)?”
  • --backpressure = “How much total concurrency/queueing pressure (per worker) will I allow before I stop accepting more connections?”

Granian generally recommends not over-tuning the threadpool and instead setting an appropriate backpressure limit for your environment/workload. [2][3]

Rule of thumb for WSGI tuning

  • If your WSGI requests are mostly CPU-bound / rarely release the GIL, increasing backpressure won’t help much (requests just wait on the GIL). [2]
  • If your WSGI requests spend time in I/O that releases the GIL (DB/network), a higher backpressure can help keep the worker busy without letting the queue explode. [2]

Sources: GitHub release notes for 2.7.2 [1]; Granian README / docs on workers, blocking threads, and backpressure [2]; PyPI project description mirroring the same guidance [3].


Add explicit Granian backpressure; GUNICORN_THREADS alone is not enough.

Granian treats --blocking-threads (Python thread pool size) and --backpressure (connection admission control) as separate knobs. With only --blocking-threads set from GUNICORN_THREADS, request admission falls back to Granian's default backpressure, which does not account for your deployment's concurrency tuning. This leaves the server vulnerable to queue buildup under concurrent load. Wire --backpressure explicitly to maintain predictable overload behavior.

🛠️ Suggested change
 # Keep the existing environment variables for backward compatibility.
-exec /venv/bin/granian --interface wsgi --workers 1 --blocking-threads "${GUNICORN_THREADS:-25}" ${ACCESS_LOG_ARGS} ${TLS_ARGS} ${BIND_ARGS} run_pgadmin:app
+GRANIAN_BACKPRESSURE="${GRANIAN_BACKPRESSURE:-${GUNICORN_THREADS:-25}}"
+exec /venv/bin/granian --interface wsgi --workers 1 \
+    --blocking-threads "${GUNICORN_THREADS:-25}" \
+    --backpressure "${GRANIAN_BACKPRESSURE}" \
+    ${ACCESS_LOG_ARGS} ${TLS_ARGS} ${BIND_ARGS} run_pgadmin:app
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Keep the existing environment variables for backward compatibility.
exec /venv/bin/granian --interface wsgi --workers 1 --blocking-threads "${GUNICORN_THREADS:-25}" ${ACCESS_LOG_ARGS} ${TLS_ARGS} ${BIND_ARGS} run_pgadmin:app
# Keep the existing environment variables for backward compatibility.
GRANIAN_BACKPRESSURE="${GRANIAN_BACKPRESSURE:-${GUNICORN_THREADS:-25}}"
exec /venv/bin/granian --interface wsgi --workers 1 \
--blocking-threads "${GUNICORN_THREADS:-25}" \
--backpressure "${GRANIAN_BACKPRESSURE}" \
${ACCESS_LOG_ARGS} ${TLS_ARGS} ${BIND_ARGS} run_pgadmin:app
🧰 Tools
🪛 Shellcheck (0.11.0)

[info] 207-207: Double quote to prevent globbing and word splitting.

(SC2086)


[info] 207-207: Double quote to prevent globbing and word splitting.

(SC2086)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/docker/entrypoint.sh` around lines 206 - 207, The entrypoint currently
sets Granian's thread pool via --blocking-threads using the GUNICORN_THREADS env
var but omits --backpressure, so connection admission uses Granian's default;
update the exec invocation that runs granian (the line invoking
/venv/bin/granian with --interface, --workers, --blocking-threads and existing
${ACCESS_LOG_ARGS} ${TLS_ARGS} ${BIND_ARGS}) to add an explicit --backpressure
flag wired to an environment variable (e.g.
${GUNICORN_BACKPRESSURE:-<sensible-default>}) so deployment concurrency tuning
is respected and overload admission is predictable.


45 changes: 0 additions & 45 deletions pkg/docker/gunicorn_config.py

This file was deleted.

Loading