{"id":50710895,"url":"https://github.com/bugbiteme/rhdh-setup","last_synced_at":"2026-06-09T15:03:26.465Z","repository":{"id":359436785,"uuid":"1243745250","full_name":"bugbiteme/rhdh-setup","owner":"bugbiteme","description":null,"archived":false,"fork":false,"pushed_at":"2026-05-21T21:36:08.000Z","size":27,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-22T06:55:26.549Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Shell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bugbiteme.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-19T16:13:46.000Z","updated_at":"2026-05-21T21:36:11.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/bugbiteme/rhdh-setup","commit_stats":null,"previous_names":["bugbiteme/rhdh-setup"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/bugbiteme/rhdh-setup","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bugbiteme%2Frhdh-setup","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bugbiteme%2Frhdh-setup/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bugbiteme%2Frhdh-setup/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bugbiteme%2Frhdh-setup/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bugbiteme","download_url":"https://codeload.github.com/bugbiteme/rhdh-setup/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bugbiteme%2Frhdh-setup/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34112225,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-09T02:00:06.510Z","response_time":63,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-09T15:03:26.389Z","updated_at":"2026-06-09T15:03:26.459Z","avatar_url":"https://github.com/bugbiteme.png","language":"Shell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Red Hat Developer Hub — Setup and Configuration Guide\n\nDocumentation reference: https://docs.redhat.com/en/documentation/red_hat_developer_hub/1.9/html/setting_up_and_configuring_your_first_red_hat_developer_hub_instance/index\n\nTested on OCP `4.20.22` with RHDH Operator `1.9.4`\n\n---\n\n## Overview\n\nThis guide walks through deploying a working Red Hat Developer Hub (RHDH) instance on OpenShift Container Platform (OCP) from scratch, including:\n\n- Installing the RHDH Operator\n- Deploying ephemeral PostgreSQL and Redis\n- Creating and configuring a GitHub App\n- Deploying and configuring the RHDH instance with GitHub authentication\n\n---\n\n## Prerequisites\n\n- Access to an OpenShift cluster (4.16–4.21) with cluster-admin permissions\n- `oc` CLI installed and logged in\n- A GitHub account with permission to create GitHub Apps\n\n---\n\n## Repository Structure\n\n```\nmanifests/\n├── operators/                      # CR-based Operator installation\n│   ├── kustomization.yaml\n│   └── rhdh-operator.yaml\n├── postgresql/\n│   └── postgresql-deploy.yaml\n├── redis/\n│   └── redis-deploy.yaml\n└── rhdh/\n    ├── kustomization.yaml\n    ├── namespace.yaml\n    ├── backstage.yaml              # Backstage Custom Resource\n    ├── dynamic-plugins.yaml        # Plugin configuration\n    ├── dynamic-plugins-pvc.yaml    # PVC for plugin caching\n    ├── app-config.yaml             # Main RHDH configuration\n    ├── app-config.yaml.template    # Template to copy from\n    ├── secrets.txt.template        # Secrets template (copy to secrets.txt)\n    └── secrets.txt                 # Your secrets (never commit this)\n```\n\n---\n\n## Step 1 — Install the Red Hat Developer Hub Operator\n\n1. In the OpenShift web console, go to **Operators → OperatorHub**\n2. Search for `Developer Hub` and click the **Red Hat Developer Hub Operator** card\n3. Click **Install** and configure:\n   - **Update channel**: `fast-1.9` (z-stream updates only; switch to `fast-1.10` manually when ready to upgrade)\n   - **Installation mode**: All namespaces (default)\n   - **Installed namespace**: `rhdh-operator` (recommended default)\n   - **Update approval**: Automatic or Manual\n4. Click **Install** and wait for the \"Installed operator: ready for use\" message\n\n\u003e **Note:** The `fast` channel includes all updates for a version and may introduce breaking changes. The `fast-1.9` channel is safer for production as it only delivers z-stream patches.\n\n\u003e **Alternative: CR-based Operator install**\n\u003e Instead of using the web console, you can install the Operator declaratively:\n\u003e ```bash\n\u003e oc apply -k manifests/operators/\n\u003e ```\n\n---\n\n## Step 2 — Deploy External Services\n\n### PostgreSQL\n\nDeploy an ephemeral PostgreSQL instance:\n\n```bash\noc apply -f manifests/postgresql\n```\n\nDefault connection values:\n\n| Key | Value |\n|-----|-------|\n| User | `rhdh-admin` |\n| Password | `rhdh-admin` |\n| Database | `rhdh-db` |\n| Port | `5432` |\n| Internal FQDN | `postgres.postgresql.svc.cluster.local` |\n\nSmoke test:\n\n```bash\noc debug deployment/postgres -- psql \\\n  \"postgresql://rhdh-admin:rhdh-admin@postgres.postgresql.svc.cluster.local:5432/rhdh-db\" \\\n  -c \"SELECT 1\"\n```\n\nExpected output:\n```\n ?column?\n----------\n        1\n(1 row)\n```\n\n### Redis\n\nDeploy an ephemeral Redis instance:\n\n```bash\noc apply -f manifests/redis\n```\n\nDefault connection values:\n\n| Key | Value |\n|-----|-------|\n| Password | `rhdh-admin` |\n| Internal FQDN | `redis.redis.svc.cluster.local` |\n\nSmoke test:\n\n```bash\noc debug deployment/redis -- redis-cli \\\n  -h redis.redis.svc.cluster.local -a rhdh-admin SET foo bar\n\noc debug deployment/redis -- redis-cli \\\n  -h redis.redis.svc.cluster.local -a rhdh-admin GET foo\n```\n\n---\n\n## Step 3 — Create a GitHub App\n\nCreate a single GitHub App that handles both catalog/repository integration and user authentication.\n\n\u003e **Chicken-and-egg situation:** The GitHub App registration requires your RHDH URL for the Homepage URL and Authorization callback URL fields. But your RHDH instance doesn't exist yet, so the route hasn't been created. You can work around this by predicting the URL from your cluster's domain — it follows a deterministic pattern based on the route name, namespace, and cluster apps domain. Run the commands below to calculate it before the instance exists.\n\n### Determine your RHDH URL first\n\n```bash\nexport RHDH_DOMAIN=$(oc get ingresses.config/cluster -o jsonpath='{.spec.domain}')\nexport RHDH_NAMESPACE=\"rhdh\"\nexport RHDH_ROUTE_NAME=\"backstage-rhdh-instance\"\necho \"RHDH URL: https://$RHDH_ROUTE_NAME-$RHDH_NAMESPACE.$RHDH_DOMAIN\"\necho \"Auth callback: https://$RHDH_ROUTE_NAME-$RHDH_NAMESPACE.$RHDH_DOMAIN/api/auth/github/handler/frame\"\n```\n\n### Register the GitHub App\n\nRegister a GitHub App at https://github.com/settings/apps/new with:\n\n| Field | Value |\n|-------|-------|\n| GitHub App name | `rhdh-\u003cGUID\u003e` |\n| Homepage URL | Your RHDH URL |\n| Authorization callback URL | `https://\u003crhdh-url\u003e/api/auth/github/handler/frame` |\n| Webhook | Disabled (uncheck Active) |\n| Repository permissions | Contents: Read-only, Commit statuses: Read-only |\n| Organization permissions | Members: Read-only |\n| Where can this be installed | Only on this account |\n\nThen:\n1. Go to **General → Client secrets** → click **Generate a new client secret**\n2. Go to **General → Private keys** → click **Generate a private key**\n3. Go to **Install App** → install on your organization\n\nSave: **App ID**, **Client ID**, **Client Secret**, **Private Key** (downloaded `.pem` file)\n\nConvert the private key to a single line for use in secrets:\n\n```bash\nawk 'NF {sub(/\\r/, \"\"); printf \"%s\\\\n\",$0;}' ~/Downloads/\u003cyour-key\u003e.pem\n```\n\n\u003e **Important:** Make sure your GitHub org membership visibility is set to **Public** at `https://github.com/orgs/\u003cyour-org\u003e/people`. Private membership is not visible to the GitHub App and will prevent user import.\n\n---\n\n## Step 4 — Prepare Secrets\n\n### Generate a Backend Secret\n\nThis is an internal secret used by RHDH to sign tokens between frontend and backend:\n\n```bash\nopenssl rand -hex 32\n```\n\n### Create secrets.txt\n\n```bash\ncp manifests/rhdh/secrets.txt.template manifests/rhdh/secrets.txt\n```\n\nEdit `manifests/rhdh/secrets.txt` and populate all values. **No blank lines, no quotes, one `KEY=value` per line:**\n\n```\nBACKEND_SECRET=\u003cgenerated-hex\u003e\nGITHUB_APP_APP_ID=\u003cyour app id\u003e\nGITHUB_APP_CLIENT_ID_INTEGRATION=\u003cyour client id\u003e\nGITHUB_APP_CLIENT_SECRET_INTEGRATION=\u003cyour client secret\u003e\nGITHUB_APP_PRIVATE_KEY=\u003csingle-line pem from awk command above\u003e\nGITHUB_URL=https://github.com\nGITHUB_ORG=\u003cyour github org slug\u003e\nGITHUB_APP_CLIENT_ID=\u003csame client id as above\u003e\nGITHUB_APP_CLIENT_SECRET=\u003csame client secret as above\u003e\nRHDH_URL=https://backstage-rhdh-instance-rhdh.apps.\u003cyour-cluster-domain\u003e\nREDIS_URL=redis://:rhdh-admin@redis.redis.svc.cluster.local:6379\nPOSTGRES_HOST=postgres.postgresql.svc.cluster.local\nPOSTGRES_PORT=5432\nPOSTGRES_USER=rhdh-admin\nPOSTGRES_PASSWORD=rhdh-admin\nPOSTGRES_DATABASE=rhdh-db\n```\n\n\u003e **Note:** `GITHUB_APP_CLIENT_ID` and `GITHUB_APP_CLIENT_SECRET` are the same values as their `_INTEGRATION` counterparts — both are needed because the config uses them in different contexts (catalog integration vs user authentication).\n\n\u003e **Critical:** Use `--from-env-file` (not `--from-file`) when creating the secret, or env vars won't be injected correctly. Blank lines in the file will cause parsing to stop at that point.\n\n\u003e **Never commit `secrets.txt` to git.** It is in `.gitignore` by default.\n\n---\n\n## Step 5 — Deploy RHDH\n\n### Create namespace, ConfigMaps, and Secret\n\n```bash\noc apply -f manifests/rhdh/namespace.yaml\n\noc -n rhdh create configmap my-rhdh-app-config \\\n  --from-file=manifests/rhdh/app-config.yaml\n\noc -n rhdh create configmap dynamic-plugins-rhdh \\\n  --from-file=manifests/rhdh/dynamic-plugins.yaml\n\noc -n rhdh create secret generic my-rhdh-secrets \\\n  --from-env-file=manifests/rhdh/secrets.txt\n```\n\nVerify all secret keys were parsed correctly (you should see individual key names, not `secrets.txt`):\n\n```bash\noc get secret my-rhdh-secrets -n rhdh -o jsonpath='{.data}' | \\\n  python3 -c \"import json,sys; [print(k) for k in json.load(sys.stdin)]\"\n```\n\n### Deploy with Kustomize\n\n```bash\noc apply -k manifests/rhdh/\n```\n\nThis deploys the dynamic plugins PVC and the Backstage Custom Resource. The namespace already exists from the previous step — Kustomize will skip it without error.\n\n---\n\n## Step 6 — Verify\n\nWatch pods come up:\n\n```bash\noc get pods -n rhdh -w\n```\n\nYou should see one backstage pod. The local PostgreSQL pod will not appear since `enableLocalDb: false` is set and we're using the external PostgreSQL instance.\n\nCheck logs for a clean startup:\n\n```bash\noc logs -f -n rhdh -c backstage-backend \\\n  $(oc get pods -n rhdh -o name | grep \"backstage-rhdh-instance\" | head -1)\n```\n\nA healthy startup ends with:\n\n```\nPlugin initialization complete, newly initialized: 'healthcheck', 'app', 'catalog', ...\n```\n\nAnd readiness probes returning `200`:\n\n```\n\"GET /.backstage/health/v1/readiness HTTP/1.1\" 200\n```\n\nVerify GitHub org sync ran successfully:\n\n```bash\noc logs -n rhdh deployment/backstage-rhdh-instance -c backstage-backend | \\\n  grep -i \"GithubMultiOrg\\|Read.*GitHub\"\n```\n\nYou should see a line like:\n```\nRead 1 GitHub users and 1 GitHub groups\n```\n\nNavigate to your RHDH URL and sign in with GitHub.\n\n### gitops integration\n\nWhat we need to do:\n\n- Create a readonly `developer-hub` user in the `openshift-gitops` ArgoCD instance\n- Store the Argo CD credentials in a secret in the `rhdh` namespace\n- Add the secret to the Backstage CR\n- Enable the Argo CD dynamic plugins in `dynamic-plugins.yaml`\n- Add the argocd config block to `app-config.yaml`\n\nDeploy new ArgoCD instance for RHDH\n\n```bash\noc apply -k manifests/argocd/\n```\n\nOnce deployed, obtain the route\n```bash\noc get route -n rhdh-gitops\n```\n\nand log in with `developer-hub` / `d3v3l0p3rs` to confirm the user account is active before we wire it into RHDH\n\nUpdate `secrets.txt`\nARGOCD_URL=https://rhdh-gitops-server-rhdh-gitops.apps.\u003cyour-cluster-domain\u003e\nARGOCD_USERNAME=developer-hub\nARGOCD_PASSWORD=d3v3l0p3rs\n\nGet the exact URL with:\n```bash\noc get route rhdh-gitops-server -n rhdh-gitops -o jsonpath='{.spec.host}'\n```\n\n---\n\n## Configuration Reference\n\n### app-config.yaml\n\n```yaml\napp:\n  title: Red Hat Developer Hub\n  baseUrl: ${RHDH_URL}\n\nbackend:\n  auth:\n    externalAccess:\n      - type: legacy\n        options:\n          subject: legacy-default-config\n          secret: ${BACKEND_SECRET}\n  baseUrl: ${RHDH_URL}\n  cors:\n    origin: ${RHDH_URL}\n  cache:\n    store: redis\n    connection: ${REDIS_URL}\n  database:\n    client: pg\n    connection:\n      host: ${POSTGRES_HOST}\n      port: ${POSTGRES_PORT}\n      user: ${POSTGRES_USER}\n      password: ${POSTGRES_PASSWORD}\n      database: ${POSTGRES_DATABASE}\n    pluginDivisionMode: schema\n\nauth:\n  environment: development\n  providers:\n    github:\n      development:\n        clientId: ${GITHUB_APP_CLIENT_ID}\n        clientSecret: ${GITHUB_APP_CLIENT_SECRET}\n        signIn:\n          resolvers:\n            - resolver: usernameMatchingUserEntityName\n\ncatalog:\n  providers:\n    githubOrg:\n      id: githuborg\n      githubUrl: ${GITHUB_URL}\n      orgs: [\"${GITHUB_ORG}\"]\n      schedule:\n        frequency: { minutes: 30 }\n        timeout: { minutes: 15 }\n        initialDelay: { seconds: 15 }\n\nintegrations:\n  github:\n    - host: github.com\n      apps:\n        - appId: ${GITHUB_APP_APP_ID}\n          clientId: ${GITHUB_APP_CLIENT_ID_INTEGRATION}\n          clientSecret: ${GITHUB_APP_CLIENT_SECRET_INTEGRATION}\n          privateKey: ${GITHUB_APP_PRIVATE_KEY}\n```\n\n### dynamic-plugins.yaml\n\n```yaml\nincludes:\n  - dynamic-plugins.default.yaml\nplugins:\n  - package: ./dynamic-plugins/dist/backstage-plugin-catalog-backend-module-github-org-dynamic\n    disabled: false\n  - package: ./dynamic-plugins/dist/backstage-community-plugin-rbac\n    disabled: false\n```\n\n### backstage.yaml (Backstage CR)\n\n```yaml\napiVersion: rhdh.redhat.com/v1alpha5\nkind: Backstage\nmetadata:\n  name: rhdh-instance\n  namespace: rhdh\nspec:\n  application:\n    appConfig:\n      mountPath: /opt/app-root/src\n      configMaps:\n        - name: my-rhdh-app-config\n    dynamicPluginsConfigMapName: dynamic-plugins-rhdh\n    extraEnvs:\n      secrets:\n        - name: my-rhdh-secrets\n  database:\n    enableLocalDb: false\n  deployment:\n    patch:\n      spec:\n        template:\n          spec:\n            volumes:\n              - $patch: replace\n                name: dynamic-plugins-root\n                persistentVolumeClaim:\n                  claimName: dynamic-plugins-root\n```\n\n---\n\n## Updating Configuration\n\nWhen making changes to `app-config.yaml`, `dynamic-plugins.yaml`, or `secrets.txt`:\n\n```bash\n# Update a configmap\noc delete configmap my-rhdh-app-config -n rhdh\noc -n rhdh create configmap my-rhdh-app-config --from-file=manifests/rhdh/app-config.yaml\n\n# Update dynamic plugins configmap\noc delete configmap dynamic-plugins-rhdh -n rhdh\noc -n rhdh create configmap dynamic-plugins-rhdh --from-file=manifests/rhdh/dynamic-plugins.yaml\n\n# Update a secret\noc delete secret my-rhdh-secrets -n rhdh\noc -n rhdh create secret generic my-rhdh-secrets --from-env-file=manifests/rhdh/secrets.txt\n\n# Restart the pod to pick up changes\noc delete pod -n rhdh -l rhdh.redhat.com/app=backstage-rhdh-instance\n```\n\n\u003e **Note:** The dynamic plugin PVC caches installed plugins across pod restarts. The first restart after any plugin change will re-download affected plugins. Subsequent restarts will use the cache and complete in 1-2 seconds.\n\n---\n\n## Troubleshooting\n\n### init container crashes in a loop (`install-dynamic-plugins`)\n\nCheck the init container logs:\n\n```bash\noc logs -n rhdh \u003cpod-name\u003e -c install-dynamic-plugins --previous\n```\n\nMost likely cause: wrong plugin package name. Backend plugins require the `-dynamic` suffix, e.g. `backstage-plugin-catalog-backend-module-github-org-dynamic` not `backstage-plugin-catalog-backend-module-github-org`.\n\n### `Missing required config value at 'backend.auth.externalAccess[0].options.secret' in 'env'`\n\nThe `BACKEND_SECRET` env var isn't making it into the pod. Check:\n\n```bash\n# Verify the secret has individual keys (not 'secrets.txt' as a single key)\noc get secret my-rhdh-secrets -n rhdh -o jsonpath='{.data}' | \\\n  python3 -c \"import json,sys; [print(k) for k in json.load(sys.stdin)]\"\n\n# Verify the env var is in the pod\noc exec -n rhdh deployment/backstage-rhdh-instance -- env | grep BACKEND_SECRET\n```\n\nIf the secret key is `secrets.txt`, recreate it using `--from-env-file` instead of `--from-file`.\n\n### `permission denied to create database`\n\nThe PostgreSQL user doesn't have permission to create databases. Add `pluginDivisionMode: schema` to the database config in `app-config.yaml` so Backstage uses schemas within the existing database instead of creating new ones.\n\n### `Migration table is already locked`\n\nCaused by a previous pod crashing mid-migration. Delete the pod and let it restart:\n\n```bash\noc delete pod -n rhdh -l rhdh.redhat.com/app=backstage-rhdh-instance\n```\n\n### `Failed to sign-in, unable to resolve user identity`\n\nBackstage authenticated with GitHub successfully but couldn't find a matching User entity in the catalog. Check:\n\n1. Your GitHub org membership is set to **Public** at `https://github.com/orgs/\u003cyour-org\u003e/people`\n2. The `githubOrg` catalog provider ran successfully:\n   ```bash\n   oc logs -n rhdh deployment/backstage-rhdh-instance -c backstage-backend | \\\n     grep -i \"Read.*GitHub\"\n   ```\n3. You are using `backstage-plugin-catalog-backend-module-github-org-dynamic` (not `github-dynamic`) in `dynamic-plugins.yaml`\n\n### `EnvConfigSource{count=1}` in startup logs\n\nOnly one env var is being substituted. The secret was likely created with `--from-file` instead of `--from-env-file`. Recreate it correctly.\n\n### Route URL mismatch\n\nThe Operator creates the route using the CR name, not a custom name. Verify the actual route hostname before configuring the GitHub App callback URL:\n\n```bash\noc get route -n rhdh\n```\n\nUse `https://` — the route uses `edge/Redirect` TLS termination.\n\n---\n\n## Lessons Learned\n\n- Always use `--from-env-file` when creating secrets from a key=value file — never `--from-file`\n- Always include `-n \u003cnamespace\u003e` on every `oc` command\n- Backend dynamic plugin names require the `-dynamic` suffix\n- Use `backstage-plugin-catalog-backend-module-github-org-dynamic` to import GitHub users/groups — `github-dynamic` only discovers repositories\n- The `database` field in the Backstage CR is at `spec.database`, not `spec.application.database`\n- The `default.app-config.yaml` baked into the RHDH image is loaded after your custom config and can override values — secrets must be injected as real env vars via the CR's `extraEnvs.secrets` section\n- The `No configuration found for cache store 'redis'` warning is harmless\n- GitHub org membership must be **Public** for the GitHub App to see org members\n- The route hostname follows the pattern `\u003ccr-name\u003e-\u003cnamespace\u003e.\u003capps-domain\u003e` and uses HTTPS\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbugbiteme%2Frhdh-setup","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbugbiteme%2Frhdh-setup","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbugbiteme%2Frhdh-setup/lists"}