From e72af594012d1c4d8270cd1c665acda70d2bcac9 Mon Sep 17 00:00:00 2001 From: Bart Broere Date: Wed, 14 Oct 2026 11:19:58 +0200 Subject: [PATCH] Add pgAdmin to the peo4c image Install pgAdmin 9.18 in its own virtualenv and run it under gunicorn on port 5050 next to PostgreSQL and the bridge. On first start the entrypoint creates the pgAdmin login (PGADMIN_DEFAULT_EMAIL, default admin@example.com; PGADMIN_DEFAULT_PASSWORD, default POSTGRES_PASSWORD) and registers the bundled PostgreSQL server. pgAdmin is optional (PEO4C_PGADMIN=false) and a failure in it does not stop the container. Co-Authored-By: Claude Opus 5.5 --- Dockerfile | 24 ++++++++++++++---- README.md | 32 +++++++++++++++++------- docker/peo4c-entrypoint.sh | 44 ++++++++++++++++++++++++++++----- docker/pgadmin/config_distro.py | 18 ++++++++++++++ 4 files changed, 98 insertions(+), 20 deletions(-) create mode 100644 docker/pgadmin/config_distro.py diff --git a/Dockerfile b/Dockerfile index bad42ea..70657c3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,9 +3,10 @@ # Stage 1 builds the neo4j-browser static frontend from source. # Stage 2 is the Apache AGE image (PostgreSQL with the AGE extension) plus the # peo4c bridge, which speaks the Bolt protocol to the browser and translates -# Cypher into AGE `cypher()` calls. +# Cypher into AGE `cypher()` calls, and pgAdmin as a SQL interface. ARG AGE_IMAGE=docker.io/apache/age:release_PG18_1.8.0 +ARG PGADMIN_VERSION=9.18 FROM docker.io/library/node:16.20.2-bullseye AS browser @@ -23,13 +24,23 @@ RUN yarn build && test -f dist/index.html FROM ${AGE_IMAGE} +ARG PGADMIN_VERSION + RUN apt-get update \ - && apt-get install -y --no-install-recommends python3 python3-psycopg \ + && apt-get install -y --no-install-recommends python3 python3-psycopg python3-venv \ && rm -rf /var/lib/apt/lists/* +RUN python3 -m venv /opt/pgadmin \ + && /opt/pgadmin/bin/pip install --no-cache-dir "pgadmin4==${PGADMIN_VERSION}" gunicorn \ + && ln -s "$(/opt/pgadmin/bin/python -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')/pgadmin4" \ + /opt/pgadmin/app \ + && mkdir -p /var/lib/pgadmin \ + && chown postgres:postgres /var/lib/pgadmin + COPY --from=browser /src/dist /opt/peo4c/browser COPY bridge /opt/peo4c/bridge COPY docker/initdb/ /docker-entrypoint-initdb.d/ +COPY docker/pgadmin/config_distro.py /opt/peo4c/pgadmin/config_distro.py COPY docker/peo4c-entrypoint.sh /usr/local/bin/peo4c-entrypoint.sh ENV PEO4C_DEFAULT_GRAPH=neo4j \ @@ -37,10 +48,13 @@ ENV PEO4C_DEFAULT_GRAPH=neo4j \ PEO4C_HTTP_PORT=7474 \ PEO4C_BOLT_PORT=7687 \ PEO4C_PG_HOST=127.0.0.1 \ - PEO4C_PG_PORT=5432 + PEO4C_PG_PORT=5432 \ + PEO4C_PGADMIN=true \ + PEO4C_PGADMIN_PORT=5050 \ + CONFIG_DISTRO_FILE_PATH=/opt/peo4c/pgadmin/config_distro.py -# 7474: neo4j-browser (HTTP), 7687: Bolt, 5432: PostgreSQL -EXPOSE 7474 7687 5432 +# 7474: neo4j-browser (HTTP), 7687: Bolt, 5050: pgAdmin, 5432: PostgreSQL +EXPOSE 7474 7687 5050 5432 ENTRYPOINT ["/usr/local/bin/peo4c-entrypoint.sh"] CMD ["postgres", "-c", "shared_preload_libraries=age"] diff --git a/README.md b/README.md index de05a93..4917d57 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ [neo4j-browser](https://github.com/neo4j/neo4j-browser) running on PostgreSQL, using [Apache AGE](https://github.com/apache/age) as the graph engine. -Everything ships in one container image: +Everything ships in one container image, together with +[pgAdmin](https://www.pgadmin.org/) for working with the same data in SQL: ``` neo4j-browser (static files) peo4c bridge (Python) PostgreSQL + AGE @@ -19,14 +20,20 @@ Everything ships in one container image: ```sh podman build -t peo4c . podman run -d --name peo4c -e POSTGRES_PASSWORD=secret \ - -p 7474:7474 -p 7687:7687 -p 5432:5432 \ - -v peo4c-data:/var/lib/postgresql peo4c + -p 7474:7474 -p 7687:7687 -p 5050:5050 -p 5432:5432 \ + -v peo4c-data:/var/lib/postgresql -v peo4c-pgadmin:/var/lib/pgadmin peo4c ``` -Open and log in with your **PostgreSQL** credentials: -user `postgres`, password the value of `POSTGRES_PASSWORD`. Bolt clients (the Neo4j -drivers, cypher-shell) can connect to `neo4j://localhost:7687` the same way, and -`psql` works on port 5432. +| Port | Service | Log in with | +| --- | --- | --- | +| 7474 | neo4j-browser: | PostgreSQL user `postgres` and `POSTGRES_PASSWORD` | +| 7687 | Bolt, for Neo4j drivers and cypher-shell (`neo4j://localhost:7687`) | same as above | +| 5050 | pgAdmin: | `admin@example.com` and `POSTGRES_PASSWORD` | +| 5432 | PostgreSQL, for `psql` and other SQL clients | same as neo4j-browser | + +pgAdmin comes with the bundled PostgreSQL server already registered as "peo4c"; +it asks for the PostgreSQL password the first time you open it. AGE stores each +graph as a schema of the same name, with one table per label. `docker` works as a drop-in replacement for `podman` in the commands above. @@ -59,9 +66,16 @@ drivers, cypher-shell) can connect to `neo4j://localhost:7687` the same way, and | `PEO4C_ADVERTISED_BOLT_PORT` | `7687` | Bolt port advertised to the browser; set it when you publish 7687 on another host port. | | `PEO4C_ADVERTISED_HOST` | `localhost` | Host reported in `SHOW DATABASES` and to clients that do not send a routing address. | | `PEO4C_LOG_LEVEL` | `INFO` | `DEBUG` logs every generated SQL statement. | +| `PGADMIN_DEFAULT_EMAIL` | `admin@example.com` | pgAdmin login, created on first start. | +| `PGADMIN_DEFAULT_PASSWORD` | `$POSTGRES_PASSWORD` | pgAdmin login password, set on first start. | +| `PEO4C_PGADMIN` | `true` | Set to `false` to not start pgAdmin. | +| `PEO4C_PGADMIN_PORT` | `5050` | Port pgAdmin listens on inside the container. | -Build arguments: `AGE_IMAGE` (base image, default `apache/age:release_PG18_1.8.0`) and -`NEO4J_BROWSER_REF` (neo4j-browser commit to build). +Build arguments: `AGE_IMAGE` (base image, default `apache/age:release_PG18_1.8.0`), +`NEO4J_BROWSER_REF` (neo4j-browser commit to build) and `PGADMIN_VERSION` (default `9.18`). + +pgAdmin keeps its users and settings in `/var/lib/pgadmin`; changing +`PGADMIN_DEFAULT_*` after the first start has no effect unless that directory is empty. ## Limitations diff --git a/docker/peo4c-entrypoint.sh b/docker/peo4c-entrypoint.sh index 9f93e74..0e6458c 100755 --- a/docker/peo4c-entrypoint.sh +++ b/docker/peo4c-entrypoint.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash -# Runs PostgreSQL (through the stock postgres entrypoint) and the peo4c bridge -# side by side, and stops the container when either of them exits. +# Runs PostgreSQL (through the stock postgres entrypoint), the peo4c bridge and +# pgAdmin side by side. The container stops when PostgreSQL or the bridge exits; +# pgAdmin is optional and its failure does not take the others down. set -euo pipefail if [ "${1:-}" != "postgres" ]; then @@ -9,6 +10,31 @@ fi export PEO4C_PG_DATABASE="${PEO4C_PG_DATABASE:-${POSTGRES_DB:-${POSTGRES_USER:-postgres}}}" +start_pgadmin() { + local app=/opt/pgadmin/app + local email="${PGADMIN_DEFAULT_EMAIL:-admin@example.com}" + + if [ ! -f /var/lib/pgadmin/pgadmin4.db ]; then + # First start: create the login user and register the bundled PostgreSQL. + cat > /var/lib/pgadmin/servers.json <&2 +fi + shutdown() { - kill -TERM "$bridge_pid" 2>/dev/null || true - kill -TERM "$pg_pid" 2>/dev/null || true + for pid in "$bridge_pid" $pgadmin_pid "$pg_pid"; do + kill -TERM "$pid" 2>/dev/null || true + done } trap shutdown TERM INT -# Wait until one of the two processes exits, then stop the other one too. +# Wait until PostgreSQL or the bridge exits, then stop everything. set +e wait -n "$pg_pid" "$bridge_pid" status=$? shutdown -wait "$pg_pid" 2>/dev/null +wait 2>/dev/null exit "$status" diff --git a/docker/pgadmin/config_distro.py b/docker/pgadmin/config_distro.py new file mode 100644 index 0000000..0ee0039 --- /dev/null +++ b/docker/pgadmin/config_distro.py @@ -0,0 +1,18 @@ +# pgAdmin settings for the peo4c image, loaded via CONFIG_DISTRO_FILE_PATH. +# Paths derived from DATA_DIR in pgAdmin's config.py are computed before this +# file is applied, so they are all set explicitly here. + +DATA_DIR = '/var/lib/pgadmin' +SQLITE_PATH = DATA_DIR + '/pgadmin4.db' +SESSION_DB_PATH = DATA_DIR + '/sessions' +STORAGE_DIR = DATA_DIR + '/storage' +KERBEROS_CCACHE_DIR = DATA_DIR + '/krbccache' +AZURE_CREDENTIAL_CACHE_DIR = DATA_DIR + '/azurecredentialcache' +LOG_FILE = '/dev/null' + +SERVER_MODE = True +UPGRADE_CHECK_ENABLED = False + +# From the config_distro.py shipped in the pgadmin4 wheel, which this replaces. +HELP_PATH = '../../docs/en_US/_build/html/' +MINIFY_HTML = False