Skip to content

Comments

docker arbitrary user support via PUID and PGID#9633

Open
kon-foo wants to merge 8 commits intopgadmin-org:masterfrom
kon-foo:feature/docker-arbitrary-user-support
Open

docker arbitrary user support via PUID and PGID#9633
kon-foo wants to merge 8 commits intopgadmin-org:masterfrom
kon-foo:feature/docker-arbitrary-user-support

Conversation

@kon-foo
Copy link
Contributor

@kon-foo kon-foo commented Feb 13, 2026

This PR adds support for PUID and PGID environment variables as popularised by linuxserver.io images. This allows users to control the UID and GID of the process running gunicorn inside the container.

Why

Currently, when running pgAdmin via Docker with bind-mounted volumes, the mounted directories must be owned by the hardcoded 5050 user on the host and inside the container. This creates two issues:

  1. Permission Headaches:
    Users are forced to manually chown directories on the host to 5050 before starting the container. This is cumbersome, especially if running docker as a non-root users and potentially lose access to the mounted dir.

  2. Security Risks:
    The hardcoded 5050 UID might already be in use on the host system. Running the container as this UID could unintentionally grant pgAdmin access to files or resources owned by that UID or vise versa.

Solution

  1. Remove the hardcoded 5050 user, so the container starts then enrypoint.sh script as UID 0 by default.
  2. In the script, default to PUID 5050 and GUID 0 to keep current behaviour when no env vars are set.
  3. If the script runs as "$(id -u)" = "0" , aka no --user flag was used to run the image:
    • ensure a group with PGID exists and reassign the existing pgadmin user to PUID and PGID.
    • chown required files and dirs to said user.
    • use su-exec to drop privileges and run gunicorn as $PUID:$PGID

The check for id=0 makes sure nothing changes for users, that use the --user flag or user: in compose to run the image.

Compatability

Without PUID/PGID set, the behaviour is identical to the current image.

Tests

For each test, I cheked:

# The id of the pgadmin user
docker exec pgadmin-test id pgadmin
# The the gunicorn process runs as that user
docker exec pgadmin-test ps aux | grep gunicorn | grep -v grep
# The permissions of a mounted volume
ls -ln /tmp/pgadmin-test-data/
# Access to the web UI
curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/login
# Scenario Key results
1 Default (no PUID/PGID) ✅ Still starts as UID 5050, GID 0
2 PUID=1000 PGID=1000 ✅ UID 1000, GID 1000, vmodeolume owned correctly
3 PUID only ✅ UID custom, GID defaults to 0

Further tests conducted:

# Scenario Key results
4 TLS ✅ TLS still works. Won't work if /certs is mounted as a read-only filesystem, but that is expected.
5 Server JSON import ✅ Imported as root during initialization, no change of permissions needed.

Open Questions:

  • Are there additional paths that need to be chowned? Currently /run/pgadmin, /var/lib/pgadmin, /certs and pgadmin4/config_distro.py are covered.
  • Are there any integration tests to be run?

I have been using this for a couple of weeks now, but I am only using a very small subset of PgAdmin functionality.

Summary by CodeRabbit

  • New Features

    • PUID/PGID environment support and root-aware startup enabling the server to run as a target user.
    • Safer, conditional ownership adjustments at startup and improved OpenShift compatibility for correct file permissions.
  • Chores

    • Runtime image includes additional system utilities to expand runtime capabilities.
    • Default container runtime user changed to root.

@coderabbitai
Copy link

coderabbitai bot commented Feb 13, 2026

Walkthrough

Removes final-stage USER 5050, adds libcap and su-exec to the runtime image, and extends pkg/docker/entrypoint.sh with PUID/PGID handling, root-aware UID/GID adjustments, safe_chown ownership fixes, OpenShift passwd handling, and su-exec-wrapped Gunicorn startup.

Changes

Cohort / File(s) Summary
Docker Runtime Configuration
Dockerfile
Added libcap and su-exec to the final image apk packages; removed the final-stage USER 5050 so the container runs as the default user (root).
Entrypoint Initialization & Startup
pkg/docker/entrypoint.sh
Added PUID/PGID initialization, ensure/create group with PGID, conditional reassignment of pgadmin UID/GID when running as root, built su-exec wrapper (SU_EXEC), preserved OpenShift /etc/passwd handling, introduced safe_chown to conditionally chown runtime paths, and prefixed Gunicorn exec with $SU_EXEC in TLS and non‑TLS branches.

Sequence Diagram(s)

