#!/bin/bash # To be run by user wlfb to create pod for the Finanzberatungs-Tool # (API + Postgres + Grafana). set -e # Environment variables POD_NAME='finance_pod' DB_CTR_NAME='finance-db_ctr' API_CTR_NAME='finance-api_ctr' GRAFANA_CTR_NAME='finance-grafana_ctr' # All image tags are pinned to fixed, immutable versions (no floating tags # such as :latest, :3 or :16). Verified against the upstream registries on # 2026-07-17 via `podman pull` (both tags exist as-is; no substitution # needed). POSTGRES_IMAGE='docker.io/library/postgres:17.10' GRAFANA_IMAGE='docker.io/grafana/grafana-oss:12.1.0' API_IMAGE='localhost/finance-api:0.1.0' HOST_LOCAL_IP='127.0.0.1' # Expose API on 8096 -> 8000 API_HOST_PORT='8096' API_CONTAINER_PORT='8000' # Expose Grafana on 8097 -> 3000 GRAFANA_HOST_PORT='8097' GRAFANA_CONTAINER_PORT='3000' # Postgres is only reachable inside the pod (localhost:5432 from API/Grafana) BIND_DIR="$HOME/.local/share/$POD_NAME" POSTGRES_DATA_DIR="$BIND_DIR/postgres-data" GRAFANA_DATA_DIR="$BIND_DIR/grafana-data" INBOX_DIR="$BIND_DIR/inbox" UPLOADS_DIR="$BIND_DIR/uploads" USER_SYSTEMD_DIR="$HOME/.config/systemd/user" FINANCE_DIR="$HOME/bin/finance" ENV_FILE="$FINANCE_DIR/.env" GRAFANA_PROVISIONING_DIR="$FINANCE_DIR/grafana/provisioning" GRAFANA_DASHBOARDS_DIR="$FINANCE_DIR/grafana/dashboards" # --- Secrets bootstrap ------------------------------------------------------- # On first run, generate all secrets into finance/.env (gitignored) and print # the GUI login password once. On subsequent runs the existing .env is reused # unchanged, so re-running this script never rotates secrets under existing # data. if [ ! -f "$ENV_FILE" ]; then GUI_PASSWORD=$(openssl rand -base64 12) cat > "$ENV_FILE" </dev/null | \ grep -q "pod-$POD_NAME.service"; then systemctl --user stop "pod-$POD_NAME.service" || true fi # Prepare directories mkdir -p "$POSTGRES_DATA_DIR" "$GRAFANA_DATA_DIR" "$INBOX_DIR" "$UPLOADS_DIR" \ "$USER_SYSTEMD_DIR" # The Grafana image runs as a non-root user (uid 472) that belongs to group 0 # (root). Its data dir is bind-mounted from the host, where it is owned by the # (rootless) container root. Make the dir group-writable with the setgid bit # so the non-root Grafana process can write its sqlite db/plugins (same # "arbitrary uid + gid 0" pattern as example_create_pod_langflow.sh's # LANGFLOW_DATA_DIR fix). chmod g+rwxs "$GRAFANA_DATA_DIR" # Build the API image (Task 15 Containerfile) podman build -t "$API_IMAGE" "$FINANCE_DIR" # Create pod if not yet existing if ! podman pod exists "$POD_NAME"; then podman pod create -n "$POD_NAME" \ -p "$HOST_LOCAL_IP:$API_HOST_PORT:$API_CONTAINER_PORT" \ -p "$HOST_LOCAL_IP:$GRAFANA_HOST_PORT:$GRAFANA_CONTAINER_PORT" echo "Pod '$POD_NAME' created (rc=$?)" else echo "Pod '$POD_NAME' already exists." fi # Remove any old containers (ignore errors if they don't exist) podman rm -f "$DB_CTR_NAME" || true podman rm -f "$API_CTR_NAME" || true podman rm -f "$GRAFANA_CTR_NAME" || true # Postgres container podman run -d --name "$DB_CTR_NAME" --pod "$POD_NAME" \ -e POSTGRES_USER=finance \ -e POSTGRES_PASSWORD="$POSTGRES_PASSWORD" \ -e POSTGRES_DB=finance \ -v "$POSTGRES_DATA_DIR:/var/lib/postgresql/data:Z" \ "$POSTGRES_IMAGE" echo "Container '$DB_CTR_NAME' started (rc=$?)" # Wait for Postgres to be ready before starting API / Grafana echo "Waiting for Postgres to be ready (pg_isready)..." for attempt in $(seq 1 30); do if podman exec "$DB_CTR_NAME" pg_isready -q >/dev/null 2>&1; then echo "Postgres is ready." break fi sleep 2 if [ "$attempt" -eq 30 ]; then echo "ERROR: Postgres did not become ready in time." >&2 exit 1 fi done # Create the read-only role used by the Grafana datasource, idempotently. echo "Ensuring read-only role 'finance_read' exists..." podman exec "$DB_CTR_NAME" psql -U finance -d finance -c \ "DO \$\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname='finance_read') THEN CREATE ROLE finance_read LOGIN PASSWORD '$FINANCE_READ_PASSWORD'; END IF; END \$\$; GRANT CONNECT ON DATABASE finance TO finance_read; GRANT USAGE ON SCHEMA public TO finance_read; GRANT SELECT ON ALL TABLES IN SCHEMA public TO finance_read; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO finance_read;" echo "Role 'finance_read' is ready." # API container (runs alembic upgrade head on start via entrypoint.sh) podman run -d --name "$API_CTR_NAME" --pod "$POD_NAME" \ -e FB_DATABASE_URL="postgresql+psycopg://finance:$POSTGRES_PASSWORD@localhost:5432/finance" \ -e FB_API_KEY \ -e FB_SESSION_SECRET \ -e FB_GUI_USER \ -e FB_GUI_PASSWORD_HASH \ -e FB_INBOX_DIR=/data/inbox \ -e FB_UPLOADS_DIR=/data/uploads \ -v "$INBOX_DIR:/data/inbox:Z" \ -v "$UPLOADS_DIR:/data/uploads:Z" \ "$API_IMAGE" echo "Container '$API_CTR_NAME' started (rc=$?)" # Grafana container (provisioning + dashboards mounted read-only; datasource # yaml interpolates FINANCE_READ_PASSWORD) podman run -d --name "$GRAFANA_CTR_NAME" --pod "$POD_NAME" \ -e GF_SECURITY_ADMIN_PASSWORD="$GRAFANA_ADMIN_PASSWORD" \ -e FINANCE_READ_PASSWORD \ -v "$GRAFANA_PROVISIONING_DIR:/etc/grafana/provisioning:Z,ro" \ -v "$GRAFANA_DASHBOARDS_DIR:/var/lib/grafana/dashboards:Z,ro" \ -v "$GRAFANA_DATA_DIR:/var/lib/grafana:Z" \ "$GRAFANA_IMAGE" echo "Container '$GRAFANA_CTR_NAME' started (rc=$?)" # Generate systemd service files cd "$USER_SYSTEMD_DIR" podman generate systemd --name --new --files "$POD_NAME" echo "Generated systemd service files (rc=$?)" # Stop & remove live pod and containers podman pod stop --ignore --time 15 "$POD_NAME" podman pod rm -f --ignore "$POD_NAME" if podman pod exists "$POD_NAME"; then echo "ERROR: Pod $POD_NAME still exists." >&2 exit 1 else echo "Stopped & removed live pod $POD_NAME and containers." fi # Enable systemd user services systemctl --user daemon-reload # pod service (creates pod + containers) systemctl --user enable --now "pod-${POD_NAME}.service" systemctl --user is-enabled "pod-${POD_NAME}.service" systemctl --user is-active "pod-${POD_NAME}.service" echo "Enabled systemd service pod-${POD_NAME}.service (rc=$?)" echo "To view status: systemctl --user status pod-${POD_NAME}.service" echo "To view logs: journalctl --user -u pod-${POD_NAME}.service -f" # Wait for API and Grafana readiness CHECK_URL_API="http://$HOST_LOCAL_IP:$API_HOST_PORT/login" CHECK_URL_GRAFANA="http://$HOST_LOCAL_IP:$GRAFANA_HOST_PORT/api/health" for attempt in $(seq 1 30); do API_CODE=$(curl -s -o /dev/null -w '%{http_code}' "$CHECK_URL_API" || true) GRAFANA_CODE=$(curl -s -o /dev/null -w '%{http_code}' "$CHECK_URL_GRAFANA" || true) if [ "$API_CODE" = "200" ] && [ "$GRAFANA_CODE" = "200" ]; then echo "API is reachable at $CHECK_URL_API (200)." echo "Grafana is reachable at $CHECK_URL_GRAFANA (200)." break fi sleep 2 if [ "$attempt" -eq 30 ]; then echo "timeout error (API=$API_CODE, Grafana=$GRAFANA_CODE)." >&2 exit 1 fi done