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