sequenceDiagram
    participant Container as Container Runtime
    participant Entrypoint as entrypoint.sh
    participant System as System (users/groups, filesystem)
    participant Gunicorn as Gunicorn

    Container->>Entrypoint: start (env: PUID/PGID)
    Entrypoint->>Entrypoint: detect effective UID
    alt running as root
        Entrypoint->>System: ensure group with PGID exists
        Entrypoint->>System: modify pgadmin UID/GID to PUID/PGID
        Entrypoint->>Entrypoint: set SU_EXEC to su-exec wrapper
        Entrypoint->>System: safe_chown runtime paths if ownership differs
        Entrypoint->>Gunicorn: exec via $SU_EXEC (TLS/non-TLS preserved)
    else running as non-root
        Entrypoint->>Entrypoint: preserve/append OpenShift /etc/passwd entry
        Entrypoint->>System: skip UID/GID reassignment
        Entrypoint->>Gunicorn: exec directly (no su-exec)
    end
    Gunicorn-->>Container: server running
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • yogeshmahajan-1903
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main feature: adding arbitrary user support via PUID and PGID environment variables, which is the core change in both the Dockerfile and entrypoint.sh.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@pkg/docker/entrypoint.sh`:
- Around line 199-201: The chown block should avoid errors for missing /certs,
avoid always-recursing /var/lib/pgadmin, and honor a custom config path; update
the block to: check existence of /certs before attempting chown, check current
owner of /var/lib/pgadmin (using stat) and only run chown -R if it differs from
"$PUID:$PGID" to avoid costly repeated recursion, and replace the hardcoded
/pgadmin4/config_distro.py with the PGADMIN_CUSTOM_CONFIG_DISTRO_FILE variable
(if set and the file exists) and chown that file non-recursively; ensure all
chown commands still run only when uid is 0.
🧹 Nitpick comments (3)
pkg/docker/entrypoint.sh (3)

5-19: Consider validating PUID/PGID are numeric before use.

If a user sets PUID=abc, usermod fails silently (line 12 swallows errors), then su-exec abc:0 will fail at the very end of the script with a cryptic error. Fail-fast with a clear message would improve the user experience.

🛡️ Proposed validation
 PUID=${PUID:-5050}
 PGID=${PGID:-0}
+
+# Validate PUID/PGID are numeric
+if ! echo "$PUID" | grep -qE '^[0-9]+$'; then
+    echo "Error: PUID must be a numeric value, got '$PUID'" >&2
+    exit 1
+fi
+if ! echo "$PGID" | grep -qE '^[0-9]+$'; then
+    echo "Error: PGID must be a numeric value, got '$PGID'" >&2
+    exit 1
+fi
 
 if [ "$(id -u)" = "0" ]; then

155-162: DB initialization runs as root before ownership is fixed.

run_pgadmin.py (line 157) and load-servers (line 113) execute as UID 0 when the container starts as root, creating files in /var/lib/pgadmin owned by root. The chown -R at line 199 corrects this, but if the script fails between these points the volume is left root-owned.

An alternative is to move the chown + su-exec wrapping earlier, so that the initialization itself runs as the target user. This would also eliminate the need for the expensive recursive chown.


12-12: Silent usermod failure could mislead operators.

2>/dev/null || true suppresses all diagnostics. If usermod fails for an unexpected reason (e.g., shadow/passwd locked), the script continues and line 16 still prints the intended UID/GID — but the pgadmin user was never actually reassigned. Gunicorn will still launch via su-exec with the numeric UID (so it works), but any files created during init (lines 157–187) will be owned by the old pgadmin UID.

Consider logging the failure:

Proposed change
-    usermod -o -u "$PUID" -g "$PGID" pgadmin 2>/dev/null || true
+    if ! usermod -o -u "$PUID" -g "$PGID" pgadmin 2>/dev/null; then
+        echo "Warning: failed to reassign pgadmin user to UID=$PUID GID=$PGID" >&2
+    fi

@khushboovashi
Copy link
Contributor

@kon-foo, please fix the issues reported by CodeRabbit.

… has the correct ownership before chowning it
@kon-foo
Copy link
Contributor Author

kon-foo commented Feb 20, 2026

@khushboovashi I added a safe_chown function to check if a dir/file exists and if it already has the correct ownership before chowning it. The ownership check of course has the limitations that it only checks the top-level paths, but checking recursively would kinda negate the performance benefit.
We're now also chowning $CONFIG_DISTRO_FILE_PATH to account for $PGADMIN_CUSTOM_CONFIG_DISTRO_FILE.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
pkg/docker/entrypoint.sh (2)

2-3: Consider validating PUID and PGID are numeric.

Non-numeric values will propagate silently to addgroup, usermod, su-exec, chown, and stat comparisons, potentially causing cryptic failures. A simple guard up front would make misconfiguration obvious.

💡 Example guard
 PUID=${PUID:-5050}
 PGID=${PGID:-0}
+
+case "$PUID" in ''|*[!0-9]*) echo "Error: PUID must be numeric, got '$PUID'" >&2; exit 1;; esac
+case "$PGID" in ''|*[!0-9]*) echo "Error: PGID must be numeric, got '$PGID'" >&2; exit 1;; esac
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/docker/entrypoint.sh` around lines 2 - 3, The script sets PUID and PGID
but doesn't validate they're numeric; add an early guard in entrypoint.sh that
checks PUID and PGID are integers (e.g., using a regex or POSIX-safe test) and
prints a clear error and exits non-zero if either is non-numeric. Update the top
of the script where PUID and PGID are assigned (references: PUID, PGID) to
perform the validation before any calls to addgroup, usermod, su-exec, chown, or
stat so misconfiguration fails fast with a helpful message.

