From 120c663e61940819133525036558702cfce26f93 Mon Sep 17 00:00:00 2001 From: jh-RLI Date: Tue, 19 May 2026 19:29:04 +0200 Subject: [PATCH 01/25] #2319: Initial podman setup, more iterations follow --- podman/Dockerfile | 57 +++++++++++++++++++++++++ podman/README.md | 84 +++++++++++++++++++++++++++++++++++++ podman/entrypoint.sh | 60 ++++++++++++++++++++++++++ podman/podman-compose.yaml | 86 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 287 insertions(+) create mode 100644 podman/Dockerfile create mode 100644 podman/README.md create mode 100644 podman/entrypoint.sh create mode 100644 podman/podman-compose.yaml diff --git a/podman/Dockerfile b/podman/Dockerfile new file mode 100644 index 000000000..e7140828a --- /dev/null +++ b/podman/Dockerfile @@ -0,0 +1,57 @@ +# SPDX-FileCopyrightText: 2025 Jonas Huber © Reiner Lemoine Institut +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +# ── Stage 1: build Vite frontend assets ────────────────────────────────────── +FROM node:25.2.1 AS vite-build + +WORKDIR /app + +# Install deps first for better layer caching +COPY package*.json ./ +RUN npm ci + +COPY . . +RUN npm run build +# Output: /app/assets/ (including manifest.json read by django-vite) + + +# ── Stage 2: production application ────────────────────────────────────────── +FROM python:3.10.14 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends apache2 apache2-dev wget unzip \ + && rm -rf /var/lib/apt/lists/* + +# Enable required Apache modules +RUN a2enmod headers + +WORKDIR /app + +COPY requirements.txt /app/requirements.txt +RUN pip install -r requirements.txt \ + && pip install mod_wsgi \ + && mod_wsgi-express module-config >> /etc/apache2/apache2.conf + +COPY podman/apache2.conf /etc/apache2/conf-enabled/oeplatform.conf +COPY . /app + +# Overwrite any local assets/ with the freshly built Vite output from stage 1 +COPY --from=vite-build /app/assets /app/assets + +COPY podman/entrypoint.sh /app/entrypoint.sh +RUN chmod +x /app/entrypoint.sh + +# Build-time static asset collection (no DB needed) +# Compress is also run at build time since it only reads templates and static files +RUN cp /app/oeplatform/securitysettings.py.default /app/oeplatform/securitysettings.py \ + && python manage.py collectstatic --noinput \ + && python manage.py compress --force \ + && rm /app/oeplatform/securitysettings.py + +# Volume mount points — created here so named volumes initialise correctly +RUN mkdir -p /app/ontologies /app/media/oeo_ext + +EXPOSE 80 + +CMD ["/app/entrypoint.sh"] diff --git a/podman/README.md b/podman/README.md new file mode 100644 index 000000000..9360ab163 --- /dev/null +++ b/podman/README.md @@ -0,0 +1,84 @@ + + +# Podman Usage + +> Tested on Linux with rootless Podman. Requires `podman` and `podman-compose`. + +This directory contains the Podman-based production deployment for OEPlatform. +All application code and static assets are baked into the container images at +build time — no bind mounts are used. + +## Prerequisites + +- [Podman](https://podman.io/getting-started/installation) ≥ 4.0 +- [podman-compose](https://github.com/containers/podman-compose) ≥ 1.0 +- Rootless Podman configured (`/etc/subuid` and `/etc/subgid` entries for your + user) + +Install podman-compose (if not already installed): + +```sh +pip install podman-compose +# or via your distro's package manager, e.g.: +# dnf install podman-compose (Fedora/RHEL) +# apt install podman-compose (Debian/Ubuntu) +``` + +## Services + +| Service | Description | Default port | +| ------------ | ------------------------------------- | ------------ | +| `postgres` | PostgreSQL with pre-seeded OEP schema | 5432 | +| `fuseki` | Apache Jena Fuseki triple store | 3030 | +| `oeplatform` | OEP web app (Apache2) | 8080 | +| `ontop` | Ontop SPARQL endpoint | 8081 | +| `lookup` | DBpedia Lookup service | 3004 | + +## Start the Stack + +Run all commands from the **repository root**. + +```sh +podman-compose -f podman/podman-compose.yaml up -d +``` + +## Stop the Stack + +```sh +podman-compose -f podman/podman-compose.yaml down +``` + +## Override Ports + +Default ports can be changed via environment variables before starting: + +```sh +export OEP_PORT_WEB=9090 +export OEP_PORT_POSTGRES=5433 +``` + +## View Logs + +```sh +podman-compose -f podman/podman-compose.yaml logs -f oeplatform +``` + +## Open a Shell + +```sh +podman exec -it oeplatform bash +``` + +## Reset Database + +```sh +podman-compose -f podman/podman-compose.yaml down +podman volume rm podman_pgdata # check exact name with: podman volume ls +podman-compose -f podman/podman-compose.yaml up -d +``` + +The postgres container recreates all tables on a fresh volume automatically. diff --git a/podman/entrypoint.sh b/podman/entrypoint.sh new file mode 100644 index 000000000..3883723fe --- /dev/null +++ b/podman/entrypoint.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: 2025 Jonas Huber © Reiner Lemoine Institut +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +set -euo pipefail + +# ── 1) Ontologies ───────────────────────────────────────────────────────────── +# Download the latest OEO release only on first start. The ontologies/ directory +# is a named volume so this survives container rebuilds. +ONT_DIR=/app/ontologies + +if [ ! -d "${ONT_DIR}/oeo" ]; then + echo "Downloading latest OEO release…" + mkdir -p "${ONT_DIR}" + wget -qO /tmp/oeo.zip \ + https://github.com/OpenEnergyPlatform/ontology/releases/latest/download/build-files.zip + unzip -q /tmp/oeo.zip -d "${ONT_DIR}" + rm /tmp/oeo.zip + echo "OEO downloaded to ${ONT_DIR}" +else + echo "OEO already present, skipping download." +fi + +# ── 2) OEO extended ─────────────────────────────────────────────────────────── +# Seed the empty template only when no oeo_ext.owl exists yet. The media/ +# directory is a named volume, so the file persists across container restarts +# and rebuilds and will never be overwritten here. +OEO_EXT=/app/media/oeo_ext/oeo_ext.owl + +if [ ! -f "${OEO_EXT}" ]; then + echo "Seeding empty OEO-extended template…" + mkdir -p /app/media/oeo_ext + cp /app/oeo_ext/oeo_extended_store/oeox_template/oeo_ext_template_empty.owl "${OEO_EXT}" + echo "OEO-extended template written to ${OEO_EXT}" +else + echo "OEO-extended file already exists, skipping seed." +fi +# TODO: load oeo_ext.owl into Fuseki as a named graph so it is queryable via +# SPARQL alongside the base OEO. See GitHub issue #. + +# ── 3) Security settings ────────────────────────────────────────────────────── +SEC=/app/oeplatform/securitysettings.py +SEC_DEF=/app/oeplatform/securitysettings.py.default + +if [ ! -f "${SEC}" ]; then + echo "Copying default securitysettings…" + cp "${SEC_DEF}" "${SEC}" +fi + +# ── 4) Database migrations ──────────────────────────────────────────────────── +echo "Applying Django migrations…" +python manage.py migrate --no-input + +echo "Applying Alembic migrations…" +python manage.py alembic upgrade head + +# ── 5) Start Apache ─────────────────────────────────────────────────────────── +echo "Starting Apache…" +exec /usr/sbin/apache2ctl -DFOREGROUND diff --git a/podman/podman-compose.yaml b/podman/podman-compose.yaml new file mode 100644 index 000000000..83f70ce23 --- /dev/null +++ b/podman/podman-compose.yaml @@ -0,0 +1,86 @@ +# SPDX-FileCopyrightText: 2025 Jonas Huber © Reiner Lemoine Institut +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +# Podman Compose production stack. +# Run from the repository root: +# podman-compose -f podman/podman-compose.yaml up -d + +volumes: + pgdata: + fuseki_databases: + oeplatform_ontologies: + oeplatform_media: + +services: + postgres: + image: ghcr.io/openenergyplatform/oeplatform-postgres:latest + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + volumes: + - pgdata:/var/lib/postgresql/data + ports: + - "${OEP_PORT_POSTGRES:-5432}:5432" + + # TODO: load oeo_ext.owl into Fuseki as a named graph so it is queryable via + # SPARQL alongside the base OEO. See GitHub issue #. + fuseki: + image: stain/jena-fuseki:5.1.0 + container_name: fuseki + environment: + ADMIN_PASSWORD: ${FUSEKI_ADMIN_PASSWORD} + FUSEKI_DATASET_1: ${FUSEKI_DATASET_1} + volumes: + - fuseki_databases:/home/fuseki/databases + restart: unless-stopped + ports: + - "${OEP_PORT_FUSEKI:-3030}:3030" + + oeplatform: + build: + context: .. + dockerfile: podman/Dockerfile + container_name: oeplatform + ports: + - "${OEP_PORT_WEB:-8080}:80" + environment: + OEP_DJANGO_USER: ${OEP_DJANGO_USER} + OEP_DB_PW: ${OEP_DB_PW} + OEP_DJANGO_HOST: ${OEP_DJANGO_HOST} + OEP_DJANGO_NAME: ${OEP_DJANGO_NAME} + LOCAL_DB_USER: ${LOCAL_DB_USER} + LOCAL_DB_PASSWORD: ${LOCAL_DB_PASSWORD} + LOCAL_DB_NAME: ${LOCAL_DB_NAME} + LOCAL_DB_HOST: ${LOCAL_DB_HOST} + volumes: + - oeplatform_ontologies:/app/ontologies + - oeplatform_media:/app/media + depends_on: + - postgres + + ontop: + build: + context: ../docker + dockerfile: Dockerfile.ontop + container_name: ontop + ports: + - "${OEP_PORT_ONTOP:-8081}:8080" + environment: + ONTOP_MAPPING_FILE: "/opt/ontop-config/mapping.obda" + ONTOP_OWL_FILE: "/opt/ontop-config/ontology.owl" + ONTOP_PROPERTIES_FILE: "/opt/ontop-config/ontop.properties" + volumes: + - ../docker/serviceConfigs/ontop:/opt/ontop-config + depends_on: + - postgres + + lookup: + restart: unless-stopped + image: dbpedia/lookup:dev + container_name: loep_lookup + ports: + - "${OEP_PORT_LOOKUP:-3004}:8082" + volumes: + - ../docker/data/index/:/index + - ../docker/serviceConfigs/lookup/config.yaml:/resources/config.yml From 99c2f744d794a408afbde13c237968b7355ccb12 Mon Sep 17 00:00:00 2001 From: jh-RLI Date: Tue, 19 May 2026 20:21:35 +0200 Subject: [PATCH 02/25] docs(podman): add first-time setup and release deployment sections to README Co-Authored-By: Claude Sonnet 4.6 --- podman/README.md | 56 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/podman/README.md b/podman/README.md index 9360ab163..2f5d30b94 100644 --- a/podman/README.md +++ b/podman/README.md @@ -28,6 +28,33 @@ pip install podman-compose # apt install podman-compose (Debian/Ubuntu) ``` +## First-time Setup + +### 1. Create your environment file + +```sh +cp podman/.env.example .env +# edit .env and fill in all values — this file must never be committed +``` + +`podman-compose` automatically loads `.env` from the repository root when run +from there. The `.gitignore` already excludes `.env` files. + +### 2. Build the images + +```sh +podman-compose -f podman/podman-compose.yaml build +``` + +The build runs the Vite frontend build and the Django `collectstatic` / +`compress` steps inside the container — no local Node.js or Python needed. + +### 3. Start the stack + +See [Start the Stack](#start-the-stack) below. + +--- + ## Services | Service | Description | Default port | @@ -82,3 +109,32 @@ podman-compose -f podman/podman-compose.yaml up -d ``` The postgres container recreates all tables on a fresh volume automatically. + +## Deploy a New Release + +Checkout the release branch/tag, rebuild the image, and restart the stack. All +release steps run automatically — no manual server commands needed. + +```sh +git checkout master # or the release tag, e.g. git checkout v1.8.0 +git pull + +podman-compose -f podman/podman-compose.yaml build +podman-compose -f podman/podman-compose.yaml up -d +``` + +### What runs where + +The table below maps the manual server release steps to their Podman equivalent. + +| Manual step (ovgu-toep-w) | Podman equivalent | +| --------------------------------------------- | ------------------------------------------- | +| `git checkout master && git pull` | `git checkout && git pull` on host | +| `npm install --no-save` | `npm ci` in Dockerfile (image build) | +| `npm run build` | `npm run build` in Dockerfile (image build) | +| `pip install -r requirements.txt` | `pip install` in Dockerfile (image build) | +| `python manage.py collectstatic --noinput` | Dockerfile build step | +| `python manage.py compress` | `compress --force` in Dockerfile build step | +| `python manage.py migrate` | `entrypoint.sh` on container start | +| `python manage.py alembic upgrade head` | `entrypoint.sh` on container start | +| `touch wsgi.py` / `systemctl reload apache24` | `podman-compose up -d` restarts container | From 0eb9af402485e3fa09593503b9f225476d1f3219 Mon Sep 17 00:00:00 2001 From: jh-RLI Date: Wed, 20 May 2026 10:25:50 +0200 Subject: [PATCH 03/25] feat(podman): add quadlets for systemd-based deployment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Provides an alternative to podman-compose for servers running a systemd user session. Each service is a standalone Podman quadlet unit file that systemd manages directly — no compose binary required at runtime. Co-Authored-By: Claude Sonnet 4.6 --- podman/quadlets/README.md | 120 +++++++++++++++++++ podman/quadlets/fuseki-databases.volume | 9 ++ podman/quadlets/install.sh | 40 +++++++ podman/quadlets/oep-fuseki.container | 22 ++++ podman/quadlets/oep-lookup.container | 25 ++++ podman/quadlets/oep-oeplatform.container | 26 ++++ podman/quadlets/oep-ontop.container | 30 +++++ podman/quadlets/oep-postgres.container | 22 ++++ podman/quadlets/oep.env.example | 25 ++++ podman/quadlets/oep.network | 9 ++ podman/quadlets/oeplatform-media.volume | 9 ++ podman/quadlets/oeplatform-ontologies.volume | 9 ++ podman/quadlets/pgdata.volume | 9 ++ 13 files changed, 355 insertions(+) create mode 100644 podman/quadlets/README.md create mode 100644 podman/quadlets/fuseki-databases.volume create mode 100755 podman/quadlets/install.sh create mode 100644 podman/quadlets/oep-fuseki.container create mode 100644 podman/quadlets/oep-lookup.container create mode 100644 podman/quadlets/oep-oeplatform.container create mode 100644 podman/quadlets/oep-ontop.container create mode 100644 podman/quadlets/oep-postgres.container create mode 100644 podman/quadlets/oep.env.example create mode 100644 podman/quadlets/oep.network create mode 100644 podman/quadlets/oeplatform-media.volume create mode 100644 podman/quadlets/oeplatform-ontologies.volume create mode 100644 podman/quadlets/pgdata.volume diff --git a/podman/quadlets/README.md b/podman/quadlets/README.md new file mode 100644 index 000000000..2bdf7508d --- /dev/null +++ b/podman/quadlets/README.md @@ -0,0 +1,120 @@ + + +# Quadlets — Podman Systemd Integration + +> Alternative to `podman-compose`. Each service is a systemd unit managed +> directly by `systemctl`. Requires Podman ≥ 4.4 and a rootless Podman setup. + +Quadlets translate `.container`, `.volume`, and `.network` files into systemd +units. systemd then manages the full lifecycle: start on boot, restart on +failure, dependency ordering, and log access via `journalctl`. + +## Files + +| File | Type | Description | +| ------------------------------ | --------- | ------------------------------------------ | +| `oep.network` | network | Shared network for all services | +| `pgdata.volume` | volume | PostgreSQL data | +| `fuseki-databases.volume` | volume | Fuseki triple store data | +| `oeplatform-ontologies.volume` | volume | OEO ontologies (downloaded at first start) | +| `oeplatform-media.volume` | volume | Media files and OEO-extended | +| `oep-postgres.container` | container | PostgreSQL database | +| `oep-fuseki.container` | container | Apache Jena Fuseki | +| `oep-oeplatform.container` | container | OEP web app (Apache2) | +| `oep-ontop.container` | container | Ontop SPARQL endpoint | +| `oep-lookup.container` | container | DBpedia Lookup service | + +The `Dockerfile`, `apache2.conf`, and `entrypoint.sh` in the parent `podman/` +directory are shared with the podman-compose setup — both approaches build and +run the same image. + +## First-time Setup + +Run all commands from the **repository root**. + +### 1. Install units and create the environment file + +```sh +bash podman/quadlets/install.sh +``` + +This copies all unit files to `~/.config/containers/systemd/` and creates +`~/.config/oeplatform/oep.env` from the example template. + +### 2. Fill in credentials + +```sh +$EDITOR ~/.config/oeplatform/oep.env +``` + +### 3. Build the application images + +```sh +podman build -t localhost/oeplatform:latest -f podman/Dockerfile . +podman build -t localhost/oep-ontop:latest -f docker/Dockerfile.ontop docker/ +``` + +### 4. Enable and start all services + +```sh +systemctl --user enable --now \ + oep-postgres oep-fuseki oep-oeplatform oep-ontop oep-lookup +``` + +Services start in dependency order. `oep-oeplatform` and `oep-ontop` wait for +`oep-postgres` before starting. + +## Managing Services + +```sh +# Status +systemctl --user status oep-oeplatform + +# Logs +journalctl --user -u oep-oeplatform -f + +# Restart a single service +systemctl --user restart oep-oeplatform + +# Stop everything +systemctl --user stop oep-postgres oep-fuseki oep-oeplatform oep-ontop oep-lookup +``` + +## Deploy a New Release + +```sh +git checkout master && git pull + +# Rebuild the application image +podman build -t localhost/oeplatform:latest -f podman/Dockerfile . + +# Restart the app container — postgres and fuseki keep running +systemctl --user restart oep-oeplatform +``` + +## Repo Path for Ontop and Lookup + +The `oep-ontop.container` and `oep-lookup.container` files bind-mount config +files from the repository. They default to `/opt/oeplatform`. If your checkout +is elsewhere, update the `Volume=` lines in both files, or create a symlink: + +```sh +sudo ln -s /your/actual/repo/path /opt/oeplatform +``` + +## Uninstall + +```sh +systemctl --user disable --now \ + oep-postgres oep-fuseki oep-oeplatform oep-ontop oep-lookup + +rm ~/.config/containers/systemd/oep-*.container +rm ~/.config/containers/systemd/*.volume +rm ~/.config/containers/systemd/oep.network + +systemctl --user daemon-reload +``` diff --git a/podman/quadlets/fuseki-databases.volume b/podman/quadlets/fuseki-databases.volume new file mode 100644 index 000000000..a1b9dbb3e --- /dev/null +++ b/podman/quadlets/fuseki-databases.volume @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2025 Jonas Huber © Reiner Lemoine Institut +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +[Unit] +Description=OEPlatform Fuseki triple store data volume + +[Volume] +Label=app=oeplatform diff --git a/podman/quadlets/install.sh b/podman/quadlets/install.sh new file mode 100755 index 000000000..0cc2e86d1 --- /dev/null +++ b/podman/quadlets/install.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: 2025 Jonas Huber © Reiner Lemoine Institut +# +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# Installs OEPlatform Quadlet units for the current user and reloads systemd. +# Run from the repository root: +# bash podman/quadlets/install.sh + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +QUADLET_DIR="${HOME}/.config/containers/systemd" +ENV_DIR="${HOME}/.config/oeplatform" + +echo "Installing Quadlet units to ${QUADLET_DIR}…" +mkdir -p "${QUADLET_DIR}" +cp "${SCRIPT_DIR}"/*.container "${QUADLET_DIR}/" +cp "${SCRIPT_DIR}"/*.volume "${QUADLET_DIR}/" +cp "${SCRIPT_DIR}"/*.network "${QUADLET_DIR}/" + +if [ ! -f "${ENV_DIR}/oep.env" ]; then + echo "Creating ${ENV_DIR}/oep.env from example…" + mkdir -p "${ENV_DIR}" + cp "${SCRIPT_DIR}/oep.env.example" "${ENV_DIR}/oep.env" + echo "" + echo " !! Edit ${ENV_DIR}/oep.env and fill in all values before starting services." +fi + +echo "Reloading systemd user daemon…" +systemctl --user daemon-reload + +echo "" +echo "Done. Next steps:" +echo " 1. Edit ${ENV_DIR}/oep.env with real credentials (if not done yet)." +echo " 2. Build the application image:" +echo " podman build -t localhost/oeplatform:latest -f podman/Dockerfile ." +echo " podman build -t localhost/oep-ontop:latest -f docker/Dockerfile.ontop docker/" +echo " 3. Enable and start all services:" +echo " systemctl --user enable --now oep-postgres oep-fuseki oep-oeplatform oep-ontop oep-lookup" diff --git a/podman/quadlets/oep-fuseki.container b/podman/quadlets/oep-fuseki.container new file mode 100644 index 000000000..395127502 --- /dev/null +++ b/podman/quadlets/oep-fuseki.container @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: 2025 Jonas Huber © Reiner Lemoine Institut +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +[Unit] +Description=OEPlatform Apache Jena Fuseki triple store +After=network-online.target + +[Container] +Image=stain/jena-fuseki:5.1.0 +ContainerName=fuseki +EnvironmentFile=%h/.config/oeplatform/oep.env +Volume=fuseki-databases.volume:/home/fuseki/databases +PublishPort=3030:3030 +Network=oep.network + +[Service] +Restart=always +TimeoutStartSec=300 + +[Install] +WantedBy=default.target diff --git a/podman/quadlets/oep-lookup.container b/podman/quadlets/oep-lookup.container new file mode 100644 index 000000000..4b9117c5c --- /dev/null +++ b/podman/quadlets/oep-lookup.container @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2025 Jonas Huber © Reiner Lemoine Institut +# +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# NOTE: The Volume= paths below must point to your actual repo checkout. +# Update /opt/oeplatform to match your deployment path, or symlink it: +# sudo ln -s /your/repo/path /opt/oeplatform + +[Unit] +Description=OEPlatform DBpedia Lookup service +After=network-online.target + +[Container] +Image=dbpedia/lookup:dev +ContainerName=lookup +Volume=/opt/oeplatform/docker/data/index:/index:ro +Volume=/opt/oeplatform/docker/serviceConfigs/lookup/config.yaml:/resources/config.yml:ro +PublishPort=3004:8082 +Network=oep.network + +[Service] +Restart=always + +[Install] +WantedBy=default.target diff --git a/podman/quadlets/oep-oeplatform.container b/podman/quadlets/oep-oeplatform.container new file mode 100644 index 000000000..53c432da5 --- /dev/null +++ b/podman/quadlets/oep-oeplatform.container @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: 2025 Jonas Huber © Reiner Lemoine Institut +# +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# Build the image before enabling this unit: +# podman build -t localhost/oeplatform:latest -f podman/Dockerfile . + +[Unit] +Description=OEPlatform web application +After=oep-postgres.service oep-fuseki.service + +[Container] +Image=localhost/oeplatform:latest +ContainerName=oeplatform +EnvironmentFile=%h/.config/oeplatform/oep.env +Volume=oeplatform-ontologies.volume:/app/ontologies +Volume=oeplatform-media.volume:/app/media +PublishPort=8080:80 +Network=oep.network + +[Service] +Restart=on-failure +TimeoutStartSec=120 + +[Install] +WantedBy=default.target diff --git a/podman/quadlets/oep-ontop.container b/podman/quadlets/oep-ontop.container new file mode 100644 index 000000000..3477e7746 --- /dev/null +++ b/podman/quadlets/oep-ontop.container @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: 2025 Jonas Huber © Reiner Lemoine Institut +# +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# Build the image before enabling this unit: +# podman build -t localhost/oep-ontop:latest -f docker/Dockerfile.ontop docker/ +# +# NOTE: The Volume= path below must point to your actual repo checkout. +# Update /opt/oeplatform to match your deployment path, or symlink it: +# sudo ln -s /your/repo/path /opt/oeplatform + +[Unit] +Description=OEPlatform Ontop SPARQL endpoint +After=oep-postgres.service + +[Container] +Image=localhost/oep-ontop:latest +ContainerName=ontop +Volume=/opt/oeplatform/docker/serviceConfigs/ontop:/opt/ontop-config:ro +Environment=ONTOP_MAPPING_FILE=/opt/ontop-config/mapping.obda +Environment=ONTOP_OWL_FILE=/opt/ontop-config/ontology.owl +Environment=ONTOP_PROPERTIES_FILE=/opt/ontop-config/ontop.properties +PublishPort=8081:8080 +Network=oep.network + +[Service] +Restart=on-failure + +[Install] +WantedBy=default.target diff --git a/podman/quadlets/oep-postgres.container b/podman/quadlets/oep-postgres.container new file mode 100644 index 000000000..387fd5806 --- /dev/null +++ b/podman/quadlets/oep-postgres.container @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: 2025 Jonas Huber © Reiner Lemoine Institut +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +[Unit] +Description=OEPlatform PostgreSQL database +After=network-online.target + +[Container] +Image=ghcr.io/openenergyplatform/oeplatform-postgres:latest +ContainerName=postgres +EnvironmentFile=%h/.config/oeplatform/oep.env +Volume=pgdata.volume:/var/lib/postgresql/data +PublishPort=5432:5432 +Network=oep.network + +[Service] +Restart=on-failure +TimeoutStartSec=300 + +[Install] +WantedBy=default.target diff --git a/podman/quadlets/oep.env.example b/podman/quadlets/oep.env.example new file mode 100644 index 000000000..346774f2b --- /dev/null +++ b/podman/quadlets/oep.env.example @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2025 Jonas Huber © Reiner Lemoine Institut +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +# Copy this file to ~/.config/oeplatform/oep.env on the server and fill in +# all values. This file is read by each container unit via EnvironmentFile=. +# It must never be committed to version control. + +# ── PostgreSQL ──────────────────────────────────────────────────────────────── +POSTGRES_USER= +POSTGRES_PASSWORD= + +# ── OEPlatform app ──────────────────────────────────────────────────────────── +OEP_DJANGO_USER= +OEP_DB_PW= +OEP_DJANGO_HOST=postgres +OEP_DJANGO_NAME=oep_django +LOCAL_DB_USER= +LOCAL_DB_PASSWORD= +LOCAL_DB_NAME=oedb +LOCAL_DB_HOST=postgres + +# ── Fuseki ──────────────────────────────────────────────────────────────────── +FUSEKI_ADMIN_PASSWORD= +FUSEKI_DATASET_1=ds diff --git a/podman/quadlets/oep.network b/podman/quadlets/oep.network new file mode 100644 index 000000000..2a2026008 --- /dev/null +++ b/podman/quadlets/oep.network @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2025 Jonas Huber © Reiner Lemoine Institut +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +[Unit] +Description=OEPlatform shared container network + +[Network] +Label=app=oeplatform diff --git a/podman/quadlets/oeplatform-media.volume b/podman/quadlets/oeplatform-media.volume new file mode 100644 index 000000000..ee4ad3575 --- /dev/null +++ b/podman/quadlets/oeplatform-media.volume @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2025 Jonas Huber © Reiner Lemoine Institut +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +[Unit] +Description=OEPlatform media files volume + +[Volume] +Label=app=oeplatform diff --git a/podman/quadlets/oeplatform-ontologies.volume b/podman/quadlets/oeplatform-ontologies.volume new file mode 100644 index 000000000..27cb138fc --- /dev/null +++ b/podman/quadlets/oeplatform-ontologies.volume @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2025 Jonas Huber © Reiner Lemoine Institut +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +[Unit] +Description=OEPlatform OEO ontologies volume + +[Volume] +Label=app=oeplatform diff --git a/podman/quadlets/pgdata.volume b/podman/quadlets/pgdata.volume new file mode 100644 index 000000000..442192b85 --- /dev/null +++ b/podman/quadlets/pgdata.volume @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2025 Jonas Huber © Reiner Lemoine Institut +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +[Unit] +Description=OEPlatform PostgreSQL data volume + +[Volume] +Label=app=oeplatform From 03ab8bc9a05d11ec85c933991ebe2afec60fa981 Mon Sep 17 00:00:00 2001 From: jh-RLI Date: Wed, 20 May 2026 13:05:25 +0200 Subject: [PATCH 04/25] feat(podman): add production ontop service config and fix bind mount paths #2319 - Add podman/serviceConfigs/ontop/ with empty mapping.obda skeleton, ontop.properties.template, .gitignore (excludes ontology.owl, postgresql.jar, ontop.properties), and setup README - Update podman-compose.yaml and oep-ontop.container to mount from podman/serviceConfigs/ontop/ instead of docker/serviceConfigs/ontop/ Co-Authored-By: Claude Sonnet 4.6 --- podman/podman-compose.yaml | 2 +- podman/quadlets/oep-ontop.container | 4 +- podman/serviceConfigs/ontop/.gitignore | 4 + podman/serviceConfigs/ontop/README.md | 86 +++++++++++++++++++ podman/serviceConfigs/ontop/mapping.obda | 16 ++++ .../ontop/ontop.properties.template | 16 ++++ 6 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 podman/serviceConfigs/ontop/.gitignore create mode 100644 podman/serviceConfigs/ontop/README.md create mode 100644 podman/serviceConfigs/ontop/mapping.obda create mode 100644 podman/serviceConfigs/ontop/ontop.properties.template diff --git a/podman/podman-compose.yaml b/podman/podman-compose.yaml index 83f70ce23..78111298f 100644 --- a/podman/podman-compose.yaml +++ b/podman/podman-compose.yaml @@ -71,7 +71,7 @@ services: ONTOP_OWL_FILE: "/opt/ontop-config/ontology.owl" ONTOP_PROPERTIES_FILE: "/opt/ontop-config/ontop.properties" volumes: - - ../docker/serviceConfigs/ontop:/opt/ontop-config + - ./serviceConfigs/ontop:/opt/ontop-config depends_on: - postgres diff --git a/podman/quadlets/oep-ontop.container b/podman/quadlets/oep-ontop.container index 3477e7746..eeb9d87ab 100644 --- a/podman/quadlets/oep-ontop.container +++ b/podman/quadlets/oep-ontop.container @@ -7,7 +7,7 @@ # # NOTE: The Volume= path below must point to your actual repo checkout. # Update /opt/oeplatform to match your deployment path, or symlink it: -# sudo ln -s /your/repo/path /opt/oeplatform +# sudo ln -s /your/actual/repo/path /opt/oeplatform [Unit] Description=OEPlatform Ontop SPARQL endpoint @@ -16,7 +16,7 @@ After=oep-postgres.service [Container] Image=localhost/oep-ontop:latest ContainerName=ontop -Volume=/opt/oeplatform/docker/serviceConfigs/ontop:/opt/ontop-config:ro +Volume=/opt/oeplatform/podman/serviceConfigs/ontop:/opt/ontop-config:ro Environment=ONTOP_MAPPING_FILE=/opt/ontop-config/mapping.obda Environment=ONTOP_OWL_FILE=/opt/ontop-config/ontology.owl Environment=ONTOP_PROPERTIES_FILE=/opt/ontop-config/ontop.properties diff --git a/podman/serviceConfigs/ontop/.gitignore b/podman/serviceConfigs/ontop/.gitignore new file mode 100644 index 000000000..5c2290083 --- /dev/null +++ b/podman/serviceConfigs/ontop/.gitignore @@ -0,0 +1,4 @@ +# These files must be provided manually — never committed to version control. +ontology.owl +postgresql.jar +ontop.properties diff --git a/podman/serviceConfigs/ontop/README.md b/podman/serviceConfigs/ontop/README.md new file mode 100644 index 000000000..dba093cd7 --- /dev/null +++ b/podman/serviceConfigs/ontop/README.md @@ -0,0 +1,86 @@ + + +# Ontop Service Configuration + +This directory contains the production configuration for the Ontop SPARQL +endpoint. Two files must be provided manually before building or starting the +ontop service — they are gitignored and must never be committed. + +## Required files (not in git) + +### 1. `postgresql.jar` — JDBC driver + +Download the PostgreSQL JDBC driver from and +place it here as `postgresql.jar`. + +This file is copied into the ontop image at build time: + +```sh +# Build from the repository root after placing the jar here +podman build -t localhost/oep-ontop:latest -f docker/Dockerfile.ontop docker/ +``` + +> The `Dockerfile.ontop` copies the jar from `docker/serviceConfigs/ontop/`, not +> from this directory. Place a copy (or symlink) there too before building. + +### 2. `ontology.owl` — Open Energy Ontology + +Download the latest OEO build artefacts from the +[OEO GitHub releases](https://github.com/OpenEnergyPlatform/ontology/releases/latest) +and place the `ontology.owl` file here. + +```sh +wget -O /tmp/oeo.zip \ + https://github.com/OpenEnergyPlatform/ontology/releases/latest/download/build-files.zip +unzip -j /tmp/oeo.zip "*/ontology.owl" -d podman/serviceConfigs/ontop/ +rm /tmp/oeo.zip +``` + +## Files in git + +| File | Description | +| --------------------------- | ------------------------------------------------------- | +| `mapping.obda` | Empty OBDA mapping skeleton — extend for your tables | +| `ontop.properties.template` | JDBC connection template — copy and fill in credentials | + +### `ontop.properties` credentials + +`ontop.properties` is gitignored. Create it from the template and fill in the +real credentials from your `.env` / `oep.env` file: + +```sh +cp podman/serviceConfigs/ontop/ontop.properties.template \ + podman/serviceConfigs/ontop/ontop.properties +# then edit ontop.properties — it must never be committed +``` + +```properties +jdbc.user=REPLACE_WITH_POSTGRES_USER # → value of POSTGRES_USER +jdbc.password=REPLACE_WITH_POSTGRES_PASSWORD # → value of POSTGRES_PASSWORD +``` + +Ontop does not support environment variable substitution in this file, so +credentials must be written in plain text. The `.gitignore` in this directory +prevents accidental commits. + +### `mapping.obda` — extending the mapping + +The skeleton file contains only prefix declarations and an empty mapping +collection. Add OBDA mappings as needed: + +```obda +[MappingDeclaration] @collection [[ + +mappingId my_table_TargetClass +target oekg:data-descriptor/my_table/{id} a oeo:IAO_0000027 . +source SELECT "id" FROM "data"."my_table" + +]] +``` + +The source table must exist in the `oedb` database before Ontop can start +successfully with a mapping that references it. diff --git a/podman/serviceConfigs/ontop/mapping.obda b/podman/serviceConfigs/ontop/mapping.obda new file mode 100644 index 000000000..fdc461198 --- /dev/null +++ b/podman/serviceConfigs/ontop/mapping.obda @@ -0,0 +1,16 @@ +[PrefixDeclaration] +: http://example.org/voc# +owl: http://www.w3.org/2002/07/owl# +rdf: http://www.w3.org/1999/02/22-rdf-syntax-ns# +xml: http://www.w3.org/XML/1998/namespace +xsd: http://www.w3.org/2001/XMLSchema# +foaf: http://xmlns.com/foaf/0.1/ +obda: https://w3id.org/obda/vocabulary# +rdfs: http://www.w3.org/2000/01/rdf-schema# +oeo: https://openenergyplatform.org/ontology/oeo/ +oekg: https://openenergyplatform.org/ontology/oeo/oekg/ +llc: https://www.omg.org/spec/LCC/Countries/ISO3166-1-CountryCodes/ + +[MappingDeclaration] @collection [[ + +]] diff --git a/podman/serviceConfigs/ontop/ontop.properties.template b/podman/serviceConfigs/ontop/ontop.properties.template new file mode 100644 index 000000000..3e0ba7dab --- /dev/null +++ b/podman/serviceConfigs/ontop/ontop.properties.template @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: 2025 Jonas Huber © Reiner Lemoine Institut +# +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# Production ontop JDBC connection settings. +# The hostname "postgres" resolves to the postgres container on the shared +# Podman network (ContainerName=postgres in the quadlet / service name in compose). +# +# jdbc.user and jdbc.password must match POSTGRES_USER / POSTGRES_PASSWORD +# set in your .env / oep.env file. Ontop does not support environment variable +# substitution in this file, so fill in the real values directly. + +jdbc.url=jdbc:postgresql://postgres:5432/oedb +jdbc.user=REPLACE_WITH_POSTGRES_USER +jdbc.password=REPLACE_WITH_POSTGRES_PASSWORD +jdbc.driver=org.postgresql.Driver From daac738895f858326af0d66ac0efe938d0222509 Mon Sep 17 00:00:00 2001 From: jh-RLI Date: Wed, 20 May 2026 14:13:20 +0200 Subject: [PATCH 05/25] feat(podman): add production image build workflow and use registry images #2319 - Add .github/workflows/build-production-image.yaml to build and push ghcr.io/openenergyplatform/oeplatform-production (app + Vite build) and ghcr.io/openenergyplatform/oeplatform-ontop (Ontop + JDBC driver) on v* tags - Update podman-compose.yaml and quadlet container files to pull from registry instead of building locally on the server - Update quadlets/install.sh to reflect pull-based deployment Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/build-production-image.yaml | 110 ++++++++++++++++++ podman/podman-compose.yaml | 8 +- podman/quadlets/install.sh | 6 +- podman/quadlets/oep-oeplatform.container | 5 +- podman/quadlets/oep-ontop.container | 5 +- 5 files changed, 117 insertions(+), 17 deletions(-) create mode 100644 .github/workflows/build-production-image.yaml diff --git a/.github/workflows/build-production-image.yaml b/.github/workflows/build-production-image.yaml new file mode 100644 index 000000000..677d1e4e7 --- /dev/null +++ b/.github/workflows/build-production-image.yaml @@ -0,0 +1,110 @@ +# SPDX-FileCopyrightText: 2025 Jonas Huber © Reiner Lemoine Institut +# +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# Builds and pushes production container images to ghcr.io on every v* tag. +# +# Images produced: +# ghcr.io/openenergyplatform/oeplatform-production: (app + Vite build) +# ghcr.io/openenergyplatform/oeplatform-ontop: (Ontop + JDBC driver) +# +# The existing image-build.yaml continues to build the CI/testing image +# (ghcr.io/openenergyplatform/oeplatform) from docker/Dockerfile unchanged. + +name: Build and publish production images + +on: + push: + tags: + - "v*" + workflow_dispatch: + +env: + REGISTRY: ghcr.io + ORG: openenergyplatform + # PostgreSQL JDBC driver version baked into the ontop image + JDBC_VERSION: "42.7.3" + +jobs: + build-app: + name: OEPlatform app image + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Log in to container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.ORG }}/oeplatform-production + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + file: ./podman/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + build-ontop: + name: Ontop image + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Download PostgreSQL JDBC driver + run: | + curl -fsSL \ + "https://repo1.maven.org/maven2/org/postgresql/postgresql/${{ env.JDBC_VERSION }}/postgresql-${{ env.JDBC_VERSION }}.jar" \ + -o docker/serviceConfigs/ontop/postgresql.jar + + - name: Log in to container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.ORG }}/oeplatform-ontop + tags: | + type=semver,pattern={{version}} + type=raw,value=latest + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: ./docker + file: ./docker/Dockerfile.ontop + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/podman/podman-compose.yaml b/podman/podman-compose.yaml index 78111298f..caf6cea4b 100644 --- a/podman/podman-compose.yaml +++ b/podman/podman-compose.yaml @@ -38,9 +38,7 @@ services: - "${OEP_PORT_FUSEKI:-3030}:3030" oeplatform: - build: - context: .. - dockerfile: podman/Dockerfile + image: ghcr.io/openenergyplatform/oeplatform-production:latest container_name: oeplatform ports: - "${OEP_PORT_WEB:-8080}:80" @@ -60,9 +58,7 @@ services: - postgres ontop: - build: - context: ../docker - dockerfile: Dockerfile.ontop + image: ghcr.io/openenergyplatform/oeplatform-ontop:latest container_name: ontop ports: - "${OEP_PORT_ONTOP:-8081}:8080" diff --git a/podman/quadlets/install.sh b/podman/quadlets/install.sh index 0cc2e86d1..14305380c 100755 --- a/podman/quadlets/install.sh +++ b/podman/quadlets/install.sh @@ -33,8 +33,8 @@ systemctl --user daemon-reload echo "" echo "Done. Next steps:" echo " 1. Edit ${ENV_DIR}/oep.env with real credentials (if not done yet)." -echo " 2. Build the application image:" -echo " podman build -t localhost/oeplatform:latest -f podman/Dockerfile ." -echo " podman build -t localhost/oep-ontop:latest -f docker/Dockerfile.ontop docker/" +echo " 2. Pull the production images:" +echo " podman pull ghcr.io/openenergyplatform/oeplatform-production:latest" +echo " podman pull ghcr.io/openenergyplatform/oeplatform-ontop:latest" echo " 3. Enable and start all services:" echo " systemctl --user enable --now oep-postgres oep-fuseki oep-oeplatform oep-ontop oep-lookup" diff --git a/podman/quadlets/oep-oeplatform.container b/podman/quadlets/oep-oeplatform.container index 53c432da5..28a3d875e 100644 --- a/podman/quadlets/oep-oeplatform.container +++ b/podman/quadlets/oep-oeplatform.container @@ -2,15 +2,12 @@ # # SPDX-License-Identifier: AGPL-3.0-or-later # -# Build the image before enabling this unit: -# podman build -t localhost/oeplatform:latest -f podman/Dockerfile . - [Unit] Description=OEPlatform web application After=oep-postgres.service oep-fuseki.service [Container] -Image=localhost/oeplatform:latest +Image=ghcr.io/openenergyplatform/oeplatform-production:latest ContainerName=oeplatform EnvironmentFile=%h/.config/oeplatform/oep.env Volume=oeplatform-ontologies.volume:/app/ontologies diff --git a/podman/quadlets/oep-ontop.container b/podman/quadlets/oep-ontop.container index eeb9d87ab..85cf617cf 100644 --- a/podman/quadlets/oep-ontop.container +++ b/podman/quadlets/oep-ontop.container @@ -2,9 +2,6 @@ # # SPDX-License-Identifier: AGPL-3.0-or-later # -# Build the image before enabling this unit: -# podman build -t localhost/oep-ontop:latest -f docker/Dockerfile.ontop docker/ -# # NOTE: The Volume= path below must point to your actual repo checkout. # Update /opt/oeplatform to match your deployment path, or symlink it: # sudo ln -s /your/actual/repo/path /opt/oeplatform @@ -14,7 +11,7 @@ Description=OEPlatform Ontop SPARQL endpoint After=oep-postgres.service [Container] -Image=localhost/oep-ontop:latest +Image=ghcr.io/openenergyplatform/oeplatform-ontop:latest ContainerName=ontop Volume=/opt/oeplatform/podman/serviceConfigs/ontop:/opt/ontop-config:ro Environment=ONTOP_MAPPING_FILE=/opt/ontop-config/mapping.obda From 38925fabc7b1d4a22a4ce5e2d6c3e7f7a66e2291 Mon Sep 17 00:00:00 2001 From: jh-RLI Date: Wed, 20 May 2026 17:35:18 +0200 Subject: [PATCH 06/25] fix(podman): resolve inter-container DNS and network configuration #2319 - Add explicit container_name to postgres service so dnsname CNI plugin registers it as "postgres" (matches OEP_DJANGO_HOST / LOCAL_DB_HOST) - Declare external oep network; set default_network=oep in containers.conf to work around podman-compose 1.5.0 not passing --network to podman run - Add networks section with oep to all services (explicit long-form) - Add lookup_index named volume; move lookup config to podman/serviceConfigs/ to keep podman and docker setups fully separate - Prefix all short image names with docker.io/ (fixes short-name resolution) - Fix docker/Dockerfile.ontop: qualify FROM ontop/ontop as docker.io/ontop/ontop Co-Authored-By: Claude Sonnet 4.6 --- docker/Dockerfile.ontop | 2 +- podman/podman-compose.yaml | 24 ++++++++++++--- podman/quadlets/oep-fuseki.container | 2 +- podman/quadlets/oep-lookup.container | 2 +- podman/serviceConfigs/lookup/config.yaml | 37 ++++++++++++++++++++++++ 5 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 podman/serviceConfigs/lookup/config.yaml diff --git a/docker/Dockerfile.ontop b/docker/Dockerfile.ontop index 5f6774bad..3e90170b0 100644 --- a/docker/Dockerfile.ontop +++ b/docker/Dockerfile.ontop @@ -1,5 +1,5 @@ # Use the official Ontop image as the base -FROM ontop/ontop:latest +FROM docker.io/ontop/ontop:latest # Copy the PostgreSQL JDBC driver into Ontop's lib directory COPY serviceConfigs/ontop/postgresql.jar /opt/ontop/lib/postgresql.jar diff --git a/podman/podman-compose.yaml b/podman/podman-compose.yaml index caf6cea4b..f89ebed7d 100644 --- a/podman/podman-compose.yaml +++ b/podman/podman-compose.yaml @@ -6,15 +6,21 @@ # Run from the repository root: # podman-compose -f podman/podman-compose.yaml up -d +networks: + oep: + external: true + volumes: pgdata: fuseki_databases: oeplatform_ontologies: oeplatform_media: + lookup_index: services: postgres: image: ghcr.io/openenergyplatform/oeplatform-postgres:latest + container_name: postgres environment: POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} @@ -22,11 +28,13 @@ services: - pgdata:/var/lib/postgresql/data ports: - "${OEP_PORT_POSTGRES:-5432}:5432" + networks: + - oep # TODO: load oeo_ext.owl into Fuseki as a named graph so it is queryable via # SPARQL alongside the base OEO. See GitHub issue #. fuseki: - image: stain/jena-fuseki:5.1.0 + image: docker.io/stain/jena-fuseki:5.1.0 container_name: fuseki environment: ADMIN_PASSWORD: ${FUSEKI_ADMIN_PASSWORD} @@ -36,6 +44,8 @@ services: restart: unless-stopped ports: - "${OEP_PORT_FUSEKI:-3030}:3030" + networks: + - oep oeplatform: image: ghcr.io/openenergyplatform/oeplatform-production:latest @@ -56,6 +66,8 @@ services: - oeplatform_media:/app/media depends_on: - postgres + networks: + - oep ontop: image: ghcr.io/openenergyplatform/oeplatform-ontop:latest @@ -70,13 +82,17 @@ services: - ./serviceConfigs/ontop:/opt/ontop-config depends_on: - postgres + networks: + - oep lookup: restart: unless-stopped - image: dbpedia/lookup:dev + image: docker.io/dbpedia/lookup:dev container_name: loep_lookup ports: - "${OEP_PORT_LOOKUP:-3004}:8082" volumes: - - ../docker/data/index/:/index - - ../docker/serviceConfigs/lookup/config.yaml:/resources/config.yml + - lookup_index:/index + - ./serviceConfigs/lookup/config.yaml:/resources/config.yml + networks: + - oep diff --git a/podman/quadlets/oep-fuseki.container b/podman/quadlets/oep-fuseki.container index 395127502..0b0e071c3 100644 --- a/podman/quadlets/oep-fuseki.container +++ b/podman/quadlets/oep-fuseki.container @@ -7,7 +7,7 @@ Description=OEPlatform Apache Jena Fuseki triple store After=network-online.target [Container] -Image=stain/jena-fuseki:5.1.0 +Image=docker.io/stain/jena-fuseki:5.1.0 ContainerName=fuseki EnvironmentFile=%h/.config/oeplatform/oep.env Volume=fuseki-databases.volume:/home/fuseki/databases diff --git a/podman/quadlets/oep-lookup.container b/podman/quadlets/oep-lookup.container index 4b9117c5c..73e7a9792 100644 --- a/podman/quadlets/oep-lookup.container +++ b/podman/quadlets/oep-lookup.container @@ -11,7 +11,7 @@ Description=OEPlatform DBpedia Lookup service After=network-online.target [Container] -Image=dbpedia/lookup:dev +Image=docker.io/dbpedia/lookup:dev ContainerName=lookup Volume=/opt/oeplatform/docker/data/index:/index:ro Volume=/opt/oeplatform/docker/serviceConfigs/lookup/config.yaml:/resources/config.yml:ro diff --git a/podman/serviceConfigs/lookup/config.yaml b/podman/serviceConfigs/lookup/config.yaml new file mode 100644 index 000000000..6432227c5 --- /dev/null +++ b/podman/serviceConfigs/lookup/config.yaml @@ -0,0 +1,37 @@ +version: "1.0" +indexPath: ./index +maxBufferedDocs: 1000000 +logInterval: 10000 +exactMatchBoost: 6 +prefixMatchBoost: 5 +fuzzyMatchBoost: 2 +fuzzyEditDistance: 2 +fuzzyPrefixLength: 2 +boostFormula: 1 +maxResults: 1000 +format: JSON +minScore: 0.1 +lookupFields: + - name: id + weight: 10 + exact: true + tokenize: false + required: true + highlight: false + queryByDefault: false + - name: label + weight: 10 + highlight: true + tokenize: true + queryByDefault: true + allowPartialMatch: true + required: false + exact: false + - name: definition + weight: 5 + highlight: true + tokenize: true + queryByDefault: true + allowPartialMatch: true + required: false + exact: false From 8395f315d9f6ff437c4ca9d156e82c0150af9920 Mon Sep 17 00:00:00 2001 From: jh-RLI Date: Wed, 20 May 2026 17:38:55 +0200 Subject: [PATCH 07/25] docs(podman): document Ubuntu 22.04 CNI fixes and Quadlets comparison #2319 - Correct Podman version requirement from 4.0 to 3.4+ - Document CNI plugin version mismatch fix (containernetworking-plugins 0.9.1 vs cniVersion 1.0.0): download v1.9.1 binaries to ~/.config/cni/plugins - Document podman-compose 1.5.0 network assignment bug and workaround: pre-create oep network + set default_network in containers.conf - Update all commands to use --env-file podman/.env - Add Quadlets section explaining why it avoids the network workaround - Update Deploy section: pull pre-built images instead of building locally Co-Authored-By: Claude Sonnet 4.6 --- podman/README.md | 116 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 90 insertions(+), 26 deletions(-) diff --git a/podman/README.md b/podman/README.md index 2f5d30b94..4569fb12d 100644 --- a/podman/README.md +++ b/podman/README.md @@ -14,42 +14,78 @@ build time — no bind mounts are used. ## Prerequisites -- [Podman](https://podman.io/getting-started/installation) ≥ 4.0 +- [Podman](https://podman.io/getting-started/installation) ≥ 3.4 - [podman-compose](https://github.com/containers/podman-compose) ≥ 1.0 - Rootless Podman configured (`/etc/subuid` and `/etc/subgid` entries for your user) +- `dnsmasq` installed (required by the CNI dnsname plugin for inter-container + DNS resolution) -Install podman-compose (if not already installed): +Install podman-compose via pip (the apt package on Ubuntu 22.04 is too old): ```sh pip install podman-compose -# or via your distro's package manager, e.g.: -# dnf install podman-compose (Fedora/RHEL) -# apt install podman-compose (Debian/Ubuntu) ``` -## First-time Setup +## Platform Notes -### 1. Create your environment file +### Ubuntu 22.04 — CNI plugin version mismatch + +Ubuntu 22.04 ships `containernetworking-plugins 0.9.1`, which only supports CNI +spec `0.4.0`. Podman 3.x creates new networks with `cniVersion: 1.0.0`, causing +the `firewall` CNI plugin to reject the config and silently break +inter-container networking. Fix it by installing updated plugin binaries into a +user directory and pointing Podman at them: ```sh -cp podman/.env.example .env -# edit .env and fill in all values — this file must never be committed +# Download CNI plugins v1.9.1 (or later) +curl -LO https://github.com/containernetworking/plugins/releases/download/v1.9.1/cni-plugins-linux-amd64-v1.9.1.tgz +mkdir -p ~/.config/cni/plugins +tar -xzf cni-plugins-linux-amd64-v1.9.1.tgz -C ~/.config/cni/plugins +rm cni-plugins-linux-amd64-v1.9.1.tgz + +# Tell Podman to search the user directory first +mkdir -p ~/.config/containers +cat >> ~/.config/containers/containers.conf << 'EOF' +[network] +cni_plugin_dirs = ["/home//.config/cni/plugins", "/usr/lib/cni", "/opt/cni/bin"] +EOF ``` -`podman-compose` automatically loads `.env` from the repository root when run -from there. The `.gitignore` already excludes `.env` files. +Replace `` with your actual username or use `$HOME`. -### 2. Build the images +### Ubuntu 22.04 — podman-compose does not pass `--network` to podman run + +`podman-compose` 1.5.0 with Podman 3.4.x has a bug where the `networks:` service +assignment is ignored and all containers land on the default `podman` network, +which has no DNS. The workaround is to pre-create the `oep` network and make it +the default: ```sh -podman-compose -f podman/podman-compose.yaml build +podman network create oep + +cat >> ~/.config/containers/containers.conf << 'EOF' +default_network = "oep" +EOF ``` -The build runs the Vite frontend build and the Django `collectstatic` / -`compress` steps inside the container — no local Node.js or Python needed. +This makes the `oep` network — which has the dnsname plugin — the network all +containers use unless explicitly overridden. + +> **Note:** This issue does not affect Podman 4.x (Netavark backend, native DNS) +> or the Quadlets deployment path (see below), which attach containers to the +> network via explicit `Network=oep.network` directives in the unit files. -### 3. Start the stack +## First-time Setup + +### 1. Create your environment file + +```sh +cp podman/.env.example podman/.env +# edit podman/.env and fill in all values — this file must never be committed +``` + +### 2. Start the stack See [Start the Stack](#start-the-stack) below. @@ -70,13 +106,13 @@ See [Start the Stack](#start-the-stack) below. Run all commands from the **repository root**. ```sh -podman-compose -f podman/podman-compose.yaml up -d +podman-compose --env-file podman/.env -f podman/podman-compose.yaml up -d ``` ## Stop the Stack ```sh -podman-compose -f podman/podman-compose.yaml down +podman-compose --env-file podman/.env -f podman/podman-compose.yaml down ``` ## Override Ports @@ -91,7 +127,9 @@ export OEP_PORT_POSTGRES=5433 ## View Logs ```sh -podman-compose -f podman/podman-compose.yaml logs -f oeplatform +podman-compose --env-file podman/.env -f podman/podman-compose.yaml logs -f oeplatform +# or directly: +podman logs -f oeplatform ``` ## Open a Shell @@ -103,24 +141,50 @@ podman exec -it oeplatform bash ## Reset Database ```sh -podman-compose -f podman/podman-compose.yaml down +podman-compose --env-file podman/.env -f podman/podman-compose.yaml down podman volume rm podman_pgdata # check exact name with: podman volume ls -podman-compose -f podman/podman-compose.yaml up -d +podman-compose --env-file podman/.env -f podman/podman-compose.yaml up -d ``` The postgres container recreates all tables on a fresh volume automatically. ## Deploy a New Release -Checkout the release branch/tag, rebuild the image, and restart the stack. All -release steps run automatically — no manual server commands needed. +Pull the latest production images and restart — all release steps run inside the +container automatically (migrations, static files, etc.). ```sh -git checkout master # or the release tag, e.g. git checkout v1.8.0 git pull +podman pull ghcr.io/openenergyplatform/oeplatform-production:latest +podman pull ghcr.io/openenergyplatform/oeplatform-ontop:latest +podman-compose --env-file podman/.env -f podman/podman-compose.yaml up -d +``` + +## Quadlets (systemd) Alternative + +The `quadlets/` directory contains systemd Quadlet unit files as an alternative +to podman-compose. Quadlets are better suited for long-running production +servers because systemd manages restarts, dependencies, and logging. + +**Why Quadlets are simpler on Podman 3.x:** Each `.container` file declares +`Network=oep.network` explicitly. Systemd creates the network via the +`oep.network` unit and attaches every container before it starts. This bypasses +the podman-compose network assignment bug entirely — no `default_network` +workaround needed. + +You still need the CNI plugin fix from the +[Ubuntu 22.04 section](#ubuntu-2204--cni-plugin-version-mismatch) if running on +Ubuntu 22.04. -podman-compose -f podman/podman-compose.yaml build -podman-compose -f podman/podman-compose.yaml up -d +```sh +bash podman/quadlets/install.sh +systemctl --user enable --now oep-postgres oep-fuseki oep-oeplatform oep-ontop oep-lookup +``` + +View logs via journald: + +```sh +journalctl --user -u oep-oeplatform -f ``` ### What runs where From c5a90e76c9bfd043dd8490dcdfe030ad4dfc68e2 Mon Sep 17 00:00:00 2001 From: jh-RLI Date: Fri, 22 May 2026 10:26:34 +0200 Subject: [PATCH 08/25] fix(podman): align quadlet lookup service with compose setup #2319 - Use lookup-index named volume instead of docker/data/index bind mount (keeps podman and docker index data separate; fixes write.lock permission error) - Point config volume to podman/serviceConfigs/lookup/ instead of docker/ - Add lookup-index.volume quadlet unit Co-Authored-By: Claude Sonnet 4.6 --- podman/quadlets/lookup-index.volume | 9 +++++++++ podman/quadlets/oep-lookup.container | 6 +++--- 2 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 podman/quadlets/lookup-index.volume diff --git a/podman/quadlets/lookup-index.volume b/podman/quadlets/lookup-index.volume new file mode 100644 index 000000000..e53b16f0b --- /dev/null +++ b/podman/quadlets/lookup-index.volume @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2025 Jonas Huber © Reiner Lemoine Institut +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +[Unit] +Description=OEPlatform DBpedia Lookup index data volume + +[Volume] +Label=app=oeplatform diff --git a/podman/quadlets/oep-lookup.container b/podman/quadlets/oep-lookup.container index 73e7a9792..2bf7e00e3 100644 --- a/podman/quadlets/oep-lookup.container +++ b/podman/quadlets/oep-lookup.container @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: AGPL-3.0-or-later # -# NOTE: The Volume= paths below must point to your actual repo checkout. +# NOTE: The Volume= path for the config must point to your actual repo checkout. # Update /opt/oeplatform to match your deployment path, or symlink it: # sudo ln -s /your/repo/path /opt/oeplatform @@ -13,8 +13,8 @@ After=network-online.target [Container] Image=docker.io/dbpedia/lookup:dev ContainerName=lookup -Volume=/opt/oeplatform/docker/data/index:/index:ro -Volume=/opt/oeplatform/docker/serviceConfigs/lookup/config.yaml:/resources/config.yml:ro +Volume=lookup-index.volume:/index +Volume=/opt/oeplatform/podman/serviceConfigs/lookup/config.yaml:/resources/config.yml:ro PublishPort=3004:8082 Network=oep.network From 04289a62c680de77099ff7bf8e4ea5c281eeec20 Mon Sep 17 00:00:00 2001 From: jh-RLI Date: Fri, 22 May 2026 22:25:43 +0200 Subject: [PATCH 09/25] fix(podman): track .env.example files and document env variables #2319 - Add gitignore negation rules so .env.example and oep.env.example are tracked while .env files remain ignored (.env* glob was too broad) - Add podman/.env.example to version control - Document all required environment variables in README with descriptions Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 2 ++ podman/.env.example | 37 +++++++++++++++++++++++++++++++++++++ podman/README.md | 29 ++++++++++++++++++++++++++++- 3 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 podman/.env.example diff --git a/.gitignore b/.gitignore index f94f57840..449c4a224 100644 --- a/.gitignore +++ b/.gitignore @@ -84,6 +84,8 @@ venv*/ /envs /node_env .env* +!.env.example +!**/oep.env.example /fuseki apache* /oep-django-5 diff --git a/podman/.env.example b/podman/.env.example new file mode 100644 index 000000000..d9ff39926 --- /dev/null +++ b/podman/.env.example @@ -0,0 +1,37 @@ +# SPDX-FileCopyrightText: 2025 Jonas Huber © Reiner Lemoine Institut +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +# Copy this file to .env on the server and fill in all values before starting +# the stack. The .env file must never be committed to version control. +# +# Usage: +# cp podman/.env.example .env +# # edit .env with real values +# podman-compose --env-file .env -f podman/podman-compose.yaml up -d + +# ── PostgreSQL ──────────────────────────────────────────────────────────────── +POSTGRES_USER= +POSTGRES_PASSWORD= + +# ── OEPlatform app ──────────────────────────────────────────────────────────── +# Database credentials passed to Django (must match the PostgreSQL values above) +OEP_DJANGO_USER= +OEP_DB_PW= +OEP_DJANGO_HOST=postgres +OEP_DJANGO_NAME=oep_django +LOCAL_DB_USER= +LOCAL_DB_PASSWORD= +LOCAL_DB_NAME=oedb +LOCAL_DB_HOST=postgres + +# ── Fuseki ──────────────────────────────────────────────────────────────────── +FUSEKI_ADMIN_PASSWORD= +FUSEKI_DATASET_1=ds + +# ── Ports (optional — defaults shown) ──────────────────────────────────────── +# OEP_PORT_WEB=8080 +# OEP_PORT_POSTGRES=5432 +# OEP_PORT_FUSEKI=3030 +# OEP_PORT_ONTOP=8081 +# OEP_PORT_LOOKUP=3004 diff --git a/podman/README.md b/podman/README.md index 4569fb12d..2c90cff5d 100644 --- a/podman/README.md +++ b/podman/README.md @@ -82,7 +82,34 @@ containers use unless explicitly overridden. ```sh cp podman/.env.example podman/.env -# edit podman/.env and fill in all values — this file must never be committed +``` + +Edit `podman/.env` and fill in all values. The file is read by `podman-compose` +at startup and must never be committed (it is gitignored). + +| Variable | Description | +| ----------------------- | -------------------------------------------------------------------- | +| `POSTGRES_USER` | PostgreSQL superuser name | +| `POSTGRES_PASSWORD` | PostgreSQL superuser password | +| `OEP_DJANGO_USER` | DB user Django connects as (usually same as `POSTGRES_USER`) | +| `OEP_DB_PW` | Password for `OEP_DJANGO_USER` | +| `OEP_DJANGO_HOST` | Hostname of the postgres container — keep as `postgres` | +| `OEP_DJANGO_NAME` | Django database name — keep as `oep_django` | +| `LOCAL_DB_USER` | User for the local (oedb) database — usually same as `POSTGRES_USER` | +| `LOCAL_DB_PASSWORD` | Password for `LOCAL_DB_USER` | +| `LOCAL_DB_NAME` | Local database name — keep as `oedb` | +| `LOCAL_DB_HOST` | Hostname of the postgres container — keep as `postgres` | +| `FUSEKI_ADMIN_PASSWORD` | Fuseki web UI admin password | +| `FUSEKI_DATASET_1` | Fuseki dataset name — keep as `ds` | + +Optional port overrides (defaults shown): + +```sh +OEP_PORT_WEB=8080 +OEP_PORT_POSTGRES=5432 +OEP_PORT_FUSEKI=3030 +OEP_PORT_ONTOP=8081 +OEP_PORT_LOOKUP=3004 ``` ### 2. Start the stack From 08e812ffe755b3289edf3ca3823bb3b8ad626c11 Mon Sep 17 00:00:00 2001 From: jh-RLI Date: Thu, 11 Jun 2026 17:09:07 +0200 Subject: [PATCH 10/25] feat(podman): add nginx reverse proxy for HTTPS on port 443 #2319 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Serve the rootless quadlet deployment over HTTPS without exposing the container directly: - Add host nginx reverse proxy config (podman/nginx/oeplatform.conf) that terminates TLS on :443 and proxies to the container on 127.0.0.1:8080. No certbot — operator supplies their own certificate. - Bind oep-oeplatform PublishPort to 127.0.0.1:8080 so the app is only reachable through nginx. - Make Django HTTPS/host settings env-driven: OEP_DEBUG, OEP_URL, OEP_ALLOWED_HOSTS, OEP_BEHIND_TLS_PROXY (sets SECURE_PROXY_SSL_HEADER and secure cookies), OEP_CSRF_TRUSTED_ORIGINS. Defaults preserve dev behavior. - Document the HTTPS setup and new env vars in the quadlets README and oep.env.example. Co-Authored-By: Claude Opus 4.8 (1M context) --- oeplatform/securitysettings.py.default | 18 ++++-- oeplatform/settings.py | 27 +++++++++ podman/nginx/oeplatform.conf | 77 ++++++++++++++++++++++++ podman/quadlets/README.md | 71 ++++++++++++++++++++++ podman/quadlets/oep-oeplatform.container | 5 +- podman/quadlets/oep.env.example | 16 +++++ 6 files changed, 208 insertions(+), 6 deletions(-) create mode 100644 podman/nginx/oeplatform.conf diff --git a/oeplatform/securitysettings.py.default b/oeplatform/securitysettings.py.default index 1da92eba2..6db24315a 100644 --- a/oeplatform/securitysettings.py.default +++ b/oeplatform/securitysettings.py.default @@ -5,10 +5,12 @@ from pathlib import Path SECRET_KEY = '0' # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +# Set OEP_DEBUG=False on production servers. +DEBUG = os.environ.get("OEP_DEBUG", "True").strip().lower() in ("true", "1", "yes") -# Runs on localhost only -URL = '127.0.0.1' +# Public host the platform is served under (without scheme). +# On localhost this stays 127.0.0.1; in production set OEP_URL to your domain. +URL = os.environ.get("OEP_URL", "127.0.0.1") USE_DOCKER = True @@ -25,8 +27,14 @@ DATABASES = { } -# This is unnecessary as long DEBUG is True -ALLOWED_HOSTS = [] if DEBUG else ['localhost'] +# Comma-separated list of hosts/domains the site may serve, e.g. +# OEP_ALLOWED_HOSTS="openenergyplatform.org,www.openenergyplatform.org". +# When DEBUG is on this can stay empty; in production it must contain your domain. +_allowed_hosts = os.environ.get("OEP_ALLOWED_HOSTS", "").strip() +if _allowed_hosts: + ALLOWED_HOSTS = [h.strip() for h in _allowed_hosts.split(",") if h.strip()] +else: + ALLOWED_HOSTS = [] if DEBUG else ['localhost'] TIME_OUT = 30 USER_CONNECTION_LIMIT = 4 diff --git a/oeplatform/settings.py b/oeplatform/settings.py index 56652b5b0..49f979322 100644 --- a/oeplatform/settings.py +++ b/oeplatform/settings.py @@ -33,6 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later """ # noqa: 501 +import os import sys # Build paths inside the project like this: os.path.join(BASE_DIR, ...) @@ -121,6 +122,32 @@ ] +# ── Reverse proxy / HTTPS ───────────────────────────────────────────────────── +# When the platform runs behind a TLS-terminating reverse proxy (e.g. nginx on +# the production server), the proxy speaks HTTPS to the client and plain HTTP to +# the container. These settings let Django recognise the original HTTPS request. +# Enable by setting OEP_BEHIND_TLS_PROXY=True on the server. +if os.environ.get("OEP_BEHIND_TLS_PROXY", "False").strip().lower() in ( + "true", + "1", + "yes", +): + # The proxy must send this header + # (nginx: proxy_set_header X-Forwarded-Proto $scheme;) + SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") + SESSION_COOKIE_SECURE = True + CSRF_COOKIE_SECURE = True + +# Comma-separated list of trusted origins for CSRF checks under HTTPS, e.g. +# OEP_CSRF_TRUSTED_ORIGINS="https://openenergyplatform.org,https://www.openenergyplatform.org". +# Required by Django for unsafe (POST/PUT/…) requests served over HTTPS. +_csrf_trusted_origins = os.environ.get("OEP_CSRF_TRUSTED_ORIGINS", "").strip() +if _csrf_trusted_origins: + CSRF_TRUSTED_ORIGINS = [ + o.strip() for o in _csrf_trusted_origins.split(",") if o.strip() + ] + + # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ diff --git a/podman/nginx/oeplatform.conf b/podman/nginx/oeplatform.conf new file mode 100644 index 000000000..da6c1baa3 --- /dev/null +++ b/podman/nginx/oeplatform.conf @@ -0,0 +1,77 @@ +# SPDX-FileCopyrightText: 2025 Jonas Huber © Reiner Lemoine Institut +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +# nginx reverse proxy for the OEPlatform Podman stack. +# +# nginx runs on the HOST (a normal system service running as root, so binding +# port 443 is fine) and terminates TLS. It proxies plain HTTP to the rootless +# Podman container published on 127.0.0.1:8080. This sidesteps the rootless +# "privileged port < 1024" restriction entirely — the container never touches +# 443. +# +# Certificates are provided by YOU (no certbot). Drop your certificate chain and +# private key at the ssl_certificate / ssl_certificate_key paths below. +# +# Install on the server: +# sudo apt install nginx +# sudo cp podman/nginx/oeplatform.conf /etc/nginx/sites-available/oeplatform +# sudo ln -s /etc/nginx/sites-available/oeplatform /etc/nginx/sites-enabled/oeplatform +# sudo rm -f /etc/nginx/sites-enabled/default # drop the default site +# sudo nginx -t && sudo systemctl reload nginx + +# ── Redirect all plain HTTP to HTTPS ───────────────────────────────────────── +server { + listen 80; + listen [::]:80; + server_name openenergyplatform.example.org; # <-- change to your domain + + return 301 https://$host$request_uri; +} + +# ── HTTPS ──────────────────────────────────────────────────────────────────── +server { + listen 443 ssl; + listen [::]:443 ssl; + http2 on; + server_name openenergyplatform.example.org; # <-- change to your domain + + # TLS certificate — provide your own (no certbot). + # For a quick self-signed cert to test the setup: + # sudo mkdir -p /etc/nginx/ssl/oeplatform + # sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ + # -keyout /etc/nginx/ssl/oeplatform/privkey.pem \ + # -out /etc/nginx/ssl/oeplatform/fullchain.pem \ + # -subj "/CN=openenergyplatform.example.org" + ssl_certificate /etc/nginx/ssl/oeplatform/fullchain.pem; + ssl_certificate_key /etc/nginx/ssl/oeplatform/privkey.pem; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 1d; + + # OEP is a data platform — allow large table/media uploads. + # Set a concrete ceiling (e.g. 2g) or 0 to disable the limit entirely. + client_max_body_size 2g; + + # HSTS — only enable once you are certain HTTPS works and will stay on. + # add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + location / { + # Must match the host-published port of the oeplatform container + # (PublishPort / ports: 127.0.0.1:8080:80). + proxy_pass http://127.0.0.1:8080; + proxy_http_version 1.1; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; # Django reads this (OEP_BEHIND_TLS_PROXY) + proxy_set_header X-Forwarded-Host $host; + + proxy_read_timeout 300s; + proxy_send_timeout 300s; + } +} diff --git a/podman/quadlets/README.md b/podman/quadlets/README.md index 2bdf7508d..37d77da06 100644 --- a/podman/quadlets/README.md +++ b/podman/quadlets/README.md @@ -68,6 +68,77 @@ systemctl --user enable --now \ Services start in dependency order. `oep-oeplatform` and `oep-ontop` wait for `oep-postgres` before starting. +## Serving over HTTPS (port 443) with nginx + +The `oep-oeplatform.container` publishes the app on **`127.0.0.1:8080`** only — +it is not reachable from outside the server. A reverse proxy on the host handles +the public port 443 and TLS. + +> **Why a host proxy?** Rootless Podman (`systemctl --user`) cannot bind +> privileged ports < 1024, and the container's Apache only speaks plain HTTP. +> nginx runs as a normal system service (root), so binding 443 is fine, and it +> terminates TLS before proxying to the container on 8080. + +### 1. Install nginx and the site config + +```sh +sudo apt install nginx +sudo cp podman/nginx/oeplatform.conf /etc/nginx/sites-available/oeplatform +sudo ln -s /etc/nginx/sites-available/oeplatform /etc/nginx/sites-enabled/oeplatform +sudo rm -f /etc/nginx/sites-enabled/default +``` + +Edit `/etc/nginx/sites-available/oeplatform` and replace +`openenergyplatform.example.org` with your real domain. + +### 2. Provide a TLS certificate (no certbot) + +Place your certificate chain and private key at the paths referenced in the +config: + +```text +/etc/nginx/ssl/oeplatform/fullchain.pem +/etc/nginx/ssl/oeplatform/privkey.pem +``` + +To generate a self-signed certificate for testing: + +```sh +sudo mkdir -p /etc/nginx/ssl/oeplatform +sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ + -keyout /etc/nginx/ssl/oeplatform/privkey.pem \ + -out /etc/nginx/ssl/oeplatform/fullchain.pem \ + -subj "/CN=openenergyplatform.example.org" +``` + +### 3. Tell Django it runs behind HTTPS + +In `~/.config/oeplatform/oep.env` set the production block (see +`oep.env.example`): + +```sh +OEP_DEBUG=False +OEP_URL=your-domain.org +OEP_ALLOWED_HOSTS=your-domain.org +OEP_BEHIND_TLS_PROXY=True +OEP_CSRF_TRUSTED_ORIGINS=https://your-domain.org +``` + +Then restart the app so it picks up the new environment file: + +```sh +systemctl --user restart oep-oeplatform +``` + +### 4. Enable nginx + +```sh +sudo nginx -t && sudo systemctl reload nginx +``` + +The platform is now served on `https://your-domain.org`. Plain HTTP on port 80 +is redirected to HTTPS. + ## Managing Services ```sh diff --git a/podman/quadlets/oep-oeplatform.container b/podman/quadlets/oep-oeplatform.container index 28a3d875e..6cfc76c39 100644 --- a/podman/quadlets/oep-oeplatform.container +++ b/podman/quadlets/oep-oeplatform.container @@ -12,7 +12,10 @@ ContainerName=oeplatform EnvironmentFile=%h/.config/oeplatform/oep.env Volume=oeplatform-ontologies.volume:/app/ontologies Volume=oeplatform-media.volume:/app/media -PublishPort=8080:80 +# Bound to localhost only — the nginx reverse proxy on the host terminates TLS +# on :443 and proxies here. Drop the 127.0.0.1 prefix only if you intend to +# expose the app directly without a proxy. +PublishPort=127.0.0.1:8080:80 Network=oep.network [Service] diff --git a/podman/quadlets/oep.env.example b/podman/quadlets/oep.env.example index 346774f2b..8b6409210 100644 --- a/podman/quadlets/oep.env.example +++ b/podman/quadlets/oep.env.example @@ -23,3 +23,19 @@ LOCAL_DB_HOST=postgres # ── Fuseki ──────────────────────────────────────────────────────────────────── FUSEKI_ADMIN_PASSWORD= FUSEKI_DATASET_1=ds + +# ── Production / reverse proxy (HTTPS) ──────────────────────────────────────── +# Leave the defaults for a local/dev run. On a public server behind the nginx +# reverse proxy (see podman/nginx/oeplatform.conf), set these to harden Django. +# +# Turn off Django debug mode in production. +OEP_DEBUG=False +# Public domain the platform is served under (without scheme). +OEP_URL=openenergyplatform.example.org +# Comma-separated hosts Django is allowed to serve. Must include your domain. +OEP_ALLOWED_HOSTS=openenergyplatform.example.org +# Tell Django it sits behind a TLS-terminating proxy (sets secure cookies and +# trusts the X-Forwarded-Proto header nginx sends). +OEP_BEHIND_TLS_PROXY=True +# Comma-separated HTTPS origins trusted for CSRF (scheme required). +OEP_CSRF_TRUSTED_ORIGINS=https://openenergyplatform.example.org From a5c83757fcdd57416b5ee524929f8c19a36d53a2 Mon Sep 17 00:00:00 2001 From: jh-RLI Date: Thu, 11 Jun 2026 22:26:28 +0200 Subject: [PATCH 11/25] refactor(podman): adapt nginx proxy for upstream TLS termination #2319 The production server sits behind an upstream reverse proxy / load balancer that terminates HTTPS, so the host nginx no longer manages certificates: - Drop the SSL server block and cert paths; nginx now listens on plain HTTP and proxies to the container on 127.0.0.1:8080. - Preserve the upstream's X-Forwarded-Proto via a map so Django still sees the request as HTTPS, instead of clobbering it to $scheme (http). - Update the quadlet comment and README HTTPS section accordingly (remove certbot/self-signed steps). Co-Authored-By: Claude Opus 4.8 (1M context) --- podman/nginx/oeplatform.conf | 57 ++++++++---------------- podman/quadlets/README.md | 52 ++++++++------------- podman/quadlets/oep-oeplatform.container | 6 +-- 3 files changed, 40 insertions(+), 75 deletions(-) diff --git a/podman/nginx/oeplatform.conf b/podman/nginx/oeplatform.conf index da6c1baa3..8698fcb34 100644 --- a/podman/nginx/oeplatform.conf +++ b/podman/nginx/oeplatform.conf @@ -4,14 +4,12 @@ # nginx reverse proxy for the OEPlatform Podman stack. # -# nginx runs on the HOST (a normal system service running as root, so binding -# port 443 is fine) and terminates TLS. It proxies plain HTTP to the rootless -# Podman container published on 127.0.0.1:8080. This sidesteps the rootless -# "privileged port < 1024" restriction entirely — the container never touches -# 443. +# TLS is terminated UPSTREAM (an institutional reverse proxy / load balancer in +# front of this host handles HTTPS on :443). This nginx therefore speaks plain +# HTTP: it receives already-decrypted traffic from the upstream and proxies it +# to the rootless Podman container published on 127.0.0.1:8080. # -# Certificates are provided by YOU (no certbot). Drop your certificate chain and -# private key at the ssl_certificate / ssl_certificate_key paths below. +# No certificates live here — they are managed by the upstream proxy. # # Install on the server: # sudo apt install nginx @@ -20,55 +18,36 @@ # sudo rm -f /etc/nginx/sites-enabled/default # drop the default site # sudo nginx -t && sudo systemctl reload nginx -# ── Redirect all plain HTTP to HTTPS ───────────────────────────────────────── -server { - listen 80; - listen [::]:80; - server_name openenergyplatform.example.org; # <-- change to your domain - - return 301 https://$host$request_uri; +# Preserve the original scheme the client used. The upstream proxy sets +# X-Forwarded-Proto=https; only fall back to this hop's scheme if it is absent. +# (Setting it unconditionally to $scheme would clobber it to "http" and make +# Django treat the request as insecure.) +map $http_x_forwarded_proto $forwarded_proto { + default $http_x_forwarded_proto; + "" $scheme; } -# ── HTTPS ──────────────────────────────────────────────────────────────────── server { - listen 443 ssl; - listen [::]:443 ssl; - http2 on; + # Port the upstream proxy forwards decrypted HTTP to. 80 is the common + # default — change it if your upstream targets a different port. + listen 80; + listen [::]:80; server_name openenergyplatform.example.org; # <-- change to your domain - # TLS certificate — provide your own (no certbot). - # For a quick self-signed cert to test the setup: - # sudo mkdir -p /etc/nginx/ssl/oeplatform - # sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ - # -keyout /etc/nginx/ssl/oeplatform/privkey.pem \ - # -out /etc/nginx/ssl/oeplatform/fullchain.pem \ - # -subj "/CN=openenergyplatform.example.org" - ssl_certificate /etc/nginx/ssl/oeplatform/fullchain.pem; - ssl_certificate_key /etc/nginx/ssl/oeplatform/privkey.pem; - - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers HIGH:!aNULL:!MD5; - ssl_prefer_server_ciphers on; - ssl_session_cache shared:SSL:10m; - ssl_session_timeout 1d; - # OEP is a data platform — allow large table/media uploads. # Set a concrete ceiling (e.g. 2g) or 0 to disable the limit entirely. client_max_body_size 2g; - # HSTS — only enable once you are certain HTTPS works and will stay on. - # add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; - location / { # Must match the host-published port of the oeplatform container - # (PublishPort / ports: 127.0.0.1:8080:80). + # (PublishPort 127.0.0.1:8080:80). proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; # Django reads this (OEP_BEHIND_TLS_PROXY) + proxy_set_header X-Forwarded-Proto $forwarded_proto; # Django reads this (OEP_BEHIND_TLS_PROXY) proxy_set_header X-Forwarded-Host $host; proxy_read_timeout 300s; diff --git a/podman/quadlets/README.md b/podman/quadlets/README.md index 37d77da06..2e6d45195 100644 --- a/podman/quadlets/README.md +++ b/podman/quadlets/README.md @@ -68,16 +68,17 @@ systemctl --user enable --now \ Services start in dependency order. `oep-oeplatform` and `oep-ontop` wait for `oep-postgres` before starting. -## Serving over HTTPS (port 443) with nginx +## Serving over HTTPS with nginx (TLS terminated upstream) The `oep-oeplatform.container` publishes the app on **`127.0.0.1:8080`** only — -it is not reachable from outside the server. A reverse proxy on the host handles -the public port 443 and TLS. +it is not reachable from outside the server. An nginx reverse proxy on the host +listens on plain HTTP and forwards to the container. -> **Why a host proxy?** Rootless Podman (`systemctl --user`) cannot bind -> privileged ports < 1024, and the container's Apache only speaks plain HTTP. -> nginx runs as a normal system service (root), so binding 443 is fine, and it -> terminates TLS before proxying to the container on 8080. +> **Where is TLS?** HTTPS is terminated by an **upstream proxy / load balancer** +> in front of this host. That upstream forwards already-decrypted HTTP to the +> server, so this nginx holds no certificates. It exists to (a) keep the +> rootless container bound to localhost and (b) forward the original request +> headers (notably `X-Forwarded-Proto`) so Django knows the client used HTTPS. ### 1. Install nginx and the site config @@ -88,30 +89,13 @@ sudo ln -s /etc/nginx/sites-available/oeplatform /etc/nginx/sites-enabled/oeplat sudo rm -f /etc/nginx/sites-enabled/default ``` -Edit `/etc/nginx/sites-available/oeplatform` and replace -`openenergyplatform.example.org` with your real domain. +Edit `/etc/nginx/sites-available/oeplatform`: -### 2. Provide a TLS certificate (no certbot) +- Replace `openenergyplatform.example.org` with your real domain. +- If the upstream proxy forwards to a port other than `80`, adjust the `listen` + directive to match. -Place your certificate chain and private key at the paths referenced in the -config: - -```text -/etc/nginx/ssl/oeplatform/fullchain.pem -/etc/nginx/ssl/oeplatform/privkey.pem -``` - -To generate a self-signed certificate for testing: - -```sh -sudo mkdir -p /etc/nginx/ssl/oeplatform -sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ - -keyout /etc/nginx/ssl/oeplatform/privkey.pem \ - -out /etc/nginx/ssl/oeplatform/fullchain.pem \ - -subj "/CN=openenergyplatform.example.org" -``` - -### 3. Tell Django it runs behind HTTPS +### 2. Tell Django it runs behind HTTPS In `~/.config/oeplatform/oep.env` set the production block (see `oep.env.example`): @@ -124,20 +108,22 @@ OEP_BEHIND_TLS_PROXY=True OEP_CSRF_TRUSTED_ORIGINS=https://your-domain.org ``` -Then restart the app so it picks up the new environment file: +`OEP_BEHIND_TLS_PROXY=True` makes Django trust the `X-Forwarded-Proto` header +that the upstream sets and this nginx forwards. Then restart the app so it picks +up the new environment file: ```sh systemctl --user restart oep-oeplatform ``` -### 4. Enable nginx +### 3. Enable nginx ```sh sudo nginx -t && sudo systemctl reload nginx ``` -The platform is now served on `https://your-domain.org`. Plain HTTP on port 80 -is redirected to HTTPS. +The platform is now served over HTTPS via the upstream proxy. No certificates +are configured on this host. ## Managing Services diff --git a/podman/quadlets/oep-oeplatform.container b/podman/quadlets/oep-oeplatform.container index 6cfc76c39..dec180caf 100644 --- a/podman/quadlets/oep-oeplatform.container +++ b/podman/quadlets/oep-oeplatform.container @@ -12,9 +12,9 @@ ContainerName=oeplatform EnvironmentFile=%h/.config/oeplatform/oep.env Volume=oeplatform-ontologies.volume:/app/ontologies Volume=oeplatform-media.volume:/app/media -# Bound to localhost only — the nginx reverse proxy on the host terminates TLS -# on :443 and proxies here. Drop the 127.0.0.1 prefix only if you intend to -# expose the app directly without a proxy. +# Bound to localhost only — the nginx reverse proxy on the host proxies here +# (TLS is terminated by an upstream proxy). Drop the 127.0.0.1 prefix only if +# you intend to expose the app directly without a proxy. PublishPort=127.0.0.1:8080:80 Network=oep.network From 1cfa2bf932baf8d72c5286f2686c493d9eb5bb6a Mon Sep 17 00:00:00 2001 From: jh-RLI Date: Wed, 1 Jul 2026 11:21:39 +0200 Subject: [PATCH 12/25] reorder docker commands --- podman/Dockerfile | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/podman/Dockerfile b/podman/Dockerfile index e7140828a..421a81af8 100644 --- a/podman/Dockerfile +++ b/podman/Dockerfile @@ -42,6 +42,12 @@ COPY --from=vite-build /app/assets /app/assets COPY podman/entrypoint.sh /app/entrypoint.sh RUN chmod +x /app/entrypoint.sh +# Volume mount points — created here so named volumes initialise correctly. +# The OEO release and oeo_ext.owl are NOT baked into the image; entrypoint.sh +# downloads/seeds them into the mounted named volumes on first container start +# (see podman/entrypoint.sh). +RUN mkdir -p /app/ontologies /app/media/oeo_ext + # Build-time static asset collection (no DB needed) # Compress is also run at build time since it only reads templates and static files RUN cp /app/oeplatform/securitysettings.py.default /app/oeplatform/securitysettings.py \ @@ -49,8 +55,6 @@ RUN cp /app/oeplatform/securitysettings.py.default /app/oeplatform/securitysetti && python manage.py compress --force \ && rm /app/oeplatform/securitysettings.py -# Volume mount points — created here so named volumes initialise correctly -RUN mkdir -p /app/ontologies /app/media/oeo_ext EXPOSE 80 From 45f30717adeaaf3fae01f41e91c12cef539cdea2 Mon Sep 17 00:00:00 2001 From: jh-RLI Date: Wed, 1 Jul 2026 11:29:18 +0200 Subject: [PATCH 13/25] add latest oeo to podman image --- podman/Dockerfile | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/podman/Dockerfile b/podman/Dockerfile index 421a81af8..a3c01e515 100644 --- a/podman/Dockerfile +++ b/podman/Dockerfile @@ -46,7 +46,14 @@ RUN chmod +x /app/entrypoint.sh # The OEO release and oeo_ext.owl are NOT baked into the image; entrypoint.sh # downloads/seeds them into the mounted named volumes on first container start # (see podman/entrypoint.sh). -RUN mkdir -p /app/ontologies /app/media/oeo_ext +RUN mkdir -p /app/ontologies /app/media/oeo_ext \ + && cd /app/ontologies \ + && wget -q https://github.com/OpenEnergyPlatform/ontology/releases/latest/download/build-files.zip \ + && unzip -q build-files.zip \ + && rm build-files.zip \ + && cp /app/oeo_ext/oeo_extended_store/oeox_template/oeo_ext_template_empty.owl \ + /app/media/oeo_ext/oeo_ext.owl + # Build-time static asset collection (no DB needed) # Compress is also run at build time since it only reads templates and static files From 9eb95420df96c2f8de140e1a954c82ec4f2ad01d Mon Sep 17 00:00:00 2001 From: jh-RLI Date: Wed, 1 Jul 2026 11:42:50 +0200 Subject: [PATCH 14/25] fix(podman): bake OEO artifacts before collectstatic in production image #2319 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ontology/apps.py OntologyConfig.ready() aborts Django startup (raising ImportError) when the OEO release folder or oeo_ext.owl are absent. Because collectstatic/compress run django.setup() at build time, the image build failed unless these artifacts existed beforehand. Download build-files.zip (→ ontologies/oeo//…) and seed oeo_ext.owl from the template *before* the collectstatic step. Use unzip -o / cp -f so a stale copy pulled in by COPY . /app doesn't trigger an interactive overwrite prompt that hangs the non-interactive build. Co-Authored-By: Claude Opus 4.8 (1M context) --- podman/Dockerfile | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/podman/Dockerfile b/podman/Dockerfile index a3c01e515..0ba04c09b 100644 --- a/podman/Dockerfile +++ b/podman/Dockerfile @@ -42,17 +42,20 @@ COPY --from=vite-build /app/assets /app/assets COPY podman/entrypoint.sh /app/entrypoint.sh RUN chmod +x /app/entrypoint.sh -# Volume mount points — created here so named volumes initialise correctly. -# The OEO release and oeo_ext.owl are NOT baked into the image; entrypoint.sh -# downloads/seeds them into the mounted named volumes on first container start -# (see podman/entrypoint.sh). +# Bake the OEO release and seed the OEO-extended store into the image BEFORE any +# manage.py command runs: ontology/apps.py OntologyConfig.ready() aborts Django +# startup — including the collectstatic/compress step below — unless these files +# exist. build-files.zip extracts to ontologies/oeo//… , the layout +# settings.py and ontology/views.py expect. -o overwrites any stale copy that +# COPY . /app pulled in from the build context (non-interactive build has no TTY +# to answer unzip's overwrite prompt). RUN mkdir -p /app/ontologies /app/media/oeo_ext \ && cd /app/ontologies \ - && wget -q https://github.com/OpenEnergyPlatform/ontology/releases/latest/download/build-files.zip \ - && unzip -q build-files.zip \ - && rm build-files.zip \ - && cp /app/oeo_ext/oeo_extended_store/oeox_template/oeo_ext_template_empty.owl \ - /app/media/oeo_ext/oeo_ext.owl + && wget -q https://github.com/OpenEnergyPlatform/ontology/releases/latest/download/build-files.zip \ + && unzip -qo build-files.zip \ + && rm build-files.zip \ + && cp -f /app/oeo_ext/oeo_extended_store/oeox_template/oeo_ext_template_empty.owl \ + /app/media/oeo_ext/oeo_ext.owl # Build-time static asset collection (no DB needed) From bb7a49a7d11ba347df0be1da861747d6564ebba2 Mon Sep 17 00:00:00 2001 From: jh-RLI Date: Wed, 1 Jul 2026 11:43:29 +0200 Subject: [PATCH 15/25] docs(podman): clarify ontop JDBC driver location, keep it out of git #2319 The postgresql.jar driver is baked into the ontop image from docker/serviceConfigs/ontop/ (docker/Dockerfile.ontop copies it to /opt/ontop/lib/); it is NOT read from podman/serviceConfigs/ontop/, which is only the runtime config mount. Previous docs implied the jar belonged in the podman config dir, causing confusion. - Rewrite podman/serviceConfigs/ontop/README.md: split into runtime files (ontology.owl, ontop.properties) vs the build-time driver in docker/. - Drop postgresql.jar from podman/serviceConfigs/ontop/.gitignore (not used there). - Broaden the root .gitignore rule to **/serviceConfigs/ontop/postgresql.jar so the driver stays out of the repo in any location. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 4 +- podman/serviceConfigs/ontop/.gitignore | 3 +- podman/serviceConfigs/ontop/README.md | 70 ++++++++++++++------------ 3 files changed, 43 insertions(+), 34 deletions(-) diff --git a/.gitignore b/.gitignore index 2b50b8206..9fb6a7dfa 100644 --- a/.gitignore +++ b/.gitignore @@ -95,7 +95,9 @@ apache* # Docker .node_modules_stamp .vite -docker/serviceConfigs/ontop/postgresql.jar +# PostgreSQL JDBC driver for Ontop — provided manually, never committed +# (matches docker/ and podman/ service config dirs) +**/serviceConfigs/ontop/postgresql.jar # Deployment files docker/oeplatform_data diff --git a/podman/serviceConfigs/ontop/.gitignore b/podman/serviceConfigs/ontop/.gitignore index 5c2290083..263742af7 100644 --- a/podman/serviceConfigs/ontop/.gitignore +++ b/podman/serviceConfigs/ontop/.gitignore @@ -1,4 +1,5 @@ # These files must be provided manually — never committed to version control. +# (The postgresql.jar driver is NOT used here — it is baked into the ontop image +# from docker/serviceConfigs/ontop/; see this directory's README.) ontology.owl -postgresql.jar ontop.properties diff --git a/podman/serviceConfigs/ontop/README.md b/podman/serviceConfigs/ontop/README.md index dba093cd7..6016da593 100644 --- a/podman/serviceConfigs/ontop/README.md +++ b/podman/serviceConfigs/ontop/README.md @@ -6,28 +6,14 @@ SPDX-License-Identifier: AGPL-3.0-or-later # Ontop Service Configuration -This directory contains the production configuration for the Ontop SPARQL -endpoint. Two files must be provided manually before building or starting the -ontop service — they are gitignored and must never be committed. +This directory is mounted **read-only** into the ontop container at +`/opt/ontop-config` and supplies its **runtime configuration**. Two files here +must be provided manually before starting the service — they are gitignored and +must never be committed. -## Required files (not in git) +## Required runtime files (not in git) -### 1. `postgresql.jar` — JDBC driver - -Download the PostgreSQL JDBC driver from and -place it here as `postgresql.jar`. - -This file is copied into the ontop image at build time: - -```sh -# Build from the repository root after placing the jar here -podman build -t localhost/oep-ontop:latest -f docker/Dockerfile.ontop docker/ -``` - -> The `Dockerfile.ontop` copies the jar from `docker/serviceConfigs/ontop/`, not -> from this directory. Place a copy (or symlink) there too before building. - -### 2. `ontology.owl` — Open Energy Ontology +### 1. `ontology.owl` — Open Energy Ontology Download the latest OEO build artefacts from the [OEO GitHub releases](https://github.com/OpenEnergyPlatform/ontology/releases/latest) @@ -36,21 +22,14 @@ and place the `ontology.owl` file here. ```sh wget -O /tmp/oeo.zip \ https://github.com/OpenEnergyPlatform/ontology/releases/latest/download/build-files.zip -unzip -j /tmp/oeo.zip "*/ontology.owl" -d podman/serviceConfigs/ontop/ +unzip -jo /tmp/oeo.zip "*/ontology.owl" -d podman/serviceConfigs/ontop/ rm /tmp/oeo.zip ``` -## Files in git - -| File | Description | -| --------------------------- | ------------------------------------------------------- | -| `mapping.obda` | Empty OBDA mapping skeleton — extend for your tables | -| `ontop.properties.template` | JDBC connection template — copy and fill in credentials | - -### `ontop.properties` credentials +### 2. `ontop.properties` — JDBC connection + credentials -`ontop.properties` is gitignored. Create it from the template and fill in the -real credentials from your `.env` / `oep.env` file: +Create it from the template and fill in the real credentials from your `.env` / +`oep.env` file: ```sh cp podman/serviceConfigs/ontop/ontop.properties.template \ @@ -59,7 +38,7 @@ cp podman/serviceConfigs/ontop/ontop.properties.template \ ``` ```properties -jdbc.user=REPLACE_WITH_POSTGRES_USER # → value of POSTGRES_USER +jdbc.user=REPLACE_WITH_POSTGRES_USER # → value of POSTGRES_USER jdbc.password=REPLACE_WITH_POSTGRES_PASSWORD # → value of POSTGRES_PASSWORD ``` @@ -67,6 +46,33 @@ Ontop does not support environment variable substitution in this file, so credentials must be written in plain text. The `.gitignore` in this directory prevents accidental commits. +## The PostgreSQL JDBC driver lives elsewhere + +The `postgresql.jar` driver is **not** used from this directory. It is baked +into the ontop image at **build time** from `docker/serviceConfigs/ontop/` — +[docker/Dockerfile.ontop](../../../docker/Dockerfile.ontop) copies it to +`/opt/ontop/lib/postgresql.jar`. Place it there before building the image: + +```sh +# From the repository root +curl -fsSL \ + "https://repo1.maven.org/maven2/org/postgresql/postgresql/42.7.3/postgresql-42.7.3.jar" \ + -o docker/serviceConfigs/ontop/postgresql.jar + +podman build -t ghcr.io/openenergyplatform/oeplatform-ontop:latest \ + -f docker/Dockerfile.ontop docker/ +``` + +That path is gitignored (`**/serviceConfigs/ontop/postgresql.jar` in the root +`.gitignore`), so the driver never enters the repository. + +## Files in git + +| File | Description | +| --------------------------- | ------------------------------------------------------- | +| `mapping.obda` | Empty OBDA mapping skeleton — extend for your tables | +| `ontop.properties.template` | JDBC connection template — copy and fill in credentials | + ### `mapping.obda` — extending the mapping The skeleton file contains only prefix declarations and an empty mapping From 6f4ca152c424cad30f2272f7d5a1e48708d6075a Mon Sep 17 00:00:00 2001 From: jh-RLI Date: Wed, 1 Jul 2026 11:51:21 +0200 Subject: [PATCH 16/25] build(podman): exclude ontologies/ and media/ from Docker build context #2319 COPY . /app was pulling local, untracked ontologies/ and media/ trees into the image, which bloated the context and collided with the OEO release the Dockerfile bakes (forcing the unzip -o workaround). Ignore them so the baked artifacts are the single source of truth and the build context stays small. Co-Authored-By: Claude Opus 4.8 (1M context) --- .dockerignore | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.dockerignore b/.dockerignore index 8e3d0e1a7..4e0fe05c8 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,3 +3,9 @@ env* oeplatform/securitysettings.py node_modules/ + +# Runtime artifacts — must NOT be copied into the build context. The image bakes +# the OEO release + oeo_ext.owl itself (see podman/Dockerfile); a local copy here +# would bloat the context and collide with the baked version. +ontologies/ +media/ From 852cfb2a86a01099cd3e35cc06a3086b30c202c7 Mon Sep 17 00:00:00 2001 From: jh-RLI Date: Wed, 1 Jul 2026 13:33:14 +0200 Subject: [PATCH 17/25] fix(podman): nginx listens on 443 (plain HTTP) for upstream TLS #2319 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The upstream LB terminates TLS and forwards cleartext HTTP to this host on port 443 (the only port open to the LB), not 80. Switch the nginx listen directive from 80 to 443 — still without ssl / no certificate, since TLS is handled upstream. nginx runs as root so binding privileged :443 in front of the rootless container (127.0.0.1:8080) is fine. Update the quadlets README HTTPS section to match (listen 443, why the root nginx shim is needed for a rootless container). Co-Authored-By: Claude Opus 4.8 (1M context) --- podman/nginx/oeplatform.conf | 21 +++++++++++++-------- podman/quadlets/README.md | 17 ++++++++++------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/podman/nginx/oeplatform.conf b/podman/nginx/oeplatform.conf index 8698fcb34..77490f6a3 100644 --- a/podman/nginx/oeplatform.conf +++ b/podman/nginx/oeplatform.conf @@ -5,11 +5,12 @@ # nginx reverse proxy for the OEPlatform Podman stack. # # TLS is terminated UPSTREAM (an institutional reverse proxy / load balancer in -# front of this host handles HTTPS on :443). This nginx therefore speaks plain -# HTTP: it receives already-decrypted traffic from the upstream and proxies it -# to the rootless Podman container published on 127.0.0.1:8080. +# front of this host handles public HTTPS). It then forwards already-decrypted, +# plain HTTP to this host on port 443 (the only port open to the LB), and this +# nginx proxies it to the rootless Podman container published on 127.0.0.1:8080. # -# No certificates live here — they are managed by the upstream proxy. +# So port 443 here carries cleartext HTTP, not TLS — there is no `ssl` directive +# and no certificate; those live on the upstream proxy. # # Install on the server: # sudo apt install nginx @@ -28,10 +29,14 @@ map $http_x_forwarded_proto $forwarded_proto { } server { - # Port the upstream proxy forwards decrypted HTTP to. 80 is the common - # default — change it if your upstream targets a different port. - listen 80; - listen [::]:80; + # Port the upstream proxy forwards decrypted HTTP to. NOTE: this is plain + # HTTP even though it is port 443 — TLS is terminated by the upstream LB, + # which forwards cleartext HTTP here (there is intentionally no `ssl` and no + # certificate). nginx runs as root, so binding privileged :443 is fine even + # though the app container is rootless. Change the port if your upstream + # targets a different one. + listen 443; + listen [::]:443; server_name openenergyplatform.example.org; # <-- change to your domain # OEP is a data platform — allow large table/media uploads. diff --git a/podman/quadlets/README.md b/podman/quadlets/README.md index 2e6d45195..2a015c5b6 100644 --- a/podman/quadlets/README.md +++ b/podman/quadlets/README.md @@ -72,13 +72,16 @@ Services start in dependency order. `oep-oeplatform` and `oep-ontop` wait for The `oep-oeplatform.container` publishes the app on **`127.0.0.1:8080`** only — it is not reachable from outside the server. An nginx reverse proxy on the host -listens on plain HTTP and forwards to the container. +listens on **port 443** and forwards to the container. > **Where is TLS?** HTTPS is terminated by an **upstream proxy / load balancer** -> in front of this host. That upstream forwards already-decrypted HTTP to the -> server, so this nginx holds no certificates. It exists to (a) keep the -> rootless container bound to localhost and (b) forward the original request -> headers (notably `X-Forwarded-Proto`) so Django knows the client used HTTPS. +> in front of this host. It forwards already-decrypted **plain HTTP to this host +> on port 443** (the only port open to the LB) — so nginx here listens on 443 +> _without_ `ssl` and holds no certificates. nginx runs as root, so it can bind +> the privileged port 443 even though the app container is rootless. It exists +> to (a) bind 443 in front of the rootless container, (b) keep that container +> bound to localhost, and (c) forward the original request headers (notably +> `X-Forwarded-Proto`) so Django knows the client used HTTPS. ### 1. Install nginx and the site config @@ -92,8 +95,8 @@ sudo rm -f /etc/nginx/sites-enabled/default Edit `/etc/nginx/sites-available/oeplatform`: - Replace `openenergyplatform.example.org` with your real domain. -- If the upstream proxy forwards to a port other than `80`, adjust the `listen` - directive to match. +- The config listens on `443` (plain HTTP — TLS is terminated upstream). If your + upstream forwards to a different port, adjust the `listen` directive to match. ### 2. Tell Django it runs behind HTTPS From 0587497bea82ab59c28b580cf4d3233171acc677 Mon Sep 17 00:00:00 2001 From: jh-RLI Date: Wed, 1 Jul 2026 14:54:51 +0200 Subject: [PATCH 18/25] feat(ontop): auto-download JDBC driver and bake config into the image #2319 - Multi-stage Dockerfile.ontop fetches the PostgreSQL JDBC driver from Maven Central at build time (--build-arg JDBC_VERSION); no jar in the context. - Bake the ontology and an empty default mapping so the endpoint starts with zero host files, regardless of database state. - CI passes JDBC_VERSION as a build arg. --- .github/workflows/build-production-image.yaml | 8 +--- docker/Dockerfile.ontop | 40 ++++++++++++++++--- docker/serviceConfigs/ontop/README.md | 12 ++++-- .../serviceConfigs/ontop/mapping.default.obda | 16 ++++++++ 4 files changed, 62 insertions(+), 14 deletions(-) create mode 100644 docker/serviceConfigs/ontop/mapping.default.obda diff --git a/.github/workflows/build-production-image.yaml b/.github/workflows/build-production-image.yaml index 677d1e4e7..9b07814ad 100644 --- a/.github/workflows/build-production-image.yaml +++ b/.github/workflows/build-production-image.yaml @@ -76,12 +76,6 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Download PostgreSQL JDBC driver - run: | - curl -fsSL \ - "https://repo1.maven.org/maven2/org/postgresql/postgresql/${{ env.JDBC_VERSION }}/postgresql-${{ env.JDBC_VERSION }}.jar" \ - -o docker/serviceConfigs/ontop/postgresql.jar - - name: Log in to container registry uses: docker/login-action@v3 with: @@ -104,6 +98,8 @@ jobs: context: ./docker file: ./docker/Dockerfile.ontop push: true + build-args: | + JDBC_VERSION=${{ env.JDBC_VERSION }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha diff --git a/docker/Dockerfile.ontop b/docker/Dockerfile.ontop index 3e90170b0..19260cb00 100644 --- a/docker/Dockerfile.ontop +++ b/docker/Dockerfile.ontop @@ -1,8 +1,38 @@ -# Use the official Ontop image as the base +# SPDX-FileCopyrightText: 2025 Jonas Huber © Reiner Lemoine Institut +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +# ── Stage 1: fetch the PostgreSQL JDBC driver ──────────────────────────────── +# Downloaded at build time from Maven Central, so no jar needs to live in the +# build context (nothing to download or commit manually). Override the version +# with --build-arg JDBC_VERSION=x.y.z. +FROM docker.io/curlimages/curl:latest AS jdbc +ARG JDBC_VERSION=42.7.3 +RUN curl -fsSL \ + "https://repo1.maven.org/maven2/org/postgresql/postgresql/${JDBC_VERSION}/postgresql-${JDBC_VERSION}.jar" \ + -o /tmp/postgresql.jar + +# ── Stage 2: Ontop, fully self-provisioned ─────────────────────────────────── FROM docker.io/ontop/ontop:latest -# Copy the PostgreSQL JDBC driver into Ontop's lib directory -COPY serviceConfigs/ontop/postgresql.jar /opt/ontop/lib/postgresql.jar +# JDBC driver on Ontop's classpath (/opt/ontop/jdbc is searched by /opt/ontop/ontop). +COPY --from=jdbc /tmp/postgresql.jar /opt/ontop/jdbc/postgresql.jar + +# Bake the ontology and an EMPTY default mapping into the image so the service +# starts with zero host files and works regardless of database state — an empty +# mapping references no tables, so the endpoint comes up cleanly even before the +# OEDB data tables exist. Provide the real mapping at runtime by bind-mounting a +# file over /opt/ontop-config/mapping.obda (see docker/serviceConfigs/ontop/ +# mapping.obda) or by pointing ONTOP_MAPPING_FILE elsewhere. +COPY serviceConfigs/ontop/ontology.owl /opt/ontop-config/ontology.owl +COPY serviceConfigs/ontop/mapping.default.obda /opt/ontop-config/mapping.obda -# Ensure it's world-readable (optional but safe) -# RUN chmod 644 /opt/ontop/lib/postgresql.jar +# Non-secret configuration baked as defaults. The DB connection is supplied at +# runtime via environment variables (ONTOP_DB_URL / ONTOP_DB_USER / +# ONTOP_DB_PASSWORD) from oep.env / .env — Ontop reads them natively, so no +# ontop.properties file is needed. ONTOP_LAZY_INIT lets the endpoint start even +# before the mapped tables exist in the database. +ENV ONTOP_MAPPING_FILE=/opt/ontop-config/mapping.obda \ + ONTOP_ONTOLOGY_FILE=/opt/ontop-config/ontology.owl \ + ONTOP_DB_DRIVER=org.postgresql.Driver \ + ONTOP_LAZY_INIT=true diff --git a/docker/serviceConfigs/ontop/README.md b/docker/serviceConfigs/ontop/README.md index 07ca09f81..63fbff22f 100644 --- a/docker/serviceConfigs/ontop/README.md +++ b/docker/serviceConfigs/ontop/README.md @@ -1,7 +1,13 @@ # Complete ontop setup -Download the database JDBC driver for ontop: +The PostgreSQL JDBC driver is **downloaded automatically** when the ontop image +is built — `docker/Dockerfile.ontop` fetches it from Maven Central and places it +on Ontop's classpath. No manual download is required. -- +To use a different driver version, build with: -Add the file postgresql.jar to this directory. +```sh +podman build --build-arg JDBC_VERSION=42.7.3 \ + -t ghcr.io/openenergyplatform/oeplatform-ontop:latest \ + -f docker/Dockerfile.ontop docker/ +``` diff --git a/docker/serviceConfigs/ontop/mapping.default.obda b/docker/serviceConfigs/ontop/mapping.default.obda new file mode 100644 index 000000000..fdc461198 --- /dev/null +++ b/docker/serviceConfigs/ontop/mapping.default.obda @@ -0,0 +1,16 @@ +[PrefixDeclaration] +: http://example.org/voc# +owl: http://www.w3.org/2002/07/owl# +rdf: http://www.w3.org/1999/02/22-rdf-syntax-ns# +xml: http://www.w3.org/XML/1998/namespace +xsd: http://www.w3.org/2001/XMLSchema# +foaf: http://xmlns.com/foaf/0.1/ +obda: https://w3id.org/obda/vocabulary# +rdfs: http://www.w3.org/2000/01/rdf-schema# +oeo: https://openenergyplatform.org/ontology/oeo/ +oekg: https://openenergyplatform.org/ontology/oeo/oekg/ +llc: https://www.omg.org/spec/LCC/Countries/ISO3166-1-CountryCodes/ + +[MappingDeclaration] @collection [[ + +]] From 37998db003dfaff0baac9d272d3626e16b5d55aa Mon Sep 17 00:00:00 2001 From: jh-RLI Date: Wed, 1 Jul 2026 14:55:04 +0200 Subject: [PATCH 19/25] refactor(ontop): configure connection via env vars, drop ontop.properties #2319 - Supply the DB connection through ONTOP_DB_URL / ONTOP_DB_USER / ONTOP_DB_PASSWORD (read natively by ontop); remove ontop.properties.template. - Fix the no-op ONTOP_OWL_FILE -> ONTOP_ONTOLOGY_FILE. - Drop the ontop host bind mount and /opt/oeplatform path dependency; wait for postgres via ONTOP_WAIT_FOR. - Mirror the same env-based config in podman-compose and docker-compose.dev. - Add the ONTOP_DB_* block to the env examples. --- docker/docker-compose.dev.yaml | 10 ++++++++-- podman/.env.example | 6 ++++++ podman/podman-compose.yaml | 11 ++++++----- podman/quadlets/oep-ontop.container | 15 ++++++++------- podman/quadlets/oep.env.example | 8 ++++++++ podman/serviceConfigs/ontop/.gitignore | 8 +++++--- .../ontop/ontop.properties.template | 16 ---------------- 7 files changed, 41 insertions(+), 33 deletions(-) delete mode 100644 podman/serviceConfigs/ontop/ontop.properties.template diff --git a/docker/docker-compose.dev.yaml b/docker/docker-compose.dev.yaml index d70a5d443..bea676dc2 100644 --- a/docker/docker-compose.dev.yaml +++ b/docker/docker-compose.dev.yaml @@ -115,10 +115,16 @@ services: container_name: ontop ports: - "8080:8080" + # Ontology, mapping and JDBC driver are baked into the image. The dir is + # still mounted so you can live-edit mapping.obda / ontology.owl without a + # rebuild. DB connection comes from env — no ontop.properties needed. environment: ONTOP_MAPPING_FILE: "/opt/ontop-config/mapping.obda" - ONTOP_OWL_FILE: "/opt/ontop-config/ontology.owl" - ONTOP_PROPERTIES_FILE: "/opt/ontop-config/ontop.properties" + ONTOP_ONTOLOGY_FILE: "/opt/ontop-config/ontology.owl" + ONTOP_DB_URL: "jdbc:postgresql://postgres:5432/oedb" + ONTOP_DB_USER: postgres + ONTOP_DB_PASSWORD: postgres + ONTOP_WAIT_FOR: postgres:5432 volumes: - ./serviceConfigs/ontop:/opt/ontop-config depends_on: diff --git a/podman/.env.example b/podman/.env.example index d9ff39926..98dc11dc3 100644 --- a/podman/.env.example +++ b/podman/.env.example @@ -29,6 +29,12 @@ LOCAL_DB_HOST=postgres FUSEKI_ADMIN_PASSWORD= FUSEKI_DATASET_1=ds +# ── Ontop SPARQL endpoint ───────────────────────────────────────────────────── +# Ontology, mapping and JDBC driver are baked into the image; only the DB +# connection is configured. USER/PASSWORD default to the POSTGRES_* values above +# via the compose file — override the URL here if the DB is elsewhere. +# ONTOP_DB_URL=jdbc:postgresql://postgres:5432/oedb + # ── Ports (optional — defaults shown) ──────────────────────────────────────── # OEP_PORT_WEB=8080 # OEP_PORT_POSTGRES=5432 diff --git a/podman/podman-compose.yaml b/podman/podman-compose.yaml index f89ebed7d..98cb43869 100644 --- a/podman/podman-compose.yaml +++ b/podman/podman-compose.yaml @@ -74,12 +74,13 @@ services: container_name: ontop ports: - "${OEP_PORT_ONTOP:-8081}:8080" + # Ontology, mapping and JDBC driver are baked into the image; only the DB + # connection is configured here (reusing the postgres credentials). environment: - ONTOP_MAPPING_FILE: "/opt/ontop-config/mapping.obda" - ONTOP_OWL_FILE: "/opt/ontop-config/ontology.owl" - ONTOP_PROPERTIES_FILE: "/opt/ontop-config/ontop.properties" - volumes: - - ./serviceConfigs/ontop:/opt/ontop-config + ONTOP_DB_URL: ${ONTOP_DB_URL:-jdbc:postgresql://postgres:5432/oedb} + ONTOP_DB_USER: ${POSTGRES_USER} + ONTOP_DB_PASSWORD: ${POSTGRES_PASSWORD} + ONTOP_WAIT_FOR: postgres:5432 depends_on: - postgres networks: diff --git a/podman/quadlets/oep-ontop.container b/podman/quadlets/oep-ontop.container index 85cf617cf..6ec577a02 100644 --- a/podman/quadlets/oep-ontop.container +++ b/podman/quadlets/oep-ontop.container @@ -2,9 +2,9 @@ # # SPDX-License-Identifier: AGPL-3.0-or-later # -# NOTE: The Volume= path below must point to your actual repo checkout. -# Update /opt/oeplatform to match your deployment path, or symlink it: -# sudo ln -s /your/actual/repo/path /opt/oeplatform +# The ontology, mapping and JDBC driver are baked into the image — no host +# files or bind mounts are required. The database connection is configured +# through oep.env (ONTOP_DB_URL / ONTOP_DB_USER / ONTOP_DB_PASSWORD). [Unit] Description=OEPlatform Ontop SPARQL endpoint @@ -13,10 +13,11 @@ After=oep-postgres.service [Container] Image=ghcr.io/openenergyplatform/oeplatform-ontop:latest ContainerName=ontop -Volume=/opt/oeplatform/podman/serviceConfigs/ontop:/opt/ontop-config:ro -Environment=ONTOP_MAPPING_FILE=/opt/ontop-config/mapping.obda -Environment=ONTOP_OWL_FILE=/opt/ontop-config/ontology.owl -Environment=ONTOP_PROPERTIES_FILE=/opt/ontop-config/ontop.properties +# DB connection (ONTOP_DB_URL / ONTOP_DB_USER / ONTOP_DB_PASSWORD) comes from +# the shared env file, like the web app. +EnvironmentFile=%h/.config/oeplatform/oep.env +# Wait for the database to accept connections before starting the endpoint. +Environment=ONTOP_WAIT_FOR=postgres:5432 PublishPort=8081:8080 Network=oep.network diff --git a/podman/quadlets/oep.env.example b/podman/quadlets/oep.env.example index 8b6409210..1cc0ac04c 100644 --- a/podman/quadlets/oep.env.example +++ b/podman/quadlets/oep.env.example @@ -24,6 +24,14 @@ LOCAL_DB_HOST=postgres FUSEKI_ADMIN_PASSWORD= FUSEKI_DATASET_1=ds +# ── Ontop SPARQL endpoint ───────────────────────────────────────────────────── +# Ontop connects to the OEDB. Ontology, mapping and JDBC driver are baked into +# the image; only the connection is configured here. USER/PASSWORD normally +# match POSTGRES_USER / POSTGRES_PASSWORD above. +ONTOP_DB_URL=jdbc:postgresql://postgres:5432/oedb +ONTOP_DB_USER= +ONTOP_DB_PASSWORD= + # ── Production / reverse proxy (HTTPS) ──────────────────────────────────────── # Leave the defaults for a local/dev run. On a public server behind the nginx # reverse proxy (see podman/nginx/oeplatform.conf), set these to harden Django. diff --git a/podman/serviceConfigs/ontop/.gitignore b/podman/serviceConfigs/ontop/.gitignore index 263742af7..c548ae3c8 100644 --- a/podman/serviceConfigs/ontop/.gitignore +++ b/podman/serviceConfigs/ontop/.gitignore @@ -1,5 +1,7 @@ -# These files must be provided manually — never committed to version control. -# (The postgresql.jar driver is NOT used here — it is baked into the ontop image -# from docker/serviceConfigs/ontop/; see this directory's README.) +# The ontop image is self-provisioning: the JDBC driver, ontology and mapping +# are baked in, and the DB connection comes from env vars (see README). Nothing +# here needs to be provided manually. These entries guard against accidentally +# committing a large ontology override, or a leftover legacy ontop.properties +# that may hold plaintext DB credentials. ontology.owl ontop.properties diff --git a/podman/serviceConfigs/ontop/ontop.properties.template b/podman/serviceConfigs/ontop/ontop.properties.template deleted file mode 100644 index 3e0ba7dab..000000000 --- a/podman/serviceConfigs/ontop/ontop.properties.template +++ /dev/null @@ -1,16 +0,0 @@ -# SPDX-FileCopyrightText: 2025 Jonas Huber © Reiner Lemoine Institut -# -# SPDX-License-Identifier: AGPL-3.0-or-later -# -# Production ontop JDBC connection settings. -# The hostname "postgres" resolves to the postgres container on the shared -# Podman network (ContainerName=postgres in the quadlet / service name in compose). -# -# jdbc.user and jdbc.password must match POSTGRES_USER / POSTGRES_PASSWORD -# set in your .env / oep.env file. Ontop does not support environment variable -# substitution in this file, so fill in the real values directly. - -jdbc.url=jdbc:postgresql://postgres:5432/oedb -jdbc.user=REPLACE_WITH_POSTGRES_USER -jdbc.password=REPLACE_WITH_POSTGRES_PASSWORD -jdbc.driver=org.postgresql.Driver From b194f1b7c8c05a701cfb4869745198d64626606a Mon Sep 17 00:00:00 2001 From: jh-RLI Date: Wed, 1 Jul 2026 14:55:23 +0200 Subject: [PATCH 20/25] docs(ontop): document self-provisioning and env-based config #2319 Update the quadlets README, ontop service README and the ontop install guide to reflect the baked artifacts, env-based DB connection and the empty default mapping. --- docs/installation/guides/setup-ontop.md | 22 ++++-- podman/quadlets/README.md | 12 ++- podman/serviceConfigs/ontop/README.md | 101 ++++++++---------------- 3 files changed, 58 insertions(+), 77 deletions(-) diff --git a/docs/installation/guides/setup-ontop.md b/docs/installation/guides/setup-ontop.md index f9892748c..37f0b9781 100644 --- a/docs/installation/guides/setup-ontop.md +++ b/docs/installation/guides/setup-ontop.md @@ -8,14 +8,22 @@ mappings ontop on the "normal" sql like table definition. We offer the pre-configured ontop service as part of the OEP-docker setup for development. It comes with a empty semantic mapping template which can be -extended based on the user needs. You still need to download the JDBC database -driver to enable connection to the postgresql database OEDB. +extended based on the user needs. -Once you downloaded the driver make sure it is available in the ontop config -directory and only then build the ontop service using docker. +The ontop image is self-provisioning — the PostgreSQL JDBC driver, the ontology +(`ontology.owl`) and the mapping (`mapping.obda`) are all baked into the image +at build time. No files need to be placed or downloaded manually. -Download the database JDBC driver for ontop: +- The JDBC driver is fetched from Maven Central during the build. Pin a version + with `--build-arg JDBC_VERSION=x.y.z` (default: 42.7.3). +- The database connection is configured through environment variables (in + `oep.env` / `.env`), so there is no `ontop.properties` file to create: -- + ```sh + ONTOP_DB_URL=jdbc:postgresql://postgres:5432/oedb + ONTOP_DB_USER= + ONTOP_DB_PASSWORD= + ``` -Add the file postgresql.jar to this directory. +To customise the mapping, edit `docker/serviceConfigs/ontop/mapping.obda` and +rebuild, or bind-mount your own file over `/opt/ontop-config/mapping.obda`. diff --git a/podman/quadlets/README.md b/podman/quadlets/README.md index 2a015c5b6..6198b353d 100644 --- a/podman/quadlets/README.md +++ b/podman/quadlets/README.md @@ -156,16 +156,20 @@ podman build -t localhost/oeplatform:latest -f podman/Dockerfile . systemctl --user restart oep-oeplatform ``` -## Repo Path for Ontop and Lookup +## Repo Path for Lookup -The `oep-ontop.container` and `oep-lookup.container` files bind-mount config -files from the repository. They default to `/opt/oeplatform`. If your checkout -is elsewhere, update the `Volume=` lines in both files, or create a symlink: +The `oep-lookup.container` file bind-mounts its config from the repository, +defaulting to `/opt/oeplatform`. If your checkout is elsewhere, update the +`Volume=` line, or create a symlink: ```sh sudo ln -s /your/actual/repo/path /opt/oeplatform ``` +> `oep-ontop.container` no longer needs this — its ontology, mapping and JDBC +> driver are baked into the image and its DB connection comes from `oep.env` +> (`ONTOP_DB_URL` / `ONTOP_DB_USER` / `ONTOP_DB_PASSWORD`). + ## Uninstall ```sh diff --git a/podman/serviceConfigs/ontop/README.md b/podman/serviceConfigs/ontop/README.md index 6016da593..0d35708a6 100644 --- a/podman/serviceConfigs/ontop/README.md +++ b/podman/serviceConfigs/ontop/README.md @@ -6,77 +6,45 @@ SPDX-License-Identifier: AGPL-3.0-or-later # Ontop Service Configuration -This directory is mounted **read-only** into the ontop container at -`/opt/ontop-config` and supplies its **runtime configuration**. Two files here -must be provided manually before starting the service — they are gitignored and -must never be committed. - -## Required runtime files (not in git) - -### 1. `ontology.owl` — Open Energy Ontology - -Download the latest OEO build artefacts from the -[OEO GitHub releases](https://github.com/OpenEnergyPlatform/ontology/releases/latest) -and place the `ontology.owl` file here. - -```sh -wget -O /tmp/oeo.zip \ - https://github.com/OpenEnergyPlatform/ontology/releases/latest/download/build-files.zip -unzip -jo /tmp/oeo.zip "*/ontology.owl" -d podman/serviceConfigs/ontop/ -rm /tmp/oeo.zip -``` - -### 2. `ontop.properties` — JDBC connection + credentials - -Create it from the template and fill in the real credentials from your `.env` / -`oep.env` file: - -```sh -cp podman/serviceConfigs/ontop/ontop.properties.template \ - podman/serviceConfigs/ontop/ontop.properties -# then edit ontop.properties — it must never be committed -``` - -```properties -jdbc.user=REPLACE_WITH_POSTGRES_USER # → value of POSTGRES_USER -jdbc.password=REPLACE_WITH_POSTGRES_PASSWORD # → value of POSTGRES_PASSWORD -``` - -Ontop does not support environment variable substitution in this file, so -credentials must be written in plain text. The `.gitignore` in this directory -prevents accidental commits. - -## The PostgreSQL JDBC driver lives elsewhere - -The `postgresql.jar` driver is **not** used from this directory. It is baked -into the ontop image at **build time** from `docker/serviceConfigs/ontop/` — -[docker/Dockerfile.ontop](../../../docker/Dockerfile.ontop) copies it to -`/opt/ontop/lib/postgresql.jar`. Place it there before building the image: +The ontop service is **self-provisioning** — you do not need to place any files +here to run it. The ontop image +([docker/Dockerfile.ontop](../../../docker/Dockerfile.ontop)) bakes in +everything it needs: + +- **PostgreSQL JDBC driver** — downloaded from Maven Central at image build + time. +- **`ontology.owl`** — baked into the image at `/opt/ontop-config/ontology.owl`. +- **`mapping.obda`** — an **empty** default mapping is baked in at + `/opt/ontop-config/mapping.obda`, so the endpoint starts cleanly even before + the OEDB data tables exist. Provide the real mapping only once the tables are + present (see below). +- **DB connection** — supplied at runtime via the env vars `ONTOP_DB_URL`, + `ONTOP_DB_USER` and `ONTOP_DB_PASSWORD`. + +The database connection is configured like everything else — through `oep.env` +(quadlets) or `.env` (compose). Ontop reads these environment variables +natively, so there is **no `ontop.properties` file** to create. ```sh -# From the repository root -curl -fsSL \ - "https://repo1.maven.org/maven2/org/postgresql/postgresql/42.7.3/postgresql-42.7.3.jar" \ - -o docker/serviceConfigs/ontop/postgresql.jar - -podman build -t ghcr.io/openenergyplatform/oeplatform-ontop:latest \ - -f docker/Dockerfile.ontop docker/ +# oep.env / .env +ONTOP_DB_URL=jdbc:postgresql://postgres:5432/oedb +ONTOP_DB_USER= +ONTOP_DB_PASSWORD= ``` -That path is gitignored (`**/serviceConfigs/ontop/postgresql.jar` in the root -`.gitignore`), so the driver never enters the repository. - -## Files in git +## `mapping.obda` -| File | Description | -| --------------------------- | ------------------------------------------------------- | -| `mapping.obda` | Empty OBDA mapping skeleton — extend for your tables | -| `ontop.properties.template` | JDBC connection template — copy and fill in credentials | +The image ships an **empty** mapping so it always starts. The real mapping +(`docker/serviceConfigs/ontop/mapping.obda`) should be applied only once the +source tables exist in the `oedb` database. Two ways to apply it: -### `mapping.obda` — extending the mapping +1. **Override at runtime** (no rebuild): bind-mount your mapping over + `/opt/ontop-config/mapping.obda`, or point `ONTOP_MAPPING_FILE` at a mounted + file. This is the recommended approach while the mapping is still evolving. +2. **Bake it in**: replace the empty default in `docker/Dockerfile.ontop` + (`mapping.default.obda`) with `mapping.obda` and rebuild the image. -The skeleton file contains only prefix declarations and an empty mapping -collection. Add OBDA mappings as needed: +Extend the mapping as needed: ```obda [MappingDeclaration] @collection [[ @@ -88,5 +56,6 @@ source SELECT "id" FROM "data"."my_table" ]] ``` -The source table must exist in the `oedb` database before Ontop can start -successfully with a mapping that references it. +The endpoint starts with `ONTOP_LAZY_INIT=true`, so it comes up even before the +mapped source tables exist in the `oedb` database; mapping errors then surface +at query time rather than blocking startup. From 63faaa2b3c3e03987d49b0e263f68695da9bc39f Mon Sep 17 00:00:00 2001 From: jh-RLI Date: Wed, 1 Jul 2026 16:15:24 +0200 Subject: [PATCH 21/25] fix(podman): resolve lookup config path automatically, drop symlink #2319 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The lookup service bind-mounts its config from the repo. Instead of requiring a manual /opt/oeplatform symlink, the unit ships an @@OEP_REPO@@ placeholder that install.sh replaces with the checkout's absolute path at install time. Works from any location with no manual steps — keeps the setup deterministic. --- podman/quadlets/README.md | 20 +++++++++----------- podman/quadlets/install.sh | 6 ++++++ podman/quadlets/oep-lookup.container | 8 ++++---- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/podman/quadlets/README.md b/podman/quadlets/README.md index 6198b353d..e8c5da443 100644 --- a/podman/quadlets/README.md +++ b/podman/quadlets/README.md @@ -158,17 +158,15 @@ systemctl --user restart oep-oeplatform ## Repo Path for Lookup -The `oep-lookup.container` file bind-mounts its config from the repository, -defaulting to `/opt/oeplatform`. If your checkout is elsewhere, update the -`Volume=` line, or create a symlink: - -```sh -sudo ln -s /your/actual/repo/path /opt/oeplatform -``` - -> `oep-ontop.container` no longer needs this — its ontology, mapping and JDBC -> driver are baked into the image and its DB connection comes from `oep.env` -> (`ONTOP_DB_URL` / `ONTOP_DB_USER` / `ONTOP_DB_PASSWORD`). +`oep-lookup.container` bind-mounts its config from this repository. The unit +ships with an `@@OEP_REPO@@` placeholder that `install.sh` replaces with the +absolute path of your checkout when it installs the units — so it works from any +location with **no symlink and no manual editing**. Just run `install.sh` from +the checkout you want to use. + +> `oep-ontop.container` needs no repo path at all — its ontology, mapping and +> JDBC driver are baked into the image and its DB connection comes from +> `oep.env` (`ONTOP_DB_URL` / `ONTOP_DB_USER` / `ONTOP_DB_PASSWORD`). ## Uninstall diff --git a/podman/quadlets/install.sh b/podman/quadlets/install.sh index 14305380c..2a590a496 100755 --- a/podman/quadlets/install.sh +++ b/podman/quadlets/install.sh @@ -10,6 +10,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" QUADLET_DIR="${HOME}/.config/containers/systemd" ENV_DIR="${HOME}/.config/oeplatform" @@ -19,6 +20,11 @@ cp "${SCRIPT_DIR}"/*.container "${QUADLET_DIR}/" cp "${SCRIPT_DIR}"/*.volume "${QUADLET_DIR}/" cp "${SCRIPT_DIR}"/*.network "${QUADLET_DIR}/" +# Resolve @@OEP_REPO@@ placeholders (host bind-mount paths, e.g. the lookup +# config) to this checkout's absolute path — no manual symlink required. +echo "Resolving repo path to ${REPO_ROOT}…" +sed -i "s|@@OEP_REPO@@|${REPO_ROOT}|g" "${QUADLET_DIR}"/*.container + if [ ! -f "${ENV_DIR}/oep.env" ]; then echo "Creating ${ENV_DIR}/oep.env from example…" mkdir -p "${ENV_DIR}" diff --git a/podman/quadlets/oep-lookup.container b/podman/quadlets/oep-lookup.container index 2bf7e00e3..ede36325f 100644 --- a/podman/quadlets/oep-lookup.container +++ b/podman/quadlets/oep-lookup.container @@ -2,9 +2,9 @@ # # SPDX-License-Identifier: AGPL-3.0-or-later # -# NOTE: The Volume= path for the config must point to your actual repo checkout. -# Update /opt/oeplatform to match your deployment path, or symlink it: -# sudo ln -s /your/repo/path /opt/oeplatform +# The lookup config is mounted from your repo checkout. install.sh substitutes +# the @@OEP_REPO@@ placeholder below with the actual checkout path when it copies +# this unit into place — no manual symlink or path editing needed. [Unit] Description=OEPlatform DBpedia Lookup service @@ -14,7 +14,7 @@ After=network-online.target Image=docker.io/dbpedia/lookup:dev ContainerName=lookup Volume=lookup-index.volume:/index -Volume=/opt/oeplatform/podman/serviceConfigs/lookup/config.yaml:/resources/config.yml:ro +Volume=@@OEP_REPO@@/podman/serviceConfigs/lookup/config.yaml:/resources/config.yml:ro PublishPort=3004:8082 Network=oep.network From 8aed71ab46e6932ab273137def25a16a417b4f17 Mon Sep 17 00:00:00 2001 From: jh-RLI Date: Wed, 1 Jul 2026 16:19:12 +0200 Subject: [PATCH 22/25] feat(podman): add idempotent install-nginx.sh for the host reverse proxy #2319 Script the nginx host setup so there are no manual steps: install-nginx.sh installs nginx, writes the site config with server_name taken from OEP_URL in oep.env (single source of truth), enables the site, drops the default one and reloads. Idempotent and re-runnable; domain can also be passed as an argument. Update the quadlets README HTTPS section to use it. --- podman/nginx/install-nginx.sh | 58 +++++++++++++++++++++++++++++++++++ podman/quadlets/README.md | 35 +++++++++------------ 2 files changed, 73 insertions(+), 20 deletions(-) create mode 100755 podman/nginx/install-nginx.sh diff --git a/podman/nginx/install-nginx.sh b/podman/nginx/install-nginx.sh new file mode 100755 index 000000000..1cc142bb4 --- /dev/null +++ b/podman/nginx/install-nginx.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: 2025 Jonas Huber © Reiner Lemoine Institut +# +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# Installs and configures the host nginx reverse proxy for the OEPlatform Podman +# stack. Idempotent — safe to re-run after config changes. +# +# The public domain is taken from OEP_URL in ~/.config/oeplatform/oep.env (the +# same value Django uses), so nothing needs to be edited by hand. You may also +# pass it explicitly as the first argument. +# +# bash podman/nginx/install-nginx.sh # domain from oep.env +# bash podman/nginx/install-nginx.sh my-domain.org # domain from argument + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONF_SRC="${SCRIPT_DIR}/oeplatform.conf" +ENV_FILE="${HOME}/.config/oeplatform/oep.env" +SITE=/etc/nginx/sites-available/oeplatform + +# ── Resolve the domain: argument > OEP_URL in oep.env ──────────────────────── +DOMAIN="${1:-}" +if [ -z "${DOMAIN}" ] && [ -f "${ENV_FILE}" ]; then + DOMAIN="$(grep -E '^OEP_URL=' "${ENV_FILE}" | tail -1 | cut -d= -f2- | tr -d '"' | tr -d '[:space:]' || true)" +fi +if [ -z "${DOMAIN}" ] || [ "${DOMAIN}" = "127.0.0.1" ]; then + echo "ERROR: no public domain found." >&2 + echo "Set OEP_URL in ${ENV_FILE} or pass the domain as the first argument." >&2 + exit 1 +fi + +echo "Configuring nginx reverse proxy for '${DOMAIN}'…" + +# ── Install nginx if missing ───────────────────────────────────────────────── +if ! command -v nginx >/dev/null 2>&1; then + echo "Installing nginx…" + sudo apt-get update + sudo apt-get install -y nginx +fi + +# ── Install the site config with the domain substituted ────────────────────── +tmp="$(mktemp)" +sed "s/openenergyplatform\.example\.org/${DOMAIN}/g" "${CONF_SRC}" > "${tmp}" +sudo install -D -m 0644 "${tmp}" "${SITE}" +rm -f "${tmp}" + +sudo ln -sf "${SITE}" /etc/nginx/sites-enabled/oeplatform +sudo rm -f /etc/nginx/sites-enabled/default # drop the stock "Welcome" site + +# ── Validate and reload ────────────────────────────────────────────────────── +sudo nginx -t +sudo systemctl reload nginx + +echo "" +echo "Done. nginx is serving '${DOMAIN}' on :443 (plain HTTP — TLS terminated" +echo "upstream) and proxying to the container on 127.0.0.1:8080." diff --git a/podman/quadlets/README.md b/podman/quadlets/README.md index e8c5da443..2f77232a5 100644 --- a/podman/quadlets/README.md +++ b/podman/quadlets/README.md @@ -83,22 +83,7 @@ listens on **port 443** and forwards to the container. > bound to localhost, and (c) forward the original request headers (notably > `X-Forwarded-Proto`) so Django knows the client used HTTPS. -### 1. Install nginx and the site config - -```sh -sudo apt install nginx -sudo cp podman/nginx/oeplatform.conf /etc/nginx/sites-available/oeplatform -sudo ln -s /etc/nginx/sites-available/oeplatform /etc/nginx/sites-enabled/oeplatform -sudo rm -f /etc/nginx/sites-enabled/default -``` - -Edit `/etc/nginx/sites-available/oeplatform`: - -- Replace `openenergyplatform.example.org` with your real domain. -- The config listens on `443` (plain HTTP — TLS is terminated upstream). If your - upstream forwards to a different port, adjust the `listen` directive to match. - -### 2. Tell Django it runs behind HTTPS +### 1. Tell Django it runs behind HTTPS In `~/.config/oeplatform/oep.env` set the production block (see `oep.env.example`): @@ -112,19 +97,29 @@ OEP_CSRF_TRUSTED_ORIGINS=https://your-domain.org ``` `OEP_BEHIND_TLS_PROXY=True` makes Django trust the `X-Forwarded-Proto` header -that the upstream sets and this nginx forwards. Then restart the app so it picks -up the new environment file: +that the upstream sets and this nginx forwards. Restart the app so it picks up +the new environment file: ```sh systemctl --user restart oep-oeplatform ``` -### 3. Enable nginx +### 2. Install and configure nginx ```sh -sudo nginx -t && sudo systemctl reload nginx +bash podman/nginx/install-nginx.sh ``` +This installs nginx (if needed), writes the site config with the `server_name` +taken from `OEP_URL` in your `oep.env`, enables it, disables the default site, +and reloads nginx. It is idempotent — re-run it after changing the config or +domain. To override the domain, pass it explicitly: +`bash podman/nginx/install-nginx.sh my-domain.org`. + +> The config listens on `443` (plain HTTP — TLS is terminated upstream). If your +> upstream forwards to a different port, adjust the `listen` directive in +> `podman/nginx/oeplatform.conf` and re-run the script. + The platform is now served over HTTPS via the upstream proxy. No certificates are configured on this host. From dfaf712994f4e5de2d07a8bfc6613572e714fcf2 Mon Sep 17 00:00:00 2001 From: jh-RLI Date: Wed, 1 Jul 2026 16:22:24 +0200 Subject: [PATCH 23/25] fix(podman): make install-nginx.sh work under a separate sudo admin #2319 Support the two-account setup (rootless container user + separate sudo admin): use sudo only when not already root, and look for oep.env under the invoking user's home ($SUDO_USER) as well. Lets the admin run it as `sudo bash install-nginx.sh ` while the container user owns oep.env. --- podman/nginx/install-nginx.sh | 37 ++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/podman/nginx/install-nginx.sh b/podman/nginx/install-nginx.sh index 1cc142bb4..094ea48b7 100755 --- a/podman/nginx/install-nginx.sh +++ b/podman/nginx/install-nginx.sh @@ -17,17 +17,32 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" CONF_SRC="${SCRIPT_DIR}/oeplatform.conf" -ENV_FILE="${HOME}/.config/oeplatform/oep.env" SITE=/etc/nginx/sites-available/oeplatform +# Use sudo only when not already root, so this works both as a sudo-capable +# user (e.g. the container user) and when launched as root via `sudo bash …` +# by a separate admin account. +SUDO="" +if [ "$(id -u)" -ne 0 ]; then + SUDO="sudo" +fi + # ── Resolve the domain: argument > OEP_URL in oep.env ──────────────────────── +# When run via sudo, ${HOME} is root's home, so also look under the invoking +# user's home ($SUDO_USER) for oep.env. DOMAIN="${1:-}" -if [ -z "${DOMAIN}" ] && [ -f "${ENV_FILE}" ]; then - DOMAIN="$(grep -E '^OEP_URL=' "${ENV_FILE}" | tail -1 | cut -d= -f2- | tr -d '"' | tr -d '[:space:]' || true)" +if [ -z "${DOMAIN}" ]; then + for env_file in "${HOME}/.config/oeplatform/oep.env" \ + "${SUDO_USER:+/home/${SUDO_USER}/.config/oeplatform/oep.env}"; do + if [ -n "${env_file}" ] && [ -f "${env_file}" ]; then + DOMAIN="$(grep -E '^OEP_URL=' "${env_file}" | tail -1 | cut -d= -f2- | tr -d '"' | tr -d '[:space:]' || true)" + [ -n "${DOMAIN}" ] && break + fi + done fi if [ -z "${DOMAIN}" ] || [ "${DOMAIN}" = "127.0.0.1" ]; then echo "ERROR: no public domain found." >&2 - echo "Set OEP_URL in ${ENV_FILE} or pass the domain as the first argument." >&2 + echo "Set OEP_URL in ~/.config/oeplatform/oep.env or pass the domain as the first argument." >&2 exit 1 fi @@ -36,22 +51,22 @@ echo "Configuring nginx reverse proxy for '${DOMAIN}'…" # ── Install nginx if missing ───────────────────────────────────────────────── if ! command -v nginx >/dev/null 2>&1; then echo "Installing nginx…" - sudo apt-get update - sudo apt-get install -y nginx + ${SUDO} apt-get update + ${SUDO} apt-get install -y nginx fi # ── Install the site config with the domain substituted ────────────────────── tmp="$(mktemp)" sed "s/openenergyplatform\.example\.org/${DOMAIN}/g" "${CONF_SRC}" > "${tmp}" -sudo install -D -m 0644 "${tmp}" "${SITE}" +${SUDO} install -D -m 0644 "${tmp}" "${SITE}" rm -f "${tmp}" -sudo ln -sf "${SITE}" /etc/nginx/sites-enabled/oeplatform -sudo rm -f /etc/nginx/sites-enabled/default # drop the stock "Welcome" site +${SUDO} ln -sf "${SITE}" /etc/nginx/sites-enabled/oeplatform +${SUDO} rm -f /etc/nginx/sites-enabled/default # drop the stock "Welcome" site # ── Validate and reload ────────────────────────────────────────────────────── -sudo nginx -t -sudo systemctl reload nginx +${SUDO} nginx -t +${SUDO} systemctl reload nginx echo "" echo "Done. nginx is serving '${DOMAIN}' on :443 (plain HTTP — TLS terminated" From 6139d92e1799389025f19e110ebfcabae867b4d9 Mon Sep 17 00:00:00 2001 From: jh-RLI Date: Wed, 1 Jul 2026 16:35:53 +0200 Subject: [PATCH 24/25] fix(settings): normalise scheme-less CSRF trusted origins #2319 Django 4+ rejects CSRF_TRUSTED_ORIGINS entries without a scheme, which crashed startup when OEP_CSRF_TRUSTED_ORIGINS held a bare host. Prepend https:// to any entry lacking a scheme. Only affects deployments that set the var (podman); docker setups don't set it, so the gated block is skipped unchanged. --- oeplatform/settings.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/oeplatform/settings.py b/oeplatform/settings.py index 49f979322..ded074ecc 100644 --- a/oeplatform/settings.py +++ b/oeplatform/settings.py @@ -139,12 +139,16 @@ CSRF_COOKIE_SECURE = True # Comma-separated list of trusted origins for CSRF checks under HTTPS, e.g. -# OEP_CSRF_TRUSTED_ORIGINS="https://openenergyplatform.org,https://www.openenergyplatform.org". -# Required by Django for unsafe (POST/PUT/…) requests served over HTTPS. +# OEP_CSRF_TRUSTED_ORIGINS="https://openenergyplatform.org,www.openenergyplatform.org". +# Required by Django for unsafe (POST/PUT/…) requests served over HTTPS. Django +# 4+ requires each origin to include a scheme, so a bare host (e.g. "example.org") +# is normalised to "https://example.org". _csrf_trusted_origins = os.environ.get("OEP_CSRF_TRUSTED_ORIGINS", "").strip() if _csrf_trusted_origins: CSRF_TRUSTED_ORIGINS = [ - o.strip() for o in _csrf_trusted_origins.split(",") if o.strip() + origin if "://" in origin else f"https://{origin}" + for origin in (o.strip() for o in _csrf_trusted_origins.split(",")) + if origin ] From 92740c1987c0b3c20e1cdbd9480d8e90a3d514f7 Mon Sep 17 00:00:00 2001 From: jh-RLI Date: Wed, 1 Jul 2026 18:20:06 +0200 Subject: [PATCH 25/25] fix(podman): terminate TLS at nginx for the re-encrypting upstream proxy #2319 The upstream Apache reverse proxy re-encrypts to the backend (opens an HTTPS connection to this host on :443), so serving plain HTTP there fails with "Error during SSL Handshake with remote server". Switch nginx back to `listen 443 ssl` and have install-nginx.sh auto-generate a self-signed cert (SANs for the public + internal FQDN) when none is provided; Apache's mod_proxy does not verify the backend cert by default. Update the README HTTPS section. --- podman/nginx/install-nginx.sh | 20 ++++++++++-- podman/nginx/oeplatform.conf | 59 ++++++++++++++++++----------------- podman/quadlets/README.md | 40 +++++++++++++----------- 3 files changed, 69 insertions(+), 50 deletions(-) diff --git a/podman/nginx/install-nginx.sh b/podman/nginx/install-nginx.sh index 094ea48b7..93fc5cfb0 100755 --- a/podman/nginx/install-nginx.sh +++ b/podman/nginx/install-nginx.sh @@ -55,6 +55,22 @@ if ! command -v nginx >/dev/null 2>&1; then ${SUDO} apt-get install -y nginx fi +# ── Ensure a TLS certificate exists (self-signed unless one is provided) ───── +# nginx terminates TLS on :443; the upstream Apache proxy re-encrypts to it. +# Drop a real cert at these paths to replace the self-signed one. +CERT_DIR=/etc/nginx/ssl/oeplatform +if ! ${SUDO} test -f "${CERT_DIR}/fullchain.pem" || ! ${SUDO} test -f "${CERT_DIR}/privkey.pem"; then + echo "Generating self-signed certificate for '${DOMAIN}'…" + INTERNAL_FQDN="$(hostname -f 2>/dev/null || hostname)" + ${SUDO} mkdir -p "${CERT_DIR}" + ${SUDO} openssl req -x509 -nodes -days 825 -newkey rsa:2048 \ + -keyout "${CERT_DIR}/privkey.pem" \ + -out "${CERT_DIR}/fullchain.pem" \ + -subj "/CN=${DOMAIN}" \ + -addext "subjectAltName=DNS:${DOMAIN},DNS:${INTERNAL_FQDN},DNS:localhost,IP:127.0.0.1" + ${SUDO} chmod 600 "${CERT_DIR}/privkey.pem" +fi + # ── Install the site config with the domain substituted ────────────────────── tmp="$(mktemp)" sed "s/openenergyplatform\.example\.org/${DOMAIN}/g" "${CONF_SRC}" > "${tmp}" @@ -69,5 +85,5 @@ ${SUDO} nginx -t ${SUDO} systemctl reload nginx echo "" -echo "Done. nginx is serving '${DOMAIN}' on :443 (plain HTTP — TLS terminated" -echo "upstream) and proxying to the container on 127.0.0.1:8080." +echo "Done. nginx terminates TLS on :443 for '${DOMAIN}' and proxies to the" +echo "container on 127.0.0.1:8080. The upstream Apache proxy re-encrypts to here." diff --git a/podman/nginx/oeplatform.conf b/podman/nginx/oeplatform.conf index 77490f6a3..24903260b 100644 --- a/podman/nginx/oeplatform.conf +++ b/podman/nginx/oeplatform.conf @@ -4,55 +4,56 @@ # nginx reverse proxy for the OEPlatform Podman stack. # -# TLS is terminated UPSTREAM (an institutional reverse proxy / load balancer in -# front of this host handles public HTTPS). It then forwards already-decrypted, -# plain HTTP to this host on port 443 (the only port open to the LB), and this -# nginx proxies it to the rootless Podman container published on 127.0.0.1:8080. +# The public endpoint is an upstream Apache reverse proxy that terminates the +# public TLS and handles authentication, then RE-ENCRYPTS to this host: it opens +# an HTTPS connection to nginx here on :443. So nginx must terminate TLS as well +# and then proxies plain HTTP to the rootless container on 127.0.0.1:8080. +# nginx runs as root, so binding privileged :443 is fine even though the app +# container is rootless. # -# So port 443 here carries cleartext HTTP, not TLS — there is no `ssl` directive -# and no certificate; those live on the upstream proxy. +# The certificate may be self-signed — Apache's mod_proxy does not verify the +# backend certificate by default (SSLProxyVerify none). install-nginx.sh +# generates a self-signed cert automatically if none exists; drop a real cert at +# the same paths to replace it. # -# Install on the server: -# sudo apt install nginx -# sudo cp podman/nginx/oeplatform.conf /etc/nginx/sites-available/oeplatform -# sudo ln -s /etc/nginx/sites-available/oeplatform /etc/nginx/sites-enabled/oeplatform -# sudo rm -f /etc/nginx/sites-enabled/default # drop the default site -# sudo nginx -t && sudo systemctl reload nginx - -# Preserve the original scheme the client used. The upstream proxy sets -# X-Forwarded-Proto=https; only fall back to this hop's scheme if it is absent. -# (Setting it unconditionally to $scheme would clobber it to "http" and make -# Django treat the request as insecure.) +# Install: bash podman/nginx/install-nginx.sh (see podman/quadlets/README.md) + +# Preserve the scheme the client used (the upstream sets X-Forwarded-Proto); +# fall back to this hop's scheme (https) only if the header is absent. map $http_x_forwarded_proto $forwarded_proto { default $http_x_forwarded_proto; "" $scheme; } server { - # Port the upstream proxy forwards decrypted HTTP to. NOTE: this is plain - # HTTP even though it is port 443 — TLS is terminated by the upstream LB, - # which forwards cleartext HTTP here (there is intentionally no `ssl` and no - # certificate). nginx runs as root, so binding privileged :443 is fine even - # though the app container is rootless. Change the port if your upstream - # targets a different one. - listen 443; - listen [::]:443; - server_name openenergyplatform.example.org; # <-- change to your domain + listen 443 ssl; + listen [::]:443 ssl; + http2 on; + server_name openenergyplatform.example.org; # <-- set by install-nginx.sh (OEP_URL) + + # TLS termination. Self-signed by default (see install-nginx.sh); replace + # with a real cert at the same paths if your upstream verifies backends. + ssl_certificate /etc/nginx/ssl/oeplatform/fullchain.pem; + ssl_certificate_key /etc/nginx/ssl/oeplatform/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 1d; # OEP is a data platform — allow large table/media uploads. # Set a concrete ceiling (e.g. 2g) or 0 to disable the limit entirely. client_max_body_size 2g; location / { - # Must match the host-published port of the oeplatform container - # (PublishPort 127.0.0.1:8080:80). + # The rootless oeplatform container, published on 127.0.0.1:8080. proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $forwarded_proto; # Django reads this (OEP_BEHIND_TLS_PROXY) + proxy_set_header X-Forwarded-Proto $forwarded_proto; # Django reads this (OEP_BEHIND_TLS_PROXY) proxy_set_header X-Forwarded-Host $host; proxy_read_timeout 300s; diff --git a/podman/quadlets/README.md b/podman/quadlets/README.md index 2f77232a5..256889bf1 100644 --- a/podman/quadlets/README.md +++ b/podman/quadlets/README.md @@ -68,20 +68,20 @@ systemctl --user enable --now \ Services start in dependency order. `oep-oeplatform` and `oep-ontop` wait for `oep-postgres` before starting. -## Serving over HTTPS with nginx (TLS terminated upstream) +## Serving over HTTPS with nginx The `oep-oeplatform.container` publishes the app on **`127.0.0.1:8080`** only — it is not reachable from outside the server. An nginx reverse proxy on the host -listens on **port 443** and forwards to the container. +terminates TLS on **port 443** and forwards to the container. -> **Where is TLS?** HTTPS is terminated by an **upstream proxy / load balancer** -> in front of this host. It forwards already-decrypted **plain HTTP to this host -> on port 443** (the only port open to the LB) — so nginx here listens on 443 -> _without_ `ssl` and holds no certificates. nginx runs as root, so it can bind -> the privileged port 443 even though the app container is rootless. It exists -> to (a) bind 443 in front of the rootless container, (b) keep that container -> bound to localhost, and (c) forward the original request headers (notably -> `X-Forwarded-Proto`) so Django knows the client used HTTPS. +> **TLS flow.** The public endpoint is an **upstream Apache reverse proxy** that +> terminates the public TLS and handles authentication, then **re-encrypts to +> this host** — it opens an HTTPS connection to nginx here on :443. So nginx +> also terminates TLS (with a certificate) and proxies plain HTTP to the +> container. nginx runs as root, so binding the privileged port 443 is fine even +> though the app container is rootless. The backend certificate may be +> self-signed — `install-nginx.sh` generates one automatically, and Apache's +> `mod_proxy` does not verify the backend cert by default. ### 1. Tell Django it runs behind HTTPS @@ -110,18 +110,20 @@ systemctl --user restart oep-oeplatform bash podman/nginx/install-nginx.sh ``` -This installs nginx (if needed), writes the site config with the `server_name` -taken from `OEP_URL` in your `oep.env`, enables it, disables the default site, -and reloads nginx. It is idempotent — re-run it after changing the config or -domain. To override the domain, pass it explicitly: +This installs nginx (if needed), **generates a self-signed TLS certificate** (if +none exists at `/etc/nginx/ssl/oeplatform/`), writes the site config with the +`server_name` taken from `OEP_URL` in your `oep.env`, enables it, disables the +default site, and reloads nginx. It is idempotent — re-run it after changing the +config or domain. To override the domain, pass it explicitly: `bash podman/nginx/install-nginx.sh my-domain.org`. -> The config listens on `443` (plain HTTP — TLS is terminated upstream). If your -> upstream forwards to a different port, adjust the `listen` directive in -> `podman/nginx/oeplatform.conf` and re-run the script. +> nginx terminates TLS on `443` and proxies to the container on +> `127.0.0.1:8080`. The upstream Apache proxy re-encrypts to it, so a +> self-signed backend cert is fine (Apache does not verify it by default). To +> use a real certificate instead, drop `fullchain.pem` + `privkey.pem` into +> `/etc/nginx/ssl/oeplatform/` before running the script. -The platform is now served over HTTPS via the upstream proxy. No certificates -are configured on this host. +The platform is now served over HTTPS end-to-end. ## Managing Services