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/.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/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/.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/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..2c90cff5d --- /dev/null +++ b/podman/README.md @@ -0,0 +1,231 @@ + + +# 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) ≥ 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 via pip (the apt package on Ubuntu 22.04 is too old): + +```sh +pip install podman-compose +``` + +## Platform Notes + +### 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 +# 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 +``` + +Replace `` with your actual username or use `$HOME`. + +### 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 network create oep + +cat >> ~/.config/containers/containers.conf << 'EOF' +default_network = "oep" +EOF +``` + +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. + +## First-time Setup + +### 1. Create your environment file + +```sh +cp podman/.env.example podman/.env +``` + +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 + +See [Start the Stack](#start-the-stack) below. + +--- + +## 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 --env-file podman/.env -f podman/podman-compose.yaml up -d +``` + +## Stop the Stack + +```sh +podman-compose --env-file podman/.env -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 --env-file podman/.env -f podman/podman-compose.yaml logs -f oeplatform +# or directly: +podman logs -f oeplatform +``` + +## Open a Shell + +```sh +podman exec -it oeplatform bash +``` + +## Reset Database + +```sh +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 --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 + +Pull the latest production images and restart — all release steps run inside the +container automatically (migrations, static files, etc.). + +```sh +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. + +```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 + +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 | 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..f89ebed7d --- /dev/null +++ b/podman/podman-compose.yaml @@ -0,0 +1,98 @@ +# 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 + +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} + volumes: + - 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: docker.io/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" + networks: + - oep + + oeplatform: + image: ghcr.io/openenergyplatform/oeplatform-production:latest + 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 + networks: + - oep + + ontop: + image: ghcr.io/openenergyplatform/oeplatform-ontop:latest + 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: + - ./serviceConfigs/ontop:/opt/ontop-config + depends_on: + - postgres + networks: + - oep + + lookup: + restart: unless-stopped + image: docker.io/dbpedia/lookup:dev + container_name: loep_lookup + ports: + - "${OEP_PORT_LOOKUP:-3004}:8082" + volumes: + - lookup_index:/index + - ./serviceConfigs/lookup/config.yaml:/resources/config.yml + networks: + - oep 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..14305380c --- /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. 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/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-fuseki.container b/podman/quadlets/oep-fuseki.container new file mode 100644 index 000000000..0b0e071c3 --- /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=docker.io/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..2bf7e00e3 --- /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= 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 + +[Unit] +Description=OEPlatform DBpedia Lookup service +After=network-online.target + +[Container] +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 +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..28a3d875e --- /dev/null +++ b/podman/quadlets/oep-oeplatform.container @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: 2025 Jonas Huber © Reiner Lemoine Institut +# +# SPDX-License-Identifier: AGPL-3.0-or-later +# +[Unit] +Description=OEPlatform web application +After=oep-postgres.service oep-fuseki.service + +[Container] +Image=ghcr.io/openenergyplatform/oeplatform-production: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..85cf617cf --- /dev/null +++ b/podman/quadlets/oep-ontop.container @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: 2025 Jonas Huber © Reiner Lemoine Institut +# +# 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 + +[Unit] +Description=OEPlatform Ontop SPARQL endpoint +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 +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 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 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