5-19: Root initialization block looks correct overall; one note on usermod error suppression.

The group-creation guard, usermod -o for non-unique UID support, and SU_EXEC composition are sound. The 2>/dev/null || true on usermod (line 12) will silently swallow real failures (e.g., if the pgadmin user is missing from the image due to a Dockerfile regression). Consider logging a warning on failure instead of a blanket suppress:

💡 Example
-    usermod -o -u "$PUID" -g "$PGID" pgadmin 2>/dev/null || true
+    if ! usermod -o -u "$PUID" -g "$PGID" pgadmin 2>&1; then
+        echo "Warning: failed to reassign pgadmin user to UID=$PUID GID=$PGID" >&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 5 - 19, The usermod call currently
silences all failures (usermod ... pgadmin 2>/dev/null || true), which can hide
real issues like a missing pgadmin user; update the root-init block so that
after attempting usermod (functionality around usermod, getent, addgroup, PUID,
PGID, pgadmin, and SU_EXEC) failures are detected and logged: run usermod
without swallowing stderr, capture its exit code, and if non-zero emit a clear
warning/error message via echo (or logger) mentioning pgadmin and the PUID/PGID
values, but continue gracefully (or exit if you prefer) so regressions are
visible in container startup logs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/docker/entrypoint.sh`:
- Around line 30-49: The safe_chown function is comparing
current_uid/current_gid to the wrong positional parameters (it currently checks
against "$1" and "$2"), causing chown to run every time; update the function to
compare current_uid to the UID argument and current_gid to the GID argument
(i.e., use the second and third positional params or assign them to local
uid/gid vars) before calling chown -R, referencing the safe_chown function and
the current_uid/current_gid/target/owner symbols to locate where to change the
if-condition.

---

Nitpick comments:
In `@pkg/docker/entrypoint.sh`:
- Around line 2-3: The script sets PUID and PGID but doesn't validate they're
numeric; add an early guard in entrypoint.sh that checks PUID and PGID are
integers (e.g., using a regex or POSIX-safe test) and prints a clear error and
exits non-zero if either is non-numeric. Update the top of the script where PUID
and PGID are assigned (references: PUID, PGID) to perform the validation before
any calls to addgroup, usermod, su-exec, chown, or stat so misconfiguration
fails fast with a helpful message.
- Around line 5-19: The usermod call currently silences all failures (usermod
... pgadmin 2>/dev/null || true), which can hide real issues like a missing
pgadmin user; update the root-init block so that after attempting usermod
(functionality around usermod, getent, addgroup, PUID, PGID, pgadmin, and
SU_EXEC) failures are detected and logged: run usermod without swallowing
stderr, capture its exit code, and if non-zero emit a clear warning/error
message via echo (or logger) mentioning pgadmin and the PUID/PGID values, but
continue gracefully (or exit if you prefer) so regressions are visible in
container startup logs.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
pkg/docker/entrypoint.sh (2)

2-3: Consider validating that PUID and PGID are numeric.

Non-numeric values (e.g., PUID=foo) will cascade into cryptic failures from addgroup, usermod, chown, and su-exec with no clear indication of the root cause. A quick guard would improve UX:

Example validation
 PUID=${PUID:-5050}
 PGID=${PGID:-0}
+
+case "$PUID" in ''|*[!0-9]*) echo "ERROR: PUID must be numeric (got '$PUID')" >&2; exit 1;; esac
+case "$PGID" in ''|*[!0-9]*) echo "ERROR: PGID must be numeric (got '$PGID')" >&2; exit 1;; esac
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/docker/entrypoint.sh` around lines 2 - 3, Add a validation guard in
entrypoint.sh immediately after the PUID and PGID assignments to ensure both are
numeric; check PUID and PGID (the PUID and PGID variables used later with
addgroup, usermod, chown and su-exec) with a simple numeric check (e.g., regex
or shell arithmetic test) and if either is non-numeric emit a clear error to
stderr and exit non-zero so downstream commands don't produce cryptic failures.
Ensure the check covers empty/non-integer values and prints which variable is
invalid to aid debugging.

