Petrolyst server infrastructure: Traefik, Trino and Superset

Traefik as the server-wide edge (TLS via Let's Encrypt, dashboard on
traefik.petrolyst.com behind oauth2-proxy and Keycloak). The
fuel-monitoring stack: Superset on atoma.petrolyst.com with Keycloak login
and Petrolyst branding, Trino reading the TimescaleDB fuel-db as catalog
"fuel". Customization lives in petrolyst-insights/, deployment in the other
folders.

Deploys run server-side (deploy/deploy.sh) from a Gitea workflow or
manually; releases are immutable, Superset metadata is backed up before
migrations, and deploy/rollback.sh restores it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Nasim SadatandClaude Opus 5.5 committed 2026-10-24 00:33:11 +02:00
1 parent 24327f9462
commit d3800ac9dd
34 files changed
+2117

No files matched your search

+174
View File
@@ -0,0 +1,174 @@
# Fuel-monitoring analytics: Superset on atoma.petrolyst.com, reading the
# operational store (fuel-db) through Trino. Run only through deploy/deploy.sh
# or deploy/rollback.sh: they export SUPERSET_IMAGE_TAG and TRINO_CONFIG_HASH
# from the release's ARTIFACTS file, and the :? guards below stop Compose from
# running with either unset.
name: fuel-monitoring
x-superset-env: &superset-env
KEYCLOAK_REALM_URL: ${KEYCLOAK_REALM_URL:-https://iam.thedigitalbridge.io/realms/thedigitalbridge}
x-superset: &superset
image: fuel-superset:${SUPERSET_IMAGE_TAG:?SUPERSET_IMAGE_TAG unset - run through deploy/deploy.sh}
pull_policy: never
secrets:
- superset_secret_key
- superset_db_password
- keycloak_petrolyst_client_secret
services:
superset:
<<: *superset
build:
context: ./superset
additional_contexts:
insights: ../../petrolyst-insights
environment:
<<: *superset-env
SERVER_WORKER_AMOUNT: "2"
restart: unless-stopped
depends_on:
superset-db:
condition: service_healthy
redis:
condition: service_healthy
networks:
- backend
- proxy
mem_limit: 1536m
healthcheck:
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8088/health"]
interval: 15s
timeout: 5s
retries: 10
start_period: 60s
labels:
traefik.enable: "true"
traefik.docker.network: proxy
traefik.http.routers.fm-superset.rule: Host(`${ATOMA_HOST:-atoma.petrolyst.com}`)
traefik.http.routers.fm-superset.entrypoints: websecure
traefik.http.routers.fm-superset.tls.certresolver: le
traefik.http.routers.fm-superset.middlewares: secure-headers@file
traefik.http.services.fm-superset.loadbalancer.server.port: "8088"
# One-shot: never started by `up`; deploy.sh runs it with `compose run --rm`.
superset-init:
<<: *superset
profiles: ["init"]
environment: *superset-env
command: ["/bin/bash", "/app/petrolyst-init.sh"]
depends_on:
superset-db:
condition: service_healthy
redis:
condition: service_healthy
networks:
- backend
restart: "no"
superset-db:
image: postgres:17.11-alpine
environment:
POSTGRES_USER: superset
POSTGRES_DB: superset
POSTGRES_PASSWORD_FILE: /run/secrets/superset_db_password
secrets:
- superset_db_password
volumes:
- superset_db_data:/var/lib/postgresql/data
networks:
- backend
restart: unless-stopped
mem_limit: 512m
healthcheck:
test: ["CMD-SHELL", "pg_isready -U superset -d superset"]
interval: 10s
timeout: 5s
retries: 10
# Cache and rate-limit store only; nothing to persist.
redis:
image: redis:7.4.11-alpine
command: ["redis-server", "--maxmemory", "128mb", "--maxmemory-policy", "allkeys-lru", "--save", "", "--appendonly", "no"]
networks:
- backend
restart: unless-stopped
mem_limit: 256m
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 10
trino:
image: trinodb/trino:484
env_file:
- ${PETROLYST_ROOT:-/opt/petrolyst}/env/fuel-monitoring.env
volumes:
- ./trino/etc:/etc/trino:ro
labels:
# Compose cannot see content changes in a bind mount; this label changes
# with trino/etc, so exactly a config change recreates the container.
petrolyst.config-hash: ${TRINO_CONFIG_HASH:?TRINO_CONFIG_HASH unset - run through deploy/deploy.sh}
depends_on:
fuel-db:
condition: service_healthy
networks:
- backend
restart: unless-stopped
mem_limit: 3g
# The image's own healthcheck (/usr/lib/trino/bin/health-check) applies.
fuel-db:
image: timescale/timescaledb:2.31.0-pg17
environment:
POSTGRES_USER: postgres
POSTGRES_DB: fuel
POSTGRES_PASSWORD_FILE: /run/secrets/fuel_db_password
TS_TUNE_NUM_CPUS: "2"
secrets:
- fuel_db_password
- fuel_db_trino_password
volumes:
- fuel_db_data:/var/lib/postgresql/data
# A single file, not the folder: the image's own TimescaleDB init scripts
# live in the same directory and must stay visible.
- ./fuel-db/init/01-roles.sh:/docker-entrypoint-initdb.d/010_petrolyst_roles.sh:ro
networks:
- backend
- fuel-db-tunnel
# Loopback only, for `ssh -L 5433:127.0.0.1:5433 petrolyst`.
ports:
- "127.0.0.1:5433:5432"
restart: unless-stopped
mem_limit: 1536m
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d fuel"]
interval: 10s
timeout: 5s
retries: 10
networks:
backend:
internal: true
proxy:
external: true
# A port cannot be published from an internal network; this one exists only
# so fuel-db's loopback port works.
fuel-db-tunnel: {}
volumes:
superset_db_data:
fuel_db_data:
secrets:
superset_secret_key:
file: ${PETROLYST_ROOT:-/opt/petrolyst}/secrets/fuel-monitoring/superset_secret_key
superset_db_password:
file: ${PETROLYST_ROOT:-/opt/petrolyst}/secrets/fuel-monitoring/superset_db_password
fuel_db_password:
file: ${PETROLYST_ROOT:-/opt/petrolyst}/secrets/fuel-monitoring/fuel_db_password
fuel_db_trino_password:
file: ${PETROLYST_ROOT:-/opt/petrolyst}/secrets/fuel-monitoring/fuel_db_trino_password
keycloak_petrolyst_client_secret:
file: ${PETROLYST_ROOT:-/opt/petrolyst}/secrets/shared/keycloak_petrolyst_client_secret
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# Runs once, when the fuel-db volume is first initialised (mounted after the
# image's own TimescaleDB scripts, as 010_petrolyst_roles.sh).
# fuel_owner owns database "fuel", its public schema and every table in it.
# Loaders log in as their own role but act as fuel_owner (README
# "Data loaders"): the default privileges below cover only what
# fuel_owner itself creates, not what its members create.
# trino_ro what Trino connects as: read-only, also on tables created later.
set -euo pipefail
trino_password=$(cat /run/secrets/fuel_db_trino_password)
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" \
-v trino_password="$trino_password" <<'SQL'
CREATE ROLE fuel_owner NOLOGIN;
ALTER DATABASE fuel OWNER TO fuel_owner;
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
CREATE ROLE trino_ro LOGIN PASSWORD :'trino_password';
GRANT CONNECT ON DATABASE fuel TO trino_ro;
GRANT USAGE ON SCHEMA public TO trino_ro;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO trino_ro;
ALTER DEFAULT PRIVILEGES FOR ROLE fuel_owner IN SCHEMA public
GRANT SELECT ON TABLES TO trino_ro;
SQL
@@ -0,0 +1,28 @@
# Petrolyst Superset image. Deployment only: every customization is copied from
# petrolyst-insights/, which compose.yaml passes as the named build context
# "insights". deploy/deploy.sh tags the result with a hash of exactly these
# inputs (this file plus what is copied below), so the tag changes only when
# the image would.
FROM apache/superset:6.1.0@sha256:16b50bbef6648912a79e3293d418fefa743ce555d36c5af6b85d859119ed7f88
USER root
# The 6.1.0 image ships neither an OAuth client nor the Postgres and Trino
# drivers.
RUN uv pip install --no-cache --python /app/.venv/bin/python \
authlib==1.9.1 psycopg2-binary==2.9.13 "trino[sqlalchemy]==0.340.0"
COPY --from=insights --chown=superset:superset superset_config.py petrolyst_security.py /app/pythonpath/
COPY --from=insights --chown=superset:superset assets/branding/ /app/superset/static/assets/branding/
COPY --from=insights patches/ /app/patches/petrolyst/
# Stop at the first patch or verify that fails, so a later success cannot hide
# an earlier failure.
RUN for p in /app/patches/petrolyst/fix_*.py; do \
[ -e "$p" ] || continue; \
{ python3 "$p" && python3 "$p" --verify; } || exit 1; \
done
# Deployment, not customization: in the image (and so in the tag) rather than
# bind-mounted, so a change to it makes deploy.sh run superset-init again.
COPY init.sh /app/petrolyst-init.sh
USER superset
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# superset-init: migrate the metadata database, sync roles and permissions, and
# register the Trino connection. It is copied into the Superset image, so a
# change here changes the image tag; deploy/deploy.sh runs it on a fresh install
# and whenever that tag changes. Every step is idempotent. Unlike the
# upstream docker-init.sh it never creates a local admin: users come from
# Keycloak.
set -euo pipefail
superset db upgrade
superset init
superset set-database-uri \
--database_name "Fuel (Trino)" \
--uri "trino://superset@trino:8080/fuel"
@@ -0,0 +1,3 @@
# Analytics never writes through Trino. fuel-db's trino_ro role is read-only as
# well; this is the second lock.
access-control.name=read-only
@@ -0,0 +1,5 @@
# The fuel-monitoring operational store (service fuel-db), read-only.
connector.name=postgresql
connection-url=jdbc:postgresql://fuel-db:5432/fuel
connection-user=trino_ro
connection-password=${ENV:FUEL_DB_TRINO_PASSWORD}
@@ -0,0 +1,14 @@
# Single node: coordinator and worker in one process. Reachable only on the
# backend network (Superset); no port is published and Traefik has no route.
coordinator=true
node-scheduler.include-coordinator=true
http-server.http.port=8080
discovery.uri=http://localhost:8080
catalog.management=static
# Memory bounds. The container is capped at 3g (compose.yaml) and jvm.config
# gives the heap 75 % of it (~2.25 GB); 1 GB for queries plus 512 MB headroom
# leaves room for the rest of the heap.
query.max-memory=1GB
query.max-memory-per-node=1GB
memory.heap-headroom-per-node=512MB
@@ -0,0 +1,14 @@
-server
-agentpath:/usr/lib/trino/bin/libjvmkill.so
-XX:InitialRAMPercentage=75
-XX:MaxRAMPercentage=75
-XX:G1HeapRegionSize=32M
-XX:+ExplicitGCInvokesConcurrent
-XX:+HeapDumpOnOutOfMemoryError
-XX:+ExitOnOutOfMemoryError
-XX:-OmitStackTraceInFastThrow
-XX:ReservedCodeCacheSize=256M
-XX:PerMethodRecompilationCutoff=10000
-XX:PerBytecodeRecompilationCutoff=10000
-Djdk.attach.allowAttachSelf=true
-Djdk.nio.maxCachedBufferSize=2000000
@@ -0,0 +1 @@
io.trino=INFO
@@ -0,0 +1,3 @@
node.environment=petrolyst
node.data-dir=/data/trino
node.id=${ENV:HOSTNAME}