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

+105
View File
@@ -0,0 +1,105 @@
#!/usr/bin/env bash
# One-time host preparation for the petrolyst server (Ubuntu 24.04), run as root.
# Idempotent: re-running converges to the same state.
#
# bootstrap.sh --ci-key <public-key-file> --runner-ip <ip|any>
#
# --ci-key public half of the dedicated CI deploy key (DEPLOY_SSH_KEY in Gitea)
# --runner-ip egress IP of the Gitea act_runner; the deploy key works only from
# there ("any" drops that restriction, e.g. before the runner exists)
set -euo pipefail
die() { echo "ERROR: $*" >&2; exit 1; }
[ "$(id -u)" = 0 ] || die "run as root"
ci_key='' runner_ip=''
while [ $# -gt 0 ]; do
case $1 in
--ci-key) ci_key=$2; shift 2 ;;
--runner-ip) runner_ip=$2; shift 2 ;;
*) die "unknown argument $1" ;;
esac
done
[ -s "$ci_key" ] || die "--ci-key <public-key-file> is required"
[ -n "$runner_ip" ] || die "--runner-ip <ip|any> is required"
grep -qE '^ssh-(ed25519|rsa) ' "$ci_key" || die "$ci_key does not look like an SSH public key"
ROOT=/opt/petrolyst
echo "== packages and Docker (official apt repository)"
apt-get update -q
apt-get install -y -q ca-certificates curl rsync ufw
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
# shellcheck source=/dev/null
codename=$(. /etc/os-release && echo "$VERSION_CODENAME")
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $codename stable" \
>/etc/apt/sources.list.d/docker.list
apt-get update -q
apt-get install -y -q docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
daemon_json='{
"log-driver": "json-file",
"log-opts": { "max-size": "10m", "max-file": "3" },
"live-restore": true
}'
if [ "$(cat /etc/docker/daemon.json 2>/dev/null)" != "$daemon_json" ]; then
printf '%s\n' "$daemon_json" >/etc/docker/daemon.json
systemctl restart docker
fi
systemctl enable --now docker
echo "== swap (4 GB) for Trino, Superset and two Postgres in 7.8 GB"
if ! swapon --show=NAME --noheadings | grep -qx /swapfile; then
[ -e /swapfile ] || fallocate -l 4G /swapfile
chmod 600 /swapfile
mkswap /swapfile >/dev/null
swapon /swapfile
fi
grep -q '^/swapfile ' /etc/fstab || echo '/swapfile none swap sw 0 0' >>/etc/fstab
echo 'vm.swappiness=10' >/etc/sysctl.d/99-petrolyst.conf
sysctl -q -p /etc/sysctl.d/99-petrolyst.conf
echo "== firewall: 22, 80, 443 only"
# Docker-published ports bypass ufw. Only Traefik publishes publicly; every
# other published port in the stacks binds to 127.0.0.1.
ufw default deny incoming >/dev/null
ufw default allow outgoing >/dev/null
for port in 22 80 443; do ufw allow "$port/tcp" >/dev/null; done
ufw --force enable >/dev/null
echo "== deploy user (CI)"
id deploy >/dev/null 2>&1 || useradd --create-home --shell /bin/bash deploy
usermod -aG docker deploy # docker access makes deploy root-equivalent; the key restrictions below matter
install -d -o deploy -g deploy -m 0700 /home/deploy/.ssh
options=restrict
[ "$runner_ip" = any ] || options="restrict,from=\"$runner_ip\""
printf '%s %s\n' "$options" "$(cat "$ci_key")" >/home/deploy/.ssh/authorized_keys
chown deploy:deploy /home/deploy/.ssh/authorized_keys
chmod 0600 /home/deploy/.ssh/authorized_keys
echo "== SSH: keys only"
[ -s /root/.ssh/authorized_keys ] || die "root has no authorized_keys; refusing to turn off password login"
# 00- sorts before cloud-init's 50-cloud-init.conf, and sshd keeps the first
# value it reads, so these win over its PasswordAuthentication yes.
cat >/etc/ssh/sshd_config.d/00-petrolyst.conf <<'EOF'
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin prohibit-password
EOF
sshd -t
systemctl reload ssh
echo "== /opt/petrolyst"
install -d -o root -g root -m 0755 "$ROOT"
install -d -o deploy -g deploy -m 0755 "$ROOT/infra" "$ROOT/releases" "$ROOT/incoming" "$ROOT/state"
install -d -o deploy -g deploy -m 0750 "$ROOT/backups"
[ -e "$ROOT/deploy.lock" ] || install -o deploy -g deploy -m 0640 /dev/null "$ROOT/deploy.lock"
docker network inspect proxy >/dev/null 2>&1 || docker network create proxy >/dev/null
echo
echo "Done. Before closing this session, check from another terminal that"
echo " ssh petrolyst still logs you in as root, and"
echo " sshd -T | grep -E 'passwordauthentication|permitrootlogin' shows no / prohibit-password."
echo "Next: host/secrets-init.sh"
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env bash
# Server secrets and env files, run as root after bootstrap.sh.
# secrets-init.sh generate every missing secret; never overwrites one
# secrets-init.sh keycloak-secret store the client secret of Keycloak client
# "petrolyst" (Clients > petrolyst > Credentials),
# read from the terminal or stdin
#
# Layout: /opt/petrolyst/secrets/{shared,traefik,fuel-monitoring}/ are root:deploy
# 0750, so only root and the deploy user can reach them; the files are 0444
# because each container reads them under its own uid.
set -euo pipefail
die() { echo "ERROR: $*" >&2; exit 1; }
[ "$(id -u)" = 0 ] || die "run as root"
getent group deploy >/dev/null || die "group deploy is missing: run host/bootstrap.sh first"
ROOT=/opt/petrolyst
SECRETS=$ROOT/secrets
umask 077
install -d -o root -g deploy -m 0750 "$SECRETS" "$SECRETS/shared" "$SECRETS/traefik" "$SECRETS/fuel-monitoring" "$ROOT/env"
# write_secret <path> <value>: no trailing newline (oauth2-proxy reads the file
# byte for byte), replaced atomically.
write_secret() {
printf '%s' "$2" >"$1.tmp"
chown root:deploy "$1.tmp"
chmod 0444 "$1.tmp"
mv "$1.tmp" "$1"
}
generate() {
if [ -s "$1" ]; then
echo "kept $1"
else
write_secret "$1" "$(openssl rand -hex "$2")"
echo "generated $1"
fi
}
case ${1:-all} in
keycloak-secret)
if [ -t 0 ]; then
printf 'Client secret of Keycloak client "petrolyst": ' >&2
IFS= read -rs value
echo >&2
else
IFS= read -r value
fi
value=$(printf '%s' "$value" | tr -d '[:space:]')
[ -n "$value" ] || die "empty secret"
write_secret "$SECRETS/shared/keycloak_petrolyst_client_secret" "$value"
echo "stored $SECRETS/shared/keycloak_petrolyst_client_secret"
exit 0
;;
all) ;;
*) die "usage: secrets-init.sh [keycloak-secret]" ;;
esac
# Hex only, so no value ever needs quoting in a URL or a properties file.
generate "$SECRETS/fuel-monitoring/superset_secret_key" 32
generate "$SECRETS/fuel-monitoring/superset_db_password" 32
generate "$SECRETS/fuel-monitoring/fuel_db_password" 32
generate "$SECRETS/fuel-monitoring/fuel_db_trino_password" 32
# 32 hex characters = a 32-byte cookie key, as oauth2-proxy requires.
generate "$SECRETS/traefik/oauth2_proxy_cookie_secret" 16
# Trino reads the catalog password from its environment (${ENV:...}); keep the
# env file in step with the secret file that fuel-db's init script uses.
env_file=$ROOT/env/fuel-monitoring.env
line="FUEL_DB_TRINO_PASSWORD=$(cat "$SECRETS/fuel-monitoring/fuel_db_trino_password")"
if ! grep -qxF "$line" "$env_file" 2>/dev/null; then
printf '%s\n' "$line" >"$env_file.tmp"
chown root:deploy "$env_file.tmp"
chmod 0440 "$env_file.tmp"
mv "$env_file.tmp" "$env_file"
echo "wrote $env_file"
fi
if [ ! -s "$SECRETS/shared/keycloak_petrolyst_client_secret" ]; then
echo
echo "Still missing: the Keycloak client secret. Copy it from Keycloak"
echo "(realm thedigitalbridge > Clients > petrolyst > Credentials), then run:"
echo " $0 keycloak-secret"
exit 1
fi
echo "All secrets present."