12-12: Consider logging when usermod fails to aid debugging of non-standard configurations.

The 2>/dev/null || true suppresses all errors from usermod. While the shadow package is installed and the pgadmin user exists, so usermod will succeed in normal cases, silently hiding failures makes it harder to debug if someone passes an invalid PUID or PGID (e.g., non-numeric values). Although su-exec and chown use numeric IDs and will work regardless, some applications might call getpwuid() or whoami inside the container, which could fail with a confusing error if the passwd entry doesn't match the execution UID.

Consider logging a warning for visibility into potential misconfigurations:

Suggested improvement
-    usermod -o -u "$PUID" -g "$PGID" pgadmin 2>/dev/null || true
+    if ! usermod -o -u "$PUID" -g "$PGID" pgadmin 2>/dev/null; then
+        echo "WARNING: failed to update pgadmin user to UID=$PUID GID=$PGID; ensure values are numeric" >&2
+    fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/docker/entrypoint.sh` at line 12, The usermod call in entrypoint.sh
currently suppresses all errors with "2>/dev/null || true", which hides failures
when invalid PUID/PGID are passed; change it so failures are logged but the
script still continues: run usermod -o -u "$PUID" -g "$PGID" pgadmin, capture
its exit status and stderr, and if it fails call logger or echo a warning that
includes the failing command, the provided PUID/PGID values and the captured
error message (then continue execution). Reference the usermod invocation and
the PUID/PGID/pgadmin variables in the entrypoint.sh to locate and update the
code.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@pkg/docker/entrypoint.sh`:
- Around line 2-3: Add a validation guard in entrypoint.sh immediately after the
PUID and PGID assignments to ensure both are numeric; check PUID and PGID (the
PUID and PGID variables used later with addgroup, usermod, chown and su-exec)
with a simple numeric check (e.g., regex or shell arithmetic test) and if either
is non-numeric emit a clear error to stderr and exit non-zero so downstream
commands don't produce cryptic failures. Ensure the check covers
empty/non-integer values and prints which variable is invalid to aid debugging.
- Line 12: The usermod call in entrypoint.sh currently suppresses all errors
with "2>/dev/null || true", which hides failures when invalid PUID/PGID are
passed; change it so failures are logged but the script still continues: run
usermod -o -u "$PUID" -g "$PGID" pgadmin, capture its exit status and stderr,
and if it fails call logger or echo a warning that includes the failing command,
the provided PUID/PGID values and the captured error message (then continue
execution). Reference the usermod invocation and the PUID/PGID/pgadmin variables
in the entrypoint.sh to locate and update the code.

@kon-foo
Copy link
Contributor Author

kon-foo commented Feb 20, 2026

Regarding the "Nitpick" comment by @coderabbitai
Consider validating that PUID and PGID are numeric.
Non-numeric PUIDs or PGIDs lead to very clear error messages already, before failing to start:

addgroup: invalid number 'test'
pgAdmin will run as UID=1000, GID=test
...
chown: unknown user/group 1000:test
su-exec: getgrnam(asdasd): Invalid argument

Consider logging when usermod fails to aid debugging
The -o flag makes it even work with duplicate IDs. However I agree that supressing an error silently is not good so now we issue an additional warning when usermod fails.

WARNING: usermod failed for UID=test GID=dummy

@coderabbitai
Copy link

coderabbitai bot commented Feb 20, 2026

Tip

For best results, initiate chat on the files or code changes.

@kon-foo, that's a fair point! The error messages from chown and su-exec are indeed clear and immediately identify the invalid value. Adding explicit validation would be redundant in this case. Thanks for the clarification!


