diff --git a/.dockerignore b/.dockerignore index 8e3d0e1a7..4e0fe05c8 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,3 +3,9 @@ env* oeplatform/securitysettings.py node_modules/ + +# Runtime artifacts — must NOT be copied into the build context. The image bakes +# the OEO release + oeo_ext.owl itself (see podman/Dockerfile); a local copy here +# would bloat the context and collide with the baked version. +ontologies/ +media/ diff --git a/.github/workflows/build-production-image.yaml b/.github/workflows/build-production-image.yaml new file mode 100644 index 000000000..9b07814ad --- /dev/null +++ b/.github/workflows/build-production-image.yaml @@ -0,0 +1,106 @@ +# 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: 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 + build-args: | + JDBC_VERSION=${{ env.JDBC_VERSION }} + 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 e600a598a..9fb6a7dfa 100644 --- a/.gitignore +++ b/.gitignore @@ -84,6 +84,8 @@ venv*/ /envs /node_env .env* +!.env.example +!**/oep.env.example /fuseki apache* /oep-django-5 @@ -93,7 +95,9 @@ apache* # Docker .node_modules_stamp .vite -docker/serviceConfigs/ontop/postgresql.jar +# PostgreSQL JDBC driver for Ontop — provided manually, never committed +# (matches docker/ and podman/ service config dirs) +**/serviceConfigs/ontop/postgresql.jar # Deployment files docker/oeplatform_data diff --git a/docker/Dockerfile.ontop b/docker/Dockerfile.ontop index 5f6774bad..19260cb00 100644 --- a/docker/Dockerfile.ontop +++ b/docker/Dockerfile.ontop @@ -1,8 +1,38 @@ -# Use the official Ontop image as the base -FROM ontop/ontop:latest +# SPDX-FileCopyrightText: 2025 Jonas Huber © Reiner Lemoine Institut +# +# SPDX-License-Identifier: AGPL-3.0-or-later -# Copy the PostgreSQL JDBC driver into Ontop's lib directory -COPY serviceConfigs/ontop/postgresql.jar /opt/ontop/lib/postgresql.jar +# ── Stage 1: fetch the PostgreSQL JDBC driver ──────────────────────────────── +# Downloaded at build time from Maven Central, so no jar needs to live in the +# build context (nothing to download or commit manually). Override the version +# with --build-arg JDBC_VERSION=x.y.z. +FROM docker.io/curlimages/curl:latest AS jdbc +ARG JDBC_VERSION=42.7.3 +RUN curl -fsSL \ + "https://repo1.maven.org/maven2/org/postgresql/postgresql/${JDBC_VERSION}/postgresql-${JDBC_VERSION}.jar" \ + -o /tmp/postgresql.jar -# Ensure it's world-readable (optional but safe) -# RUN chmod 644 /opt/ontop/lib/postgresql.jar +# ── Stage 2: Ontop, fully self-provisioned ─────────────────────────────────── +FROM docker.io/ontop/ontop:latest + +# JDBC driver on Ontop's classpath (/opt/ontop/jdbc is searched by /opt/ontop/ontop). +COPY --from=jdbc /tmp/postgresql.jar /opt/ontop/jdbc/postgresql.jar + +# Bake the ontology and an EMPTY default mapping into the image so the service +# starts with zero host files and works regardless of database state — an empty +# mapping references no tables, so the endpoint comes up cleanly even before the +# OEDB data tables exist. Provide the real mapping at runtime by bind-mounting a +# file over /opt/ontop-config/mapping.obda (see docker/serviceConfigs/ontop/ +# mapping.obda) or by pointing ONTOP_MAPPING_FILE elsewhere. +COPY serviceConfigs/ontop/ontology.owl /opt/ontop-config/ontology.owl +COPY serviceConfigs/ontop/mapping.default.obda /opt/ontop-config/mapping.obda + +# Non-secret configuration baked as defaults. The DB connection is supplied at +# runtime via environment variables (ONTOP_DB_URL / ONTOP_DB_USER / +# ONTOP_DB_PASSWORD) from oep.env / .env — Ontop reads them natively, so no +# ontop.properties file is needed. ONTOP_LAZY_INIT lets the endpoint start even +# before the mapped tables exist in the database. +ENV ONTOP_MAPPING_FILE=/opt/ontop-config/mapping.obda \ + ONTOP_ONTOLOGY_FILE=/opt/ontop-config/ontology.owl \ + ONTOP_DB_DRIVER=org.postgresql.Driver \ + ONTOP_LAZY_INIT=true diff --git a/docker/docker-compose.dev.yaml b/docker/docker-compose.dev.yaml index d70a5d443..bea676dc2 100644 --- a/docker/docker-compose.dev.yaml +++ b/docker/docker-compose.dev.yaml @@ -115,10 +115,16 @@ services: container_name: ontop ports: - "8080:8080" + # Ontology, mapping and JDBC driver are baked into the image. The dir is + # still mounted so you can live-edit mapping.obda / ontology.owl without a + # rebuild. DB connection comes from env — no ontop.properties needed. environment: ONTOP_MAPPING_FILE: "/opt/ontop-config/mapping.obda" - ONTOP_OWL_FILE: "/opt/ontop-config/ontology.owl" - ONTOP_PROPERTIES_FILE: "/opt/ontop-config/ontop.properties" + ONTOP_ONTOLOGY_FILE: "/opt/ontop-config/ontology.owl" + ONTOP_DB_URL: "jdbc:postgresql://postgres:5432/oedb" + ONTOP_DB_USER: postgres + ONTOP_DB_PASSWORD: postgres + ONTOP_WAIT_FOR: postgres:5432 volumes: - ./serviceConfigs/ontop:/opt/ontop-config depends_on: diff --git a/docker/serviceConfigs/ontop/README.md b/docker/serviceConfigs/ontop/README.md index 07ca09f81..63fbff22f 100644 --- a/docker/serviceConfigs/ontop/README.md +++ b/docker/serviceConfigs/ontop/README.md @@ -1,7 +1,13 @@ # Complete ontop setup -Download the database JDBC driver for ontop: +The PostgreSQL JDBC driver is **downloaded automatically** when the ontop image +is built — `docker/Dockerfile.ontop` fetches it from Maven Central and places it +on Ontop's classpath. No manual download is required. -- +To use a different driver version, build with: -Add the file postgresql.jar to this directory. +```sh +podman build --build-arg JDBC_VERSION=42.7.3 \ + -t ghcr.io/openenergyplatform/oeplatform-ontop:latest \ + -f docker/Dockerfile.ontop docker/ +``` diff --git a/docker/serviceConfigs/ontop/mapping.default.obda b/docker/serviceConfigs/ontop/mapping.default.obda new file mode 100644 index 000000000..fdc461198 --- /dev/null +++ b/docker/serviceConfigs/ontop/mapping.default.obda @@ -0,0 +1,16 @@ +[PrefixDeclaration] +: http://example.org/voc# +owl: http://www.w3.org/2002/07/owl# +rdf: http://www.w3.org/1999/02/22-rdf-syntax-ns# +xml: http://www.w3.org/XML/1998/namespace +xsd: http://www.w3.org/2001/XMLSchema# +foaf: http://xmlns.com/foaf/0.1/ +obda: https://w3id.org/obda/vocabulary# +rdfs: http://www.w3.org/2000/01/rdf-schema# +oeo: https://openenergyplatform.org/ontology/oeo/ +oekg: https://openenergyplatform.org/ontology/oeo/oekg/ +llc: https://www.omg.org/spec/LCC/Countries/ISO3166-1-CountryCodes/ + +[MappingDeclaration] @collection [[ + +]] diff --git a/docs/installation/guides/setup-ontop.md b/docs/installation/guides/setup-ontop.md index f9892748c..37f0b9781 100644 --- a/docs/installation/guides/setup-ontop.md +++ b/docs/installation/guides/setup-ontop.md @@ -8,14 +8,22 @@ mappings ontop on the "normal" sql like table definition. We offer the pre-configured ontop service as part of the OEP-docker setup for development. It comes with a empty semantic mapping template which can be -extended based on the user needs. You still need to download the JDBC database -driver to enable connection to the postgresql database OEDB. +extended based on the user needs. -Once you downloaded the driver make sure it is available in the ontop config -directory and only then build the ontop service using docker. +The ontop image is self-provisioning — the PostgreSQL JDBC driver, the ontology +(`ontology.owl`) and the mapping (`mapping.obda`) are all baked into the image +at build time. No files need to be placed or downloaded manually. -Download the database JDBC driver for ontop: +- The JDBC driver is fetched from Maven Central during the build. Pin a version + with `--build-arg JDBC_VERSION=x.y.z` (default: 42.7.3). +- The database connection is configured through environment variables (in + `oep.env` / `.env`), so there is no `ontop.properties` file to create: -- + ```sh + ONTOP_DB_URL=jdbc:postgresql://postgres:5432/oedb + ONTOP_DB_USER= + ONTOP_DB_PASSWORD= + ``` -Add the file postgresql.jar to this directory. +To customise the mapping, edit `docker/serviceConfigs/ontop/mapping.obda` and +rebuild, or bind-mount your own file over `/opt/ontop-config/mapping.obda`. diff --git a/oeplatform/securitysettings.py.default b/oeplatform/securitysettings.py.default index 1da92eba2..6db24315a 100644 --- a/oeplatform/securitysettings.py.default +++ b/oeplatform/securitysettings.py.default @@ -5,10 +5,12 @@ from pathlib import Path SECRET_KEY = '0' # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +# Set OEP_DEBUG=False on production servers. +DEBUG = os.environ.get("OEP_DEBUG", "True").strip().lower() in ("true", "1", "yes") -# Runs on localhost only -URL = '127.0.0.1' +# Public host the platform is served under (without scheme). +# On localhost this stays 127.0.0.1; in production set OEP_URL to your domain. +URL = os.environ.get("OEP_URL", "127.0.0.1") USE_DOCKER = True @@ -25,8 +27,14 @@ DATABASES = { } -# This is unnecessary as long DEBUG is True -ALLOWED_HOSTS = [] if DEBUG else ['localhost'] +# Comma-separated list of hosts/domains the site may serve, e.g. +# OEP_ALLOWED_HOSTS="openenergyplatform.org,www.openenergyplatform.org". +# When DEBUG is on this can stay empty; in production it must contain your domain. +_allowed_hosts = os.environ.get("OEP_ALLOWED_HOSTS", "").strip() +if _allowed_hosts: + ALLOWED_HOSTS = [h.strip() for h in _allowed_hosts.split(",") if h.strip()] +else: + ALLOWED_HOSTS = [] if DEBUG else ['localhost'] TIME_OUT = 30 USER_CONNECTION_LIMIT = 4 diff --git a/oeplatform/settings.py b/oeplatform/settings.py index 56652b5b0..ded074ecc 100644 --- a/oeplatform/settings.py +++ b/oeplatform/settings.py @@ -33,6 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later """ # noqa: 501 +import os import sys # Build paths inside the project like this: os.path.join(BASE_DIR, ...) @@ -121,6 +122,36 @@ ] +# ── Reverse proxy / HTTPS ───────────────────────────────────────────────────── +# When the platform runs behind a TLS-terminating reverse proxy (e.g. nginx on +# the production server), the proxy speaks HTTPS to the client and plain HTTP to +# the container. These settings let Django recognise the original HTTPS request. +# Enable by setting OEP_BEHIND_TLS_PROXY=True on the server. +if os.environ.get("OEP_BEHIND_TLS_PROXY", "False").strip().lower() in ( + "true", + "1", + "yes", +): + # The proxy must send this header + # (nginx: proxy_set_header X-Forwarded-Proto $scheme;) + SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") + SESSION_COOKIE_SECURE = True + CSRF_COOKIE_SECURE = True + +# Comma-separated list of trusted origins for CSRF checks under HTTPS, e.g. +# OEP_CSRF_TRUSTED_ORIGINS="https://openenergyplatform.org,www.openenergyplatform.org". +# Required by Django for unsafe (POST/PUT/…) requests served over HTTPS. Django +# 4+ requires each origin to include a scheme, so a bare host (e.g. "example.org") +# is normalised to "https://example.org". +_csrf_trusted_origins = os.environ.get("OEP_CSRF_TRUSTED_ORIGINS", "").strip() +if _csrf_trusted_origins: + CSRF_TRUSTED_ORIGINS = [ + origin if "://" in origin else f"https://{origin}" + for origin in (o.strip() for o in _csrf_trusted_origins.split(",")) + if origin + ] + + # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ diff --git a/podman/.env.example b/podman/.env.example new file mode 100644 index 000000000..98dc11dc3 --- /dev/null +++ b/podman/.env.example @@ -0,0 +1,43 @@ +# 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 + +# ── Ontop SPARQL endpoint ───────────────────────────────────────────────────── +# Ontology, mapping and JDBC driver are baked into the image; only the DB +# connection is configured. USER/PASSWORD default to the POSTGRES_* values above +# via the compose file — override the URL here if the DB is elsewhere. +# ONTOP_DB_URL=jdbc:postgresql://postgres:5432/oedb + +# ── Ports (optional — defaults shown) ──────────────────────────────────────── +# OEP_PORT_WEB=8080 +# OEP_PORT_POSTGRES=5432 +# 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..0ba04c09b --- /dev/null +++ b/podman/Dockerfile @@ -0,0 +1,71 @@ +# 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 + +# Bake the OEO release and seed the OEO-extended store into the image BEFORE any +# manage.py command runs: ontology/apps.py OntologyConfig.ready() aborts Django +# startup — including the collectstatic/compress step below — unless these files +# exist. build-files.zip extracts to ontologies/oeo//… , the layout +# settings.py and ontology/views.py expect. -o overwrites any stale copy that +# COPY . /app pulled in from the build context (non-interactive build has no TTY +# to answer unzip's overwrite prompt). +RUN mkdir -p /app/ontologies /app/media/oeo_ext \ + && cd /app/ontologies \ + && wget -q https://github.com/OpenEnergyPlatform/ontology/releases/latest/download/build-files.zip \ + && unzip -qo build-files.zip \ + && rm build-files.zip \ + && cp -f /app/oeo_ext/oeo_extended_store/oeox_template/oeo_ext_template_empty.owl \ + /app/media/oeo_ext/oeo_ext.owl + + +# Build-time static asset collection (no DB needed) +# 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 + + +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/nginx/install-nginx.sh b/podman/nginx/install-nginx.sh new file mode 100755 index 000000000..93fc5cfb0 --- /dev/null +++ b/podman/nginx/install-nginx.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: 2025 Jonas Huber © Reiner Lemoine Institut +# +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# Installs and configures the host nginx reverse proxy for the OEPlatform Podman +# stack. Idempotent — safe to re-run after config changes. +# +# The public domain is taken from OEP_URL in ~/.config/oeplatform/oep.env (the +# same value Django uses), so nothing needs to be edited by hand. You may also +# pass it explicitly as the first argument. +# +# bash podman/nginx/install-nginx.sh # domain from oep.env +# bash podman/nginx/install-nginx.sh my-domain.org # domain from argument + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONF_SRC="${SCRIPT_DIR}/oeplatform.conf" +SITE=/etc/nginx/sites-available/oeplatform + +# Use sudo only when not already root, so this works both as a sudo-capable +# user (e.g. the container user) and when launched as root via `sudo bash …` +# by a separate admin account. +SUDO="" +if [ "$(id -u)" -ne 0 ]; then + SUDO="sudo" +fi + +# ── Resolve the domain: argument > OEP_URL in oep.env ──────────────────────── +# When run via sudo, ${HOME} is root's home, so also look under the invoking +# user's home ($SUDO_USER) for oep.env. +DOMAIN="${1:-}" +if [ -z "${DOMAIN}" ]; then + for env_file in "${HOME}/.config/oeplatform/oep.env" \ + "${SUDO_USER:+/home/${SUDO_USER}/.config/oeplatform/oep.env}"; do + if [ -n "${env_file}" ] && [ -f "${env_file}" ]; then + DOMAIN="$(grep -E '^OEP_URL=' "${env_file}" | tail -1 | cut -d= -f2- | tr -d '"' | tr -d '[:space:]' || true)" + [ -n "${DOMAIN}" ] && break + fi + done +fi +if [ -z "${DOMAIN}" ] || [ "${DOMAIN}" = "127.0.0.1" ]; then + echo "ERROR: no public domain found." >&2 + echo "Set OEP_URL in ~/.config/oeplatform/oep.env or pass the domain as the first argument." >&2 + exit 1 +fi + +echo "Configuring nginx reverse proxy for '${DOMAIN}'…" + +# ── Install nginx if missing ───────────────────────────────────────────────── +if ! command -v nginx >/dev/null 2>&1; then + echo "Installing nginx…" + ${SUDO} apt-get update + ${SUDO} apt-get install -y nginx +fi + +# ── Ensure a TLS certificate exists (self-signed unless one is provided) ───── +# nginx terminates TLS on :443; the upstream Apache proxy re-encrypts to it. +# Drop a real cert at these paths to replace the self-signed one. +CERT_DIR=/etc/nginx/ssl/oeplatform +if ! ${SUDO} test -f "${CERT_DIR}/fullchain.pem" || ! ${SUDO} test -f "${CERT_DIR}/privkey.pem"; then + echo "Generating self-signed certificate for '${DOMAIN}'…" + INTERNAL_FQDN="$(hostname -f 2>/dev/null || hostname)" + ${SUDO} mkdir -p "${CERT_DIR}" + ${SUDO} openssl req -x509 -nodes -days 825 -newkey rsa:2048 \ + -keyout "${CERT_DIR}/privkey.pem" \ + -out "${CERT_DIR}/fullchain.pem" \ + -subj "/CN=${DOMAIN}" \ + -addext "subjectAltName=DNS:${DOMAIN},DNS:${INTERNAL_FQDN},DNS:localhost,IP:127.0.0.1" + ${SUDO} chmod 600 "${CERT_DIR}/privkey.pem" +fi + +# ── Install the site config with the domain substituted ────────────────────── +tmp="$(mktemp)" +sed "s/openenergyplatform\.example\.org/${DOMAIN}/g" "${CONF_SRC}" > "${tmp}" +${SUDO} install -D -m 0644 "${tmp}" "${SITE}" +rm -f "${tmp}" + +${SUDO} ln -sf "${SITE}" /etc/nginx/sites-enabled/oeplatform +${SUDO} rm -f /etc/nginx/sites-enabled/default # drop the stock "Welcome" site + +# ── Validate and reload ────────────────────────────────────────────────────── +${SUDO} nginx -t +${SUDO} systemctl reload nginx + +echo "" +echo "Done. nginx terminates TLS on :443 for '${DOMAIN}' and proxies to the" +echo "container on 127.0.0.1:8080. The upstream Apache proxy re-encrypts to here." diff --git a/podman/nginx/oeplatform.conf b/podman/nginx/oeplatform.conf new file mode 100644 index 000000000..24903260b --- /dev/null +++ b/podman/nginx/oeplatform.conf @@ -0,0 +1,62 @@ +# SPDX-FileCopyrightText: 2025 Jonas Huber © Reiner Lemoine Institut +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +# nginx reverse proxy for the OEPlatform Podman stack. +# +# The public endpoint is an upstream Apache reverse proxy that terminates the +# public TLS and handles authentication, then RE-ENCRYPTS to this host: it opens +# an HTTPS connection to nginx here on :443. So nginx must terminate TLS as well +# and then proxies plain HTTP to the rootless container on 127.0.0.1:8080. +# nginx runs as root, so binding privileged :443 is fine even though the app +# container is rootless. +# +# The certificate may be self-signed — Apache's mod_proxy does not verify the +# backend certificate by default (SSLProxyVerify none). install-nginx.sh +# generates a self-signed cert automatically if none exists; drop a real cert at +# the same paths to replace it. +# +# Install: bash podman/nginx/install-nginx.sh (see podman/quadlets/README.md) + +# Preserve the scheme the client used (the upstream sets X-Forwarded-Proto); +# fall back to this hop's scheme (https) only if the header is absent. +map $http_x_forwarded_proto $forwarded_proto { + default $http_x_forwarded_proto; + "" $scheme; +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + http2 on; + server_name openenergyplatform.example.org; # <-- set by install-nginx.sh (OEP_URL) + + # TLS termination. Self-signed by default (see install-nginx.sh); replace + # with a real cert at the same paths if your upstream verifies backends. + ssl_certificate /etc/nginx/ssl/oeplatform/fullchain.pem; + ssl_certificate_key /etc/nginx/ssl/oeplatform/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 1d; + + # OEP is a data platform — allow large table/media uploads. + # Set a concrete ceiling (e.g. 2g) or 0 to disable the limit entirely. + client_max_body_size 2g; + + location / { + # The rootless oeplatform container, published on 127.0.0.1:8080. + proxy_pass http://127.0.0.1:8080; + proxy_http_version 1.1; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $forwarded_proto; # Django reads this (OEP_BEHIND_TLS_PROXY) + proxy_set_header X-Forwarded-Host $host; + + proxy_read_timeout 300s; + proxy_send_timeout 300s; + } +} diff --git a/podman/podman-compose.yaml b/podman/podman-compose.yaml new file mode 100644 index 000000000..98cb43869 --- /dev/null +++ b/podman/podman-compose.yaml @@ -0,0 +1,99 @@ +# 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" + # Ontology, mapping and JDBC driver are baked into the image; only the DB + # connection is configured here (reusing the postgres credentials). + environment: + ONTOP_DB_URL: ${ONTOP_DB_URL:-jdbc:postgresql://postgres:5432/oedb} + ONTOP_DB_USER: ${POSTGRES_USER} + ONTOP_DB_PASSWORD: ${POSTGRES_PASSWORD} + ONTOP_WAIT_FOR: postgres:5432 + depends_on: + - postgres + networks: + - 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..256889bf1 --- /dev/null +++ b/podman/quadlets/README.md @@ -0,0 +1,179 @@ + + +# 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. + +## Serving over HTTPS with nginx + +The `oep-oeplatform.container` publishes the app on **`127.0.0.1:8080`** only — +it is not reachable from outside the server. An nginx reverse proxy on the host +terminates TLS on **port 443** and forwards to the container. + +> **TLS flow.** The public endpoint is an **upstream Apache reverse proxy** that +> terminates the public TLS and handles authentication, then **re-encrypts to +> this host** — it opens an HTTPS connection to nginx here on :443. So nginx +> also terminates TLS (with a certificate) and proxies plain HTTP to the +> container. nginx runs as root, so binding the privileged port 443 is fine even +> though the app container is rootless. The backend certificate may be +> self-signed — `install-nginx.sh` generates one automatically, and Apache's +> `mod_proxy` does not verify the backend cert by default. + +### 1. Tell Django it runs behind HTTPS + +In `~/.config/oeplatform/oep.env` set the production block (see +`oep.env.example`): + +```sh +OEP_DEBUG=False +OEP_URL=your-domain.org +OEP_ALLOWED_HOSTS=your-domain.org +OEP_BEHIND_TLS_PROXY=True +OEP_CSRF_TRUSTED_ORIGINS=https://your-domain.org +``` + +`OEP_BEHIND_TLS_PROXY=True` makes Django trust the `X-Forwarded-Proto` header +that the upstream sets and this nginx forwards. Restart the app so it picks up +the new environment file: + +```sh +systemctl --user restart oep-oeplatform +``` + +### 2. Install and configure nginx + +```sh +bash podman/nginx/install-nginx.sh +``` + +This installs nginx (if needed), **generates a self-signed TLS certificate** (if +none exists at `/etc/nginx/ssl/oeplatform/`), writes the site config with the +`server_name` taken from `OEP_URL` in your `oep.env`, enables it, disables the +default site, and reloads nginx. It is idempotent — re-run it after changing the +config or domain. To override the domain, pass it explicitly: +`bash podman/nginx/install-nginx.sh my-domain.org`. + +> nginx terminates TLS on `443` and proxies to the container on +> `127.0.0.1:8080`. The upstream Apache proxy re-encrypts to it, so a +> self-signed backend cert is fine (Apache does not verify it by default). To +> use a real certificate instead, drop `fullchain.pem` + `privkey.pem` into +> `/etc/nginx/ssl/oeplatform/` before running the script. + +The platform is now served over HTTPS end-to-end. + +## 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 Lookup + +`oep-lookup.container` bind-mounts its config from this repository. The unit +ships with an `@@OEP_REPO@@` placeholder that `install.sh` replaces with the +absolute path of your checkout when it installs the units — so it works from any +location with **no symlink and no manual editing**. Just run `install.sh` from +the checkout you want to use. + +> `oep-ontop.container` needs no repo path at all — its ontology, mapping and +> JDBC driver are baked into the image and its DB connection comes from +> `oep.env` (`ONTOP_DB_URL` / `ONTOP_DB_USER` / `ONTOP_DB_PASSWORD`). + +## Uninstall + +```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..2a590a496 --- /dev/null +++ b/podman/quadlets/install.sh @@ -0,0 +1,46 @@ +#!/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)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && 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}/" + +# Resolve @@OEP_REPO@@ placeholders (host bind-mount paths, e.g. the lookup +# config) to this checkout's absolute path — no manual symlink required. +echo "Resolving repo path to ${REPO_ROOT}…" +sed -i "s|@@OEP_REPO@@|${REPO_ROOT}|g" "${QUADLET_DIR}"/*.container + +if [ ! -f "${ENV_DIR}/oep.env" ]; then + echo "Creating ${ENV_DIR}/oep.env from example…" + mkdir -p "${ENV_DIR}" + 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..ede36325f --- /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 +# +# The lookup config is mounted from your repo checkout. install.sh substitutes +# the @@OEP_REPO@@ placeholder below with the actual checkout path when it copies +# this unit into place — no manual symlink or path editing needed. + +[Unit] +Description=OEPlatform DBpedia Lookup service +After=network-online.target + +[Container] +Image=docker.io/dbpedia/lookup:dev +ContainerName=lookup +Volume=lookup-index.volume:/index +Volume=@@OEP_REPO@@/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..dec180caf --- /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 +# +[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 +# Bound to localhost only — the nginx reverse proxy on the host proxies here +# (TLS is terminated by an upstream proxy). Drop the 127.0.0.1 prefix only if +# you intend to expose the app directly without a proxy. +PublishPort=127.0.0.1:8080:80 +Network=oep.network + +[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..6ec577a02 --- /dev/null +++ b/podman/quadlets/oep-ontop.container @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: 2025 Jonas Huber © Reiner Lemoine Institut +# +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# The ontology, mapping and JDBC driver are baked into the image — no host +# files or bind mounts are required. The database connection is configured +# through oep.env (ONTOP_DB_URL / ONTOP_DB_USER / ONTOP_DB_PASSWORD). + +[Unit] +Description=OEPlatform Ontop SPARQL endpoint +After=oep-postgres.service + +[Container] +Image=ghcr.io/openenergyplatform/oeplatform-ontop:latest +ContainerName=ontop +# DB connection (ONTOP_DB_URL / ONTOP_DB_USER / ONTOP_DB_PASSWORD) comes from +# the shared env file, like the web app. +EnvironmentFile=%h/.config/oeplatform/oep.env +# Wait for the database to accept connections before starting the endpoint. +Environment=ONTOP_WAIT_FOR=postgres:5432 +PublishPort=8081:8080 +Network=oep.network + +[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..1cc0ac04c --- /dev/null +++ b/podman/quadlets/oep.env.example @@ -0,0 +1,49 @@ +# 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 + +# ── Ontop SPARQL endpoint ───────────────────────────────────────────────────── +# Ontop connects to the OEDB. Ontology, mapping and JDBC driver are baked into +# the image; only the connection is configured here. USER/PASSWORD normally +# match POSTGRES_USER / POSTGRES_PASSWORD above. +ONTOP_DB_URL=jdbc:postgresql://postgres:5432/oedb +ONTOP_DB_USER= +ONTOP_DB_PASSWORD= + +# ── Production / reverse proxy (HTTPS) ──────────────────────────────────────── +# Leave the defaults for a local/dev run. On a public server behind the nginx +# reverse proxy (see podman/nginx/oeplatform.conf), set these to harden Django. +# +# Turn off Django debug mode in production. +OEP_DEBUG=False +# Public domain the platform is served under (without scheme). +OEP_URL=openenergyplatform.example.org +# Comma-separated hosts Django is allowed to serve. Must include your domain. +OEP_ALLOWED_HOSTS=openenergyplatform.example.org +# Tell Django it sits behind a TLS-terminating proxy (sets secure cookies and +# trusts the X-Forwarded-Proto header nginx sends). +OEP_BEHIND_TLS_PROXY=True +# Comma-separated HTTPS origins trusted for CSRF (scheme required). +OEP_CSRF_TRUSTED_ORIGINS=https://openenergyplatform.example.org 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..c548ae3c8 --- /dev/null +++ b/podman/serviceConfigs/ontop/.gitignore @@ -0,0 +1,7 @@ +# The ontop image is self-provisioning: the JDBC driver, ontology and mapping +# are baked in, and the DB connection comes from env vars (see README). Nothing +# here needs to be provided manually. These entries guard against accidentally +# committing a large ontology override, or a leftover legacy ontop.properties +# that may hold plaintext DB credentials. +ontology.owl +ontop.properties diff --git a/podman/serviceConfigs/ontop/README.md b/podman/serviceConfigs/ontop/README.md new file mode 100644 index 000000000..0d35708a6 --- /dev/null +++ b/podman/serviceConfigs/ontop/README.md @@ -0,0 +1,61 @@ + + +# Ontop Service Configuration + +The ontop service is **self-provisioning** — you do not need to place any files +here to run it. The ontop image +([docker/Dockerfile.ontop](../../../docker/Dockerfile.ontop)) bakes in +everything it needs: + +- **PostgreSQL JDBC driver** — downloaded from Maven Central at image build + time. +- **`ontology.owl`** — baked into the image at `/opt/ontop-config/ontology.owl`. +- **`mapping.obda`** — an **empty** default mapping is baked in at + `/opt/ontop-config/mapping.obda`, so the endpoint starts cleanly even before + the OEDB data tables exist. Provide the real mapping only once the tables are + present (see below). +- **DB connection** — supplied at runtime via the env vars `ONTOP_DB_URL`, + `ONTOP_DB_USER` and `ONTOP_DB_PASSWORD`. + +The database connection is configured like everything else — through `oep.env` +(quadlets) or `.env` (compose). Ontop reads these environment variables +natively, so there is **no `ontop.properties` file** to create. + +```sh +# oep.env / .env +ONTOP_DB_URL=jdbc:postgresql://postgres:5432/oedb +ONTOP_DB_USER= +ONTOP_DB_PASSWORD= +``` + +## `mapping.obda` + +The image ships an **empty** mapping so it always starts. The real mapping +(`docker/serviceConfigs/ontop/mapping.obda`) should be applied only once the +source tables exist in the `oedb` database. Two ways to apply it: + +1. **Override at runtime** (no rebuild): bind-mount your mapping over + `/opt/ontop-config/mapping.obda`, or point `ONTOP_MAPPING_FILE` at a mounted + file. This is the recommended approach while the mapping is still evolving. +2. **Bake it in**: replace the empty default in `docker/Dockerfile.ontop` + (`mapping.default.obda`) with `mapping.obda` and rebuild the image. + +Extend the mapping 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 endpoint starts with `ONTOP_LAZY_INIT=true`, so it comes up even before the +mapped source tables exist in the `oedb` database; mapping errors then surface +at query time rather than blocking startup. 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 [[ + +]]