From da35fc0e1230509533428116b6bd5d4f4e2820367b054dab41728e2bd19d7bab Mon Sep 17 00:00:00 2001 From: tlg Date: Tue, 23 Jun 2026 20:27:51 +0200 Subject: [PATCH] Initial commit: pod setup and shell scripts Add create_pod_langflow.sh (Langflow + Langfuse Podman pod with pinned image versions), set-colors.sh, .gitignore, and local .claude config. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/settings.local.json | 16 ++ .gitignore | 50 ++++++ create_pod_langflow.sh | 322 ++++++++++++++++++++++++++++++++++++ set-colors.sh | 5 + 4 files changed, 393 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 .gitignore create mode 100755 create_pod_langflow.sh create mode 100755 set-colors.sh diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..c39a2ad --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,16 @@ +{ + "permissions": { + "allow": [ + "Bash(grep -o 'workspace.\\\\{0,40\\\\}current_dir.\\\\{0,300\\\\}')", + "Bash(grep -o '.\\\\{420\\\\}exceeds_200k_tokens:t,fast_mode')", + "Bash(grep -oE 'five_hour:\\\\{[^}]{0,200}')", + "Bash(grep -oE '\\(five_hour|seven_day|used_pct|utilization|resets_at|status\\)[: ]\\\\{?[^,}]{0,60}')", + "Bash(grep -oE 'five_hour:\\\\{[^@]{0,400}')", + "Bash(grep -oE '.{0,20}\\(five_hour|seven_day\\):\\\\{[^{}]*\\\\}')", + "Bash(grep -o 'five_hour:{used_percentage[^Z]\\\\{0,260\\\\}')", + "Bash(grep -oE 'function s\\\\$f\\\\\\([^\\)]*\\\\\\)\\\\{[^@]{0,300}')", + "Bash(grep -oE '\\(used_tokens|total_tokens|remaining_tokens|used_percent|max_tokens|compaction\\)[: ][^,}]{0,40}')", + "Bash(grep -oE 'function u_n\\\\\\([^\\)]*\\\\\\)\\\\{[^@]{0,200}')" + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ce0217d --- /dev/null +++ b/.gitignore @@ -0,0 +1,50 @@ +# --- OS / filesystem cruft --- +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +Thumbs.db +desktop.ini + +# --- Editor / IDE --- +*~ +*.swp +*.swo +.vscode/ +.idea/ +*.sublime-workspace + +# --- Backups & patch leftovers --- +*.bak +*.bak-* +*.bak[0-9]* +*.backup +*.orig +*.rej +*.tmp + +# --- Logs --- +*.log + +# --- Secrets / credentials (never commit) --- +.env +.env.* +*.pem +*.key +id_rsa +id_ed25519 +*_secret* +*.secret +credentials +secrets/ + +# --- Python (scripts may bring these) --- +__pycache__/ +*.py[cod] +.venv/ +venv/ + +# --- Local scratch --- +/tmp/ +scratchpad/ diff --git a/create_pod_langflow.sh b/create_pod_langflow.sh new file mode 100755 index 0000000..38538e3 --- /dev/null +++ b/create_pod_langflow.sh @@ -0,0 +1,322 @@ +#!/bin/bash + +# To be run by user lwc to create pod for Langflow, Langfuse and their tools + +set -e + +# Environment variables +POD_NAME='langflow_pod' +LANGFLOW_CTR_NAME='langflow_ctr' +POSTGRES_CTR_NAME='postgres_ctr' +LANGFUSE_WEB_CTR_NAME='langfuse-web_ctr' +LANGFUSE_WORKER_CTR_NAME='langfuse-worker_ctr' +CLICKHOUSE_CTR_NAME='clickhouse_ctr' +MINIO_CTR_NAME='minio_ctr' +REDIS_CTR_NAME='redis_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-06-23. Langflow 1.10.0 supports the MCP server (flows-as-tools). +LANGFLOW_IMAGE='docker.io/langflowai/langflow:1.10.0' +POSTGRES_IMAGE='docker.io/library/postgres:17.10' +LANGFUSE_WEB_IMAGE='docker.io/langfuse/langfuse:3.195.0' +LANGFUSE_WORKER_IMAGE='docker.io/langfuse/langfuse-worker:3.195.0' +# ClickHouse pinned to the latest patch of the 25.11 minor (>= Langfuse's +# minimum supported 24.3); 26.x is intentionally avoided as too bleeding-edge. +CLICKHOUSE_IMAGE='docker.io/clickhouse/clickhouse-server:25.11.9.34' +# Using official MinIO image here because the Chainguard image used in Langfuse +# docker-compose example does not ship +# a shell, but this script relies on a small shell wrapper to pre-create the +# langfuse bucket. And Chainguard offers only tag :latest. +MINIO_IMAGE='docker.io/minio/minio:RELEASE.2025-09-07T16-13-09Z' +REDIS_IMAGE='docker.io/redis:7.4.9' + +HOST_LOCAL_IP='127.0.0.1' +# Expose Langflow on 8090 -> 7860 +LANGFLOW_HOST_PORT='8090' +LANGFLOW_CONTAINER_PORT='7860' +# Expose Langfuse Web on 8091 -> 3000 +LANGFUSE_HOST_PORT='8091' +LANGFUSE_CONTAINER_PORT='3000' +# MinIO API/console ports inside the pod (avoid conflict with ClickHouse native port 9000) +MINIO_API_PORT='9100' +MINIO_CONSOLE_PORT='9101' +# All other services are only reachable inside the pod + +BIND_DIR="$HOME/.local/share/$POD_NAME" +LANGFLOW_DATA_DIR="$BIND_DIR/langflow-data" +POSTGRES_DATA_DIR="$BIND_DIR/postgres-data" +LANGFUSE_CLICKHOUSE_DATA_DIR="$BIND_DIR/langfuse-clickhouse-data" +LANGFUSE_CLICKHOUSE_LOGS_DIR="$BIND_DIR/langfuse-clickhouse-logs" +LANGFUSE_MINIO_DATA_DIR="$BIND_DIR/langfuse-minio-data" +USER_SYSTEMD_DIR="$HOME/.config/systemd/user" + +LANGFUSE_PUBLIC_URL="http://$HOST_LOCAL_IP:$LANGFUSE_HOST_PORT" +LANGFLOW_DB_URL='postgresql://langflow:langflow@localhost:5432/langflow' + +# --- Test / integration configuration --------------------------------------- +# Langfuse is bootstrapped headlessly with a fixed org/project/user and a fixed +# API key pair (see LANGFUSE_INIT_* below). This makes the environment usable +# for automation out of the box. Change or blank these for a clean UI signup. +LANGFUSE_INIT_ORG="test-org" +LANGFUSE_INIT_PROJECT="test-project" +LANGFUSE_INIT_PUBLIC_KEY="pk-lf-00000000-0000-0000-0000-000000000001" +LANGFUSE_INIT_SECRET_KEY="sk-lf-00000000-0000-0000-0000-000000000002" +LANGFUSE_INIT_EMAIL="admin@example.com" +LANGFUSE_INIT_PASSWORD="langfuse-admin-pw" +# Langflow sends its run traces to the in-pod Langfuse using the keys above. +# NOTE: the Langfuse web (Next.js) server binds to the pod's hostname/IP, NOT +# 127.0.0.1, so Langflow must address it via the pod name (which every pod +# container resolves to the pod IP through /etc/hosts), not "localhost". +LANGFUSE_INPOD_HOST="http://$POD_NAME:$LANGFUSE_CONTAINER_PORT" +# Local OpenAI-compatible (vLLM) inference endpoint on the host, reachable from +# inside the pod via the podman host gateway alias. +LOCAL_LLM_BASE_URL="http://host.containers.internal:8081/v1" +LOCAL_LLM_MODEL="AxionML/Qwen3.5-9B-NVFP4" + +LANGFUSE_COMMON_ENV=( + -e NEXTAUTH_URL="$LANGFUSE_PUBLIC_URL" + -e DATABASE_URL="postgresql://langflow:langflow@localhost:5432/langfuse" + -e SALT="qwexoiuzxequtbfvyqewregvg" + -e ENCRYPTION_KEY="57db4291f0243a9f6bbbf82d3fbca1a72c58fd42f852a4d74e3db8b55bee720a" + -e TELEMETRY_ENABLED=true + -e LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=true + -e CLICKHOUSE_MIGRATION_URL="clickhouse://localhost:9000" + -e CLICKHOUSE_URL="http://localhost:8123" + -e CLICKHOUSE_USER="clickhouse" + -e CLICKHOUSE_PASSWORD="clickhouse" + -e CLICKHOUSE_CLUSTER_ENABLED=false + -e LANGFUSE_USE_AZURE_BLOB=false + -e LANGFUSE_S3_EVENT_UPLOAD_BUCKET="langfuse" + -e LANGFUSE_S3_EVENT_UPLOAD_REGION="auto" + -e LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID="minio" + -e LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY="miniosecret" + -e LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT="http://localhost:$MINIO_API_PORT" + -e LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE=true + -e LANGFUSE_S3_EVENT_UPLOAD_PREFIX="events/" + -e LANGFUSE_S3_MEDIA_UPLOAD_BUCKET="langfuse" + -e LANGFUSE_S3_MEDIA_UPLOAD_REGION="auto" + -e LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID="minio" + -e LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY="miniosecret" + -e LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT="http://localhost:$MINIO_API_PORT" + -e LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE=true + -e LANGFUSE_S3_MEDIA_UPLOAD_PREFIX="media/" + -e LANGFUSE_S3_BATCH_EXPORT_ENABLED=false + -e LANGFUSE_S3_BATCH_EXPORT_BUCKET="langfuse" + -e LANGFUSE_S3_BATCH_EXPORT_PREFIX="exports/" + -e LANGFUSE_S3_BATCH_EXPORT_REGION="auto" + -e LANGFUSE_S3_BATCH_EXPORT_ENDPOINT="http://localhost:$MINIO_API_PORT" + -e LANGFUSE_S3_BATCH_EXPORT_EXTERNAL_ENDPOINT="$LANGFUSE_PUBLIC_URL" + -e LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID="minio" + -e LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY="miniosecret" + -e LANGFUSE_S3_BATCH_EXPORT_FORCE_PATH_STYLE=true + -e LANGFUSE_INGESTION_QUEUE_DELAY_MS= + -e LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS= + -e REDIS_HOST="localhost" + -e REDIS_PORT=6379 + -e REDIS_AUTH="myredissecret" + -e REDIS_TLS_ENABLED=false + -e REDIS_TLS_CA="/certs/ca.crt" + -e REDIS_TLS_CERT="/certs/redis.crt" + -e REDIS_TLS_KEY="/certs/redis.key" + -e EMAIL_FROM_ADDRESS= + -e SMTP_CONNECTION_URL= +) + +# Stop existing systemd-managed pod if present, to avoid conflicts on rerun +echo "Stopping systemd-managed pod 'pod-$POD_NAME.service' if it exists..." +if systemctl --user list-units --type=service --all 2>/dev/null | \ + grep -q "pod-$POD_NAME.service"; then + systemctl --user stop "pod-$POD_NAME.service" || true +fi + +# Prepare directories +mkdir -p "$LANGFLOW_DATA_DIR" "$POSTGRES_DATA_DIR" \ + "$LANGFUSE_CLICKHOUSE_DATA_DIR" "$LANGFUSE_CLICKHOUSE_LOGS_DIR" \ + "$LANGFUSE_MINIO_DATA_DIR" "$USER_SYSTEMD_DIR" + +# The Langflow image (>= 1.10) runs as a non-root user (uid 1000) that belongs +# to group 0 (root). Its config 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 Langflow process can write its config/log files +# (the "arbitrary uid + gid 0" pattern the image is built for). +chmod g+rwxs "$LANGFLOW_DATA_DIR" + +# Create pod if not yet existing +if ! podman pod exists "$POD_NAME"; then + podman pod create -n "$POD_NAME" \ + -p "$HOST_LOCAL_IP:$LANGFLOW_HOST_PORT:$LANGFLOW_CONTAINER_PORT" \ + -p "$HOST_LOCAL_IP:$LANGFUSE_HOST_PORT:$LANGFUSE_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 "$LANGFLOW_CTR_NAME" || true +podman rm -f "$POSTGRES_CTR_NAME" || true +podman rm -f "$LANGFUSE_WEB_CTR_NAME" || true +podman rm -f "$LANGFUSE_WORKER_CTR_NAME" || true +podman rm -f "$CLICKHOUSE_CTR_NAME" || true +podman rm -f "$MINIO_CTR_NAME" || true +podman rm -f "$REDIS_CTR_NAME" || true + +# Postgres container (shared by Langflow and Langfuse) +podman run -d --name "$POSTGRES_CTR_NAME" --pod "$POD_NAME" \ + -e POSTGRES_USER=langflow \ + -e POSTGRES_PASSWORD=langflow \ + -e POSTGRES_DB=langflow \ + -v "$POSTGRES_DATA_DIR:/var/lib/postgresql/data:Z" \ + "$POSTGRES_IMAGE" +echo "Container '$POSTGRES_CTR_NAME' started (rc=$?)" +# Wait for Postgres to be ready before starting Langflow / Langfuse +echo "Waiting for Postgres to be ready (pg_isready)..." +for attempt in $(seq 1 30); do + if podman exec "$POSTGRES_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 + +# Ensure separate database for Langfuse in the shared Postgres instance +echo "Ensuring 'langfuse' database exists for Langfuse..." +if ! podman exec "$POSTGRES_CTR_NAME" psql -U langflow -d postgres \ + -tc "SELECT 1 FROM pg_database WHERE datname = 'langfuse';" | grep -q 1; then + podman exec "$POSTGRES_CTR_NAME" psql -U langflow -d postgres \ + -c "CREATE DATABASE langfuse OWNER langflow;" +fi +echo "Database 'langfuse' is ready for Langfuse." + +# Clickhouse container (Langfuse analytics store) +podman run -d --name "$CLICKHOUSE_CTR_NAME" --pod "$POD_NAME" \ + -e CLICKHOUSE_DB=default \ + -e CLICKHOUSE_USER=clickhouse \ + -e CLICKHOUSE_PASSWORD=clickhouse \ + -v "$LANGFUSE_CLICKHOUSE_DATA_DIR:/var/lib/clickhouse:Z" \ + -v "$LANGFUSE_CLICKHOUSE_LOGS_DIR:/var/log/clickhouse-server:Z" \ + "$CLICKHOUSE_IMAGE" +echo "Container '$CLICKHOUSE_CTR_NAME' started (rc=$?)" + +# Minio container (S3-compatible storage for Langfuse) +# NOTE: The official MinIO image uses the `minio` binary as entrypoint. +# We call `server` directly as the container command. +podman run -d --name "$MINIO_CTR_NAME" --pod "$POD_NAME" \ + -e MINIO_ROOT_USER=minio \ + -e MINIO_ROOT_PASSWORD=miniosecret \ + -v "$LANGFUSE_MINIO_DATA_DIR:/data:Z" \ + "$MINIO_IMAGE" \ + server --address ":$MINIO_API_PORT" --console-address ":$MINIO_CONSOLE_PORT" /data +echo "Container '$MINIO_CTR_NAME' started (rc=$?)" + +# Pre-create the 'langfuse' bucket. Langfuse does NOT create the bucket itself; +# event/media ingestion fails with HTTP 500 if the bucket is missing. The +# official MinIO image bundles the `mc` client, so we use it inside the +# container to wait for the server and create the bucket idempotently. +echo "Creating MinIO bucket 'langfuse'..." +for attempt in $(seq 1 30); do + if podman exec "$MINIO_CTR_NAME" mc alias set local \ + "http://localhost:$MINIO_API_PORT" minio miniosecret >/dev/null 2>&1; then + podman exec "$MINIO_CTR_NAME" mc mb --ignore-existing local/langfuse + echo "MinIO bucket 'langfuse' is ready." + break + fi + sleep 2 + if [ "$attempt" -eq 30 ]; then + echo "ERROR: MinIO did not become ready in time." >&2 + exit 1 + fi +done + +# Redis container (queue/cache for Langfuse) +podman run -d --name "$REDIS_CTR_NAME" --pod "$POD_NAME" \ + "$REDIS_IMAGE" \ + --requirepass myredissecret \ + --maxmemory-policy noeviction +echo "Container '$REDIS_CTR_NAME' started (rc=$?)" + +# Langflow container +# Podman automatically provides the `host.containers.internal` hostname in the +# container's /etc/hosts (pointing at the host), so flows can reach the local +# vLLM OpenAI-compatible endpoint on the host without an explicit --add-host. +podman run -d --name "$LANGFLOW_CTR_NAME" --pod "$POD_NAME" \ + -e LANGFLOW_DATABASE_URL="$LANGFLOW_DB_URL" \ + -e LANGFLOW_CONFIG_DIR=/app/langflow \ + -e LANGFLOW_LOG_FILE=/app/langflow/langflow.log \ + -e LANGFLOW_LOG_ENV=default \ + -e LANGFLOW_PRETTY_LOGS=true \ + -e LANGFLOW_LOG_LEVEL=INFO \ + -e DO_NOT_TRACK=True \ + -e LANGFUSE_PUBLIC_KEY="$LANGFUSE_INIT_PUBLIC_KEY" \ + -e LANGFUSE_SECRET_KEY="$LANGFUSE_INIT_SECRET_KEY" \ + -e LANGFUSE_HOST="$LANGFUSE_INPOD_HOST" \ + -v "$LANGFLOW_DATA_DIR:/app/langflow:Z" \ + "$LANGFLOW_IMAGE" +echo "Container '$LANGFLOW_CTR_NAME' started (rc=$?)" + +# Langfuse worker container +podman run -d --name "$LANGFUSE_WORKER_CTR_NAME" --pod "$POD_NAME" \ + "${LANGFUSE_COMMON_ENV[@]}" \ + "$LANGFUSE_WORKER_IMAGE" +echo "Container '$LANGFUSE_WORKER_CTR_NAME' started (rc=$?)" + +# Langfuse web container (exposed on host port 8091 -> 3000) +podman run -d --name "$LANGFUSE_WEB_CTR_NAME" --pod "$POD_NAME" \ + "${LANGFUSE_COMMON_ENV[@]}" \ + -e NEXTAUTH_SECRET="qwexczutbewrgerznupvemqyw" \ + -e LANGFUSE_INIT_ORG_ID="$LANGFUSE_INIT_ORG" \ + -e LANGFUSE_INIT_ORG_NAME="Test Org" \ + -e LANGFUSE_INIT_PROJECT_ID="$LANGFUSE_INIT_PROJECT" \ + -e LANGFUSE_INIT_PROJECT_NAME="Test Project" \ + -e LANGFUSE_INIT_PROJECT_PUBLIC_KEY="$LANGFUSE_INIT_PUBLIC_KEY" \ + -e LANGFUSE_INIT_PROJECT_SECRET_KEY="$LANGFUSE_INIT_SECRET_KEY" \ + -e LANGFUSE_INIT_USER_EMAIL="$LANGFUSE_INIT_EMAIL" \ + -e LANGFUSE_INIT_USER_NAME="Admin" \ + -e LANGFUSE_INIT_USER_PASSWORD="$LANGFUSE_INIT_PASSWORD" \ + "$LANGFUSE_WEB_IMAGE" +echo "Container '$LANGFUSE_WEB_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 Langflow API readiness +CHECK_URL_LANGFLOW="http://$HOST_LOCAL_IP:$LANGFLOW_HOST_PORT" +CHECK_URL_LANGFUSE="http://$HOST_LOCAL_IP:$LANGFUSE_HOST_PORT" +for attempt in $(seq 1 30); do + if curl -fsS "$CHECK_URL_LANGFLOW" >/dev/null 2>&1; then + echo "Langflow Web UI is reachable at $CHECK_URL_LANGFLOW." + echo "Langfuse Web UI is reachable at $CHECK_URL_LANGFUSE." + break + fi + sleep 2 + if [ "$attempt" -eq 30 ]; then + echo "timeout error." >&2 + exit 1 + fi + done diff --git a/set-colors.sh b/set-colors.sh new file mode 100755 index 0000000..1362845 --- /dev/null +++ b/set-colors.sh @@ -0,0 +1,5 @@ +mkdir -p ~/.config/xfce4/terminal +cat > ~/.config/xfce4/terminal/terminalrc << 'EOF' +[Configuration] +ColorPalette=#000000;#ef5350;#4e9a06;#c4a000;#5f87ff;#75507b;#06989a;#d3d7cf;#555753;#ef2929;#8ae234;#fce94f;#729fcf;#ad7fa8;#34e2e2;#eeeeec +EOF