🧠 Learnings used
Learnt from: Guiorgy
Repo: pgadmin-org/pgadmin4 PR: 0
File: :0-0
Timestamp: 2025-11-20T20:14:11.407Z
Learning: In the pgadmin4 Dockerfile, the sudoers entry for `pgadminr` (line containing `echo "pgadminr ALL = NOPASSWD: /usr/sbin/postfix start" >> /etc/sudoers.d/postfix`) is intentional and not a typo. The `pgadminr` user is dynamically created by the docker entrypoint script when the container runs with a non-default UID (not 5050) and the user can write to /etc/passwd. Both `pgadmin` and `pgadminr` sudoers entries are needed to support different container execution scenarios.

@coderabbitai
Copy link

coderabbitai bot commented Feb 20, 2026

Tip

For best results, initiate chat on the files or code changes.

@kon-foo, thank you for addressing the logging concern! Adding the warning message when usermod fails is a great improvement for debugging. The WARNING: usermod failed for UID=test GID=dummy message will definitely help users quickly identify configuration issues without silently suppressing errors.

Your implementation strikes a good balance between error visibility and graceful handling. 👍


🧠 Learnings used
Learnt from: Guiorgy
Repo: pgadmin-org/pgadmin4 PR: 0
File: :0-0
Timestamp: 2025-11-20T20:14:11.407Z
Learning: In the pgadmin4 Dockerfile, the sudoers entry for `pgadminr` (line containing `echo "pgadminr ALL = NOPASSWD: /usr/sbin/postfix start" >> /etc/sudoers.d/postfix`) is intentional and not a typo. The `pgadminr` user is dynamically created by the docker entrypoint script when the container runs with a non-default UID (not 5050) and the user can write to /etc/passwd. Both `pgadmin` and `pgadminr` sudoers entries are needed to support different container execution scenarios.

@coderabbitai
Copy link

coderabbitai bot commented Feb 20, 2026

Tip

For best results, initiate chat on the files or code changes.

@kon-foo, I've already addressed both points from your comment in my previous replies:

  1. PUID/PGID validation: Agreed that the error messages from addgroup, chown, and su-exec are sufficiently clear, making explicit validation redundant.

  2. Usermod logging: Acknowledged and appreciated the addition of the warning message for debugging. The implementation properly balances error visibility with graceful handling.

Both concerns have been resolved! 👍


🧠 Learnings used
Learnt from: Guiorgy
Repo: pgadmin-org/pgadmin4 PR: 0
File: :0-0
Timestamp: 2025-11-20T20:14:11.407Z
Learning: In the pgadmin4 Dockerfile, the sudoers entry for `pgadminr` (line containing `echo "pgadminr ALL = NOPASSWD: /usr/sbin/postfix start" >> /etc/sudoers.d/postfix`) is intentional and not a typo. The `pgadminr` user is dynamically created by the docker entrypoint script when the container runs with a non-default UID (not 5050) and the user can write to /etc/passwd. Both `pgadmin` and `pgadminr` sudoers entries are needed to support different container execution scenarios.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
pkg/docker/entrypoint.sh (1)

31-50: safe_chown ownership comparison is correct — past issue resolved.

The comparison [ "$current_uid" = "$2" ] && [ "$current_gid" = "$3" ] now correctly references the UID and GID positional parameters (fixed in d3f2eb5). The existence guard at line 37 also cleanly handles paths that may not be present (e.g., /certs in non-TLS setups).

One optional improvement: chown -R is always used, even when $target is a regular file (e.g., $CONFIG_DISTRO_FILE_PATH). For files, -R is harmless, but you could avoid the flag for non-directories to make intent clearer:

♻️ Optional: skip `-R` for regular files
+    if [ -d "$target" ]; then
         chown -R "$owner" "$target"
+    else
+        chown "$owner" "$target"
+    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 31 - 50, The safe_chown helper
currently always calls chown -R "$owner" "$target"; modify safe_chown so it
detects whether "$target" is a directory or a regular file (use test -d / -f)
and only adds the -R flag for directories, calling chown "$owner" "$target" for
files; keep the existing existence check and UID/GID comparison logic in
safe_chown to avoid changing behavior for missing or already-owned paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@pkg/docker/entrypoint.sh`:
- Around line 31-50: The safe_chown helper currently always calls chown -R
"$owner" "$target"; modify safe_chown so it detects whether "$target" is a
directory or a regular file (use test -d / -f) and only adds the -R flag for
directories, calling chown "$owner" "$target" for files; keep the existing
existence check and UID/GID comparison logic in safe_chown to avoid changing
behavior for missing or already-owned paths.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants