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 <noreply@anthropic.com>
This commit is contained in:
Bart BroereandClaude Opus 5.5 committed 2026-10-14 11:19:58 +02:00
1 parent 47b8f5f746
commit e72af59401
4 files changed
+98 -20

No files matched your search

+19 -5
View File
@@ -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"]
+23 -9
View File
@@ -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 <http://localhost:7474/> 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: <http://localhost:7474/> | PostgreSQL user `postgres` and `POSTGRES_PASSWORD` |
| 7687 | Bolt, for Neo4j drivers and cypher-shell (`neo4j://localhost:7687`) | same as above |
| 5050 | pgAdmin: <http://localhost:5050/> | `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
+38 -6
View File
@@ -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 <<EOF
{"Servers": {"1": {"Name": "peo4c", "Group": "Servers", "Host": "127.0.0.1", "Port": 5432,
"MaintenanceDB": "${PEO4C_PG_DATABASE}", "Username": "${POSTGRES_USER:-postgres}", "SSLMode": "prefer"}}}
EOF
chown postgres:postgres /var/lib/pgadmin/servers.json
PGADMIN_SETUP_EMAIL="$email" \
PGADMIN_SETUP_PASSWORD="${PGADMIN_DEFAULT_PASSWORD:-${POSTGRES_PASSWORD:-}}" \
gosu postgres /opt/pgadmin/bin/python "$app/setup.py" setup-db \
&& gosu postgres /opt/pgadmin/bin/python "$app/setup.py" load-servers \
/var/lib/pgadmin/servers.json --user "$email" \
|| { rm -f /var/lib/pgadmin/pgadmin4.db; return 1; }
fi
gosu postgres /opt/pgadmin/bin/gunicorn --bind "0.0.0.0:${PEO4C_PGADMIN_PORT}" \
--workers 1 --threads 25 --timeout 0 --chdir "$app" --log-level warning pgAdmin4:app &
pgadmin_pid=$!
echo "peo4c: pgAdmin on http://localhost:${PEO4C_PGADMIN_PORT}/ (login: $email)"
}
docker-entrypoint.sh "$@" &
pg_pid=$!
@@ -16,16 +42,22 @@ cd /opt/peo4c/bridge
gosu postgres python3 -m peo4c &
bridge_pid=$!
pgadmin_pid=
if [ "${PEO4C_PGADMIN}" = "true" ]; then
start_pgadmin || echo "peo4c: WARNING: pgAdmin setup failed, continuing without it" >&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"
+18
View File
@@ -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