From 120c663e61940819133525036558702cfce26f93 Mon Sep 17 00:00:00 2001 From: jh-RLI Date: Tue, 19 May 2026 19:29:04 +0200 Subject: [PATCH 1/9] #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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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