Octopus Self-Host (BYOK via agentgateway) Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Self-host octopusreview/octopus AI PR review in-cluster (GitOps/ArgoCD), with LLM + embeddings routed through the cluster’s agentgateway (OpenRouter reviews under ZDR + self-hosted bge-m3 embeddings), via a minimal upstreamable fork.
Architecture: A seanb4t/octopus fork adds one env-gated change (EMBEDDING_DIM, default 3072 → 1024 for bge-m3) plus a migrator image stage running prisma migrate deploy. The cluster runs one Octopus web Deployment (Next.js standalone, in-process pg-boss review worker) + a dedicated qdrant/qdrant + a dedicated CNPG database. Octopus’s OpenAI provider path honours OPENAI_BASE_URL, so review + embed models are registered as provider=openai availableModel rows pointing at agentgateway aliases. Webhook is public (octopus-wh.fzymgc.net); admin UI is internal-only (octopus.fzymgc.house).
Tech Stack: Kubernetes (k3s), ArgoCD, Kustomize, CloudNativePG (Postgres 18), Qdrant, External Secrets + Vault, Traefik IngressRoute + Cloudflare Tunnel, cert-manager (vault-issuer), GitHub App; Octopus = Next.js/Bun + Prisma 7 + pg-boss.
Reference spec: docs/engineering/specs/2026-06-06-octopus-self-host-design.md (bd hl-0we).
Sequencing note: Tasks 1–12 are independent of epic hl-0sr (LiteLLM → agentgateway). Tasks 13–15 (final LLM wiring + go-live) block on the hl-0sr cutover; until then the Deployment ships scaled to 0 replicas. An interim LiteLLM backing is described in Task 13 as an optional unblock.
File structure
Section titled “File structure”In the seanb4t/octopus fork (separate repo):
apps/web/lib/qdrant.ts— change the hardcodedVECTOR_SIZEto readEMBEDDING_DIM.apps/web/lib/qdrant.ts— passapiKey: process.env.QDRANT_API_KEYto the client.apps/web/Dockerfile— add amigratorbuild stage (bunx prisma migrate deploy).packages/db/package.json— add adb:deployscript (prisma migrate deploy)..github/workflows/build-image.yaml— multi-arch buildx →ghcr.io/seanb4t/octopus+ghcr.io/seanb4t/octopus-migrate.
In selfhosted-cluster (this repo):
argocd/app-configs/cnpg/db-octopus.yaml— CNPGDatabaseCR.argocd/app-configs/cnpg/users-octopus.yaml— role-credential ExternalSecret.argocd/app-configs/cnpg/postgres-cluster.yaml— addoctopustomanaged.roles(Modify).argocd/app-configs/cnpg/kustomization.yaml— register the two new files (Modify).argocd/app-configs/octopus/namespace.yamlargocd/app-configs/octopus/qdrant.yaml— Qdrant Deployment + PVC + Service.argocd/app-configs/octopus/secrets.yaml— ExternalSecrets (DB url + app env).argocd/app-configs/octopus/deployment.yaml— migrator initContainer + web container.argocd/app-configs/octopus/service.yamlargocd/app-configs/octopus/certificate.yaml—octopus-tls(vault-issuer).argocd/app-configs/octopus/ingress.yaml— internal UI IngressRoute.argocd/app-configs/octopus/kustomization.yamlargocd/cluster-app/templates/octopus.yaml— ArgoCD Application.docs/reference/secrets.md,docs/reference/services.md— docs (Modify).docs/operations/octopus.md— runbook (Create).
Task 1: Fork + patch seanb4t/octopus
Section titled “Task 1: Fork + patch seanb4t/octopus”Files (in the seanb4t/octopus fork):
- Modify:
apps/web/lib/qdrant.ts - Modify:
packages/db/package.json - Step 1: Fork and clone
gh repo fork octopusreview/octopus --org-or-user seanb4t --clonecd octopusgit checkout -b feat/configurable-embedding-dim- Step 2: Make
VECTOR_SIZEconfigurable
In apps/web/lib/qdrant.ts, replace the hardcoded constant:
// before:const VECTOR_SIZE = 3072; // text-embedding-3-large// after:const VECTOR_SIZE = Number(process.env.EMBEDDING_DIM ?? 3072); // default text-embedding-3-large (3072); bge-m3 = 1024- Step 3: Wire optional Qdrant API key
In apps/web/lib/qdrant.ts, update getQdrantClient():
// before:client = new QdrantClient({ url: process.env.QDRANT_URL! });// after:client = new QdrantClient({ url: process.env.QDRANT_URL!, ...(process.env.QDRANT_API_KEY ? { apiKey: process.env.QDRANT_API_KEY } : {}),});- Step 4: Add a production migrate script
In packages/db/package.json, add to scripts (the existing db:migrate is prisma migrate dev, which is interactive/dev-only and unsafe in production):
"db:deploy": "prisma migrate deploy",- Step 5: Typecheck the change
Run: bun install && cd apps/web && bunx tsc --noEmit
Expected: no new type errors from the edited file.
- Step 6: Commit
git add apps/web/lib/qdrant.ts packages/db/package.jsongit commit -m "feat: configurable EMBEDDING_DIM + QDRANT_API_KEY + db:deploy (prod migrate)"Task 2: Add a migrator Docker stage to the fork
Section titled “Task 2: Add a migrator Docker stage to the fork”Files (in the fork):
- Modify:
apps/web/Dockerfile
The runtime image is node:22-alpine running node apps/web/server.js — it has no bun, Prisma CLI, or migration files, so migrations cannot run from it. Add a dedicated migrator target built from the builder stage (which already has bun + Prisma + packages/db/prisma/migrations).
- Step 1: Append a migrator stage
At the end of apps/web/Dockerfile, add:
# --- migrator (runs prisma migrate deploy as an initContainer) ---FROM builder AS migratorWORKDIR /app/packages/dbCMD ["bun", "run", "db:deploy"]Prisma 7 note:
migrate deployreads the datasource from the schema’senv("DATABASE_URL")— the--urlCLI flag was removed in v7. The migrator therefore needs only theDATABASE_URLenv var set (Task 9 initContainer), no--urlargument.
- Step 2: Build the migrator target locally to verify it resolves
Run: docker build -f apps/web/Dockerfile --target migrator -t octopus-migrate:dev .
Expected: build succeeds; docker run --rm octopus-migrate:dev sh -c 'bunx prisma --version' prints a Prisma 7.x version.
- Step 3: Commit
git add apps/web/Dockerfilegit commit -m "build: add migrator image stage running prisma migrate deploy"Task 3: Multi-arch image CI in the fork
Section titled “Task 3: Multi-arch image CI in the fork”Files (in the fork):
-
Create:
.github/workflows/build-image.yaml -
Step 1: Write the workflow
name: build-imageon: push: branches: [feat/configurable-embedding-dim, master]permissions: contents: read packages: writejobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: docker/setup-qemu-action@v3 - uses: docker/setup-buildx-action@v3 - uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build web image (multi-arch) uses: docker/build-push-action@v6 with: context: . file: apps/web/Dockerfile target: runner platforms: linux/amd64,linux/arm64 push: true build-args: | NEXT_PUBLIC_GITHUB_APP_SLUG=octopus-fzymgc tags: ghcr.io/seanb4t/octopus:latest - name: Build migrator image (multi-arch) uses: docker/build-push-action@v6 with: context: . file: apps/web/Dockerfile target: migrator platforms: linux/amd64,linux/arm64 push: true tags: ghcr.io/seanb4t/octopus-migrate:latest- Step 2: Verify the marketplace action versions are current
Run: date +%Y then check docker/build-push-action, docker/setup-buildx-action, actions/checkout latest majors on the GitHub Marketplace; pin to the current major.
Expected: versions confirmed (do not assume from training data).
- Step 3: Push branch, watch the build
git push -u origin feat/configurable-embedding-dimgh run watchExpected: both images build for amd64+arm64 and push. The arm64 build is the risk (Next.js standalone build under --max-old-space-size=4096); if it OOMs/times out under QEMU, switch that job to a native arm64 runner.
- Step 4: Capture the multi-arch digest
Run: docker buildx imagetools inspect ghcr.io/seanb4t/octopus:latest
Expected: a manifest list with linux/amd64 + linux/arm64. Record the sha256: digest for the Deployment (Task 9). Make both GHCR packages public, or rely on the existing ghcr-pull-secret (the cluster PAT covers seanb4t/*).
Task 4: Upstream contribution PR (tracking)
Section titled “Task 4: Upstream contribution PR (tracking)”Files: none in this repo.
- Step 1: Open the upstream PR
gh pr create --repo octopusreview/octopus --head seanb4t:feat/configurable-embedding-dim \ --title "feat: configurable embedding dimension + Qdrant API key + prod migrate script" \ --body "Adds EMBEDDING_DIM env (default 3072, backward-compatible) so self-hosters can use non-OpenAI embedders (e.g. bge-m3 @ 1024-dim); passes QDRANT_API_KEY when set; adds db:deploy (prisma migrate deploy) for production migrations."- Step 2: Note the PR for later image-source switch
Once merged + released upstream, the cluster Deployment can switch from ghcr.io/seanb4t/octopus to the upstream stock image (a follow-up bead). Record the PR URL on bd hl-0we.
Task 5: Provision the CNPG database + role
Section titled “Task 5: Provision the CNPG database + role”Files:
- Create:
argocd/app-configs/cnpg/db-octopus.yaml - Create:
argocd/app-configs/cnpg/users-octopus.yaml - Modify:
argocd/app-configs/cnpg/postgres-cluster.yaml(add tomanaged.roles) - Modify:
argocd/app-configs/cnpg/kustomization.yaml - Step 1: Seed the Vault DB credential
Out-of-band (you have Vault access), create a username/password for the role:
vault kv put secret/fzymgc-house/cluster/postgres/users/main-octopus \ username=octopus password="$(openssl rand -base64 24)"- Step 2: Create the
DatabaseCR
argocd/app-configs/cnpg/db-octopus.yaml:
---apiVersion: postgresql.cnpg.io/v1kind: Databasemetadata: name: octopus namespace: postgresspec: cluster: name: main ensure: present name: octopus owner: octopus- Step 3: Create the role-credential ExternalSecret
argocd/app-configs/cnpg/users-octopus.yaml:
---apiVersion: external-secrets.io/v1kind: ExternalSecretmetadata: name: main-octopus-credentials namespace: postgresspec: refreshPolicy: Periodic refreshInterval: 5m secretStoreRef: name: vault kind: ClusterSecretStore target: name: main-octopus-credentials creationPolicy: Owner deletionPolicy: Delete template: metadata: labels: cnpg.io/reload: "true" type: kubernetes.io/basic-auth data: username: "{{ .username }}" password: "{{ .password }}" data: - secretKey: username remoteRef: key: fzymgc-house/cluster/postgres/users/main-octopus property: username - secretKey: password remoteRef: key: fzymgc-house/cluster/postgres/users/main-octopus property: password- Step 4: Add the role to the managed cluster
In argocd/app-configs/cnpg/postgres-cluster.yaml, under spec.managed.roles, append after the litellm entry (mirror the existing entries exactly):
- name: octopus ensure: present passwordSecret: name: main-octopus-credentials superuser: false login: true connectionLimit: -1 inherit: true- Step 5: Register the new files in the kustomization
In argocd/app-configs/cnpg/kustomization.yaml, add after users-litellm.yaml:
- db-octopus.yaml - users-octopus.yaml- Step 6: Validate the kustomize build
Run: kubectl kustomize argocd/app-configs/cnpg | rg -A2 "kind: Database" | rg octopus
Expected: the octopus Database renders. Also confirm no YAML error from the whole build.
- Step 7: Commit
Commit using VCS-appropriate commands per references/vcs-preamble.md (message: feat(cnpg): octopus database + role).
Task 6: Deploy the dedicated Qdrant
Section titled “Task 6: Deploy the dedicated Qdrant”Files:
- Create:
argocd/app-configs/octopus/namespace.yaml - Create:
argocd/app-configs/octopus/qdrant.yaml - Step 1: Namespace
argocd/app-configs/octopus/namespace.yaml:
---apiVersion: v1kind: Namespacemetadata: name: octopus- Step 2: Qdrant PVC + Deployment + Service
argocd/app-configs/octopus/qdrant.yaml (pin the version Octopus tests against, v1.17.0; single replica — Qdrant OSS is single-node):
---apiVersion: v1kind: PersistentVolumeClaimmetadata: name: qdrant-octopus-data namespace: octopusspec: accessModes: ["ReadWriteOnce"] storageClassName: longhorn-encrypted resources: requests: storage: 20Gi---apiVersion: apps/v1kind: Deploymentmetadata: name: qdrant-octopus namespace: octopus labels: app.kubernetes.io/name: qdrant-octopusspec: replicas: 1 strategy: type: Recreate selector: matchLabels: app.kubernetes.io/name: qdrant-octopus template: metadata: labels: app.kubernetes.io/name: qdrant-octopus spec: securityContext: fsGroup: 1000 containers: - name: qdrant image: qdrant/qdrant:v1.17.0 ports: - containerPort: 6333 name: http - containerPort: 6334 name: grpc readinessProbe: httpGet: path: /readyz port: 6333 initialDelaySeconds: 5 periodSeconds: 10 volumeMounts: - name: data mountPath: /qdrant/storage resources: requests: cpu: 100m memory: 512Mi limits: memory: 2Gi volumes: - name: data persistentVolumeClaim: claimName: qdrant-octopus-data---apiVersion: v1kind: Servicemetadata: name: qdrant-octopus namespace: octopusspec: selector: app.kubernetes.io/name: qdrant-octopus ports: - name: http port: 6333 targetPort: 6333 - name: grpc port: 6334 targetPort: 6334- Step 3: Validate
Run: kubectl kustomize argocd/app-configs/octopus 2>/dev/null || kubectl apply --dry-run=client -f argocd/app-configs/octopus/qdrant.yaml
Expected: manifests are valid (full kustomize validates after Task 11).
- Step 4: Commit (
feat(octopus): dedicated qdrant).
Task 7: Register the GitHub App
Section titled “Task 7: Register the GitHub App”Files: none (manual + Vault).
- Step 1: Create the GitHub App
In GitHub → Settings → Developer settings → GitHub Apps → New:
- Name:
octopus-fzymgc(slug becomesNEXT_PUBLIC_GITHUB_APP_SLUG, baked in Task 3). - Homepage URL:
https://octopus.fzymgc.house - Webhook URL:
https://octopus-wh.fzymgc.net/api/github/webhook - Webhook secret:
openssl rand -hex 32(save it). - Permissions: Pull requests read/write, Contents read, Checks read/write.
- Subscribe to events: Pull request, Pull request review.
- Generate a private key (download the
.pem). - Step 2: Create the OAuth login credentials
On the same App page, note the Client ID and generate a Client secret (used by Better Auth for UI login). Add callback URL https://octopus.fzymgc.house/api/auth/callback/github.
- Step 3: Store everything in Vault
vault kv put secret/fzymgc-house/cluster/octopus \ better_auth_secret="$(openssl rand -hex 32)" \ octopus_data_key="$(openssl rand -hex 32)" \ github_app_id="<app id>" \ github_app_private_key="$(cat octopus-fzymgc.private-key.pem)" \ github_webhook_secret="<from step 1>" \ github_state_secret="$(openssl rand -hex 32)" \ github_client_id="<client id>" \ github_client_secret="<client secret>" \ admin_emails="claude2@sean.fzymgc.email" \ openai_api_key="PLACEHOLDER_AGENTGATEWAY_VK"(The openai_api_key placeholder is replaced with the real agentgateway VK at go-live, Task 13.)
- Step 4: Install the App on the target repo
Install octopus-fzymgc on fzymgc-house/selfhosted-cluster (single repo for now; add repos later with no manifest change).
Task 8: Octopus ExternalSecrets
Section titled “Task 8: Octopus ExternalSecrets”Files:
-
Create:
argocd/app-configs/octopus/secrets.yaml -
Step 1: DB-url + app-env ExternalSecrets
argocd/app-configs/octopus/secrets.yaml (DB url composed like litellm; app env carries the rest):
---apiVersion: external-secrets.io/v1kind: ExternalSecretmetadata: name: octopus-db namespace: octopusspec: refreshPolicy: Periodic refreshInterval: 5m secretStoreRef: name: vault kind: ClusterSecretStore target: name: octopus-db creationPolicy: Owner deletionPolicy: Delete template: type: Opaque data: database-url: "postgresql://{{ .u }}:{{ .p }}@main-rw.postgres.svc.cluster.local:5432/octopus" data: - secretKey: u remoteRef: key: fzymgc-house/cluster/postgres/users/main-octopus property: username - secretKey: p remoteRef: key: fzymgc-house/cluster/postgres/users/main-octopus property: password---apiVersion: external-secrets.io/v1kind: ExternalSecretmetadata: name: octopus-app namespace: octopusspec: refreshPolicy: Periodic refreshInterval: 5m secretStoreRef: name: vault kind: ClusterSecretStore target: name: octopus-app creationPolicy: Owner deletionPolicy: Delete template: type: Opaque data: better-auth-secret: "{{ .better_auth_secret }}" octopus-data-key: "{{ .octopus_data_key }}" github-app-id: "{{ .github_app_id }}" github-app-private-key: "{{ .github_app_private_key }}" github-webhook-secret: "{{ .github_webhook_secret }}" github-state-secret: "{{ .github_state_secret }}" github-client-id: "{{ .github_client_id }}" github-client-secret: "{{ .github_client_secret }}" admin-emails: "{{ .admin_emails }}" openai-api-key: "{{ .openai_api_key }}" data: - secretKey: better_auth_secret remoteRef: { key: fzymgc-house/cluster/octopus, property: better_auth_secret } - secretKey: octopus_data_key remoteRef: { key: fzymgc-house/cluster/octopus, property: octopus_data_key } - secretKey: github_app_id remoteRef: { key: fzymgc-house/cluster/octopus, property: github_app_id } - secretKey: github_app_private_key remoteRef: { key: fzymgc-house/cluster/octopus, property: github_app_private_key } - secretKey: github_webhook_secret remoteRef: { key: fzymgc-house/cluster/octopus, property: github_webhook_secret } - secretKey: github_state_secret remoteRef: { key: fzymgc-house/cluster/octopus, property: github_state_secret } - secretKey: github_client_id remoteRef: { key: fzymgc-house/cluster/octopus, property: github_client_id } - secretKey: github_client_secret remoteRef: { key: fzymgc-house/cluster/octopus, property: github_client_secret } - secretKey: admin_emails remoteRef: { key: fzymgc-house/cluster/octopus, property: admin_emails } - secretKey: openai_api_key remoteRef: { key: fzymgc-house/cluster/octopus, property: openai_api_key }-
Step 2: Validate —
kubectl apply --dry-run=client -f argocd/app-configs/octopus/secrets.yaml. Expected: both ExternalSecrets valid. -
Step 3: Commit (
feat(octopus): external secrets).
Task 9: Octopus Deployment (migrator init + web) + Service
Section titled “Task 9: Octopus Deployment (migrator init + web) + Service”Files:
- Create:
argocd/app-configs/octopus/deployment.yaml - Create:
argocd/app-configs/octopus/service.yaml - Step 1: Deployment
argocd/app-configs/octopus/deployment.yaml — note replicas: 0 (staged off until go-live, Task 13), the octopus-migrate initContainer, and the env wiring. Replace <DIGEST> with the Task 3 digest.
---apiVersion: apps/v1kind: Deploymentmetadata: name: octopus namespace: octopus labels: app.kubernetes.io/name: octopusspec: replicas: 0 # staged off until LLM backend (agentgateway, hl-0sr) is live — Task 13 strategy: type: Recreate selector: matchLabels: app.kubernetes.io/name: octopus template: metadata: labels: app.kubernetes.io/name: octopus spec: imagePullSecrets: - name: ghcr-pull-secret initContainers: - name: migrate image: ghcr.io/seanb4t/octopus-migrate:latest@sha256:<MIGRATE_DIGEST> command: ["bun", "run", "db:deploy"] workingDir: /app/packages/db env: - name: DATABASE_URL valueFrom: secretKeyRef: { name: octopus-db, key: database-url } containers: - name: octopus image: ghcr.io/seanb4t/octopus:latest@sha256:<WEB_DIGEST> ports: - containerPort: 3000 name: http env: - name: DATABASE_URL valueFrom: secretKeyRef: { name: octopus-db, key: database-url } - name: QDRANT_URL value: "http://qdrant-octopus.octopus.svc.cluster.local:6333" - name: BETTER_AUTH_URL value: "https://octopus.fzymgc.house" - name: NEXT_PUBLIC_APP_URL value: "https://octopus.fzymgc.house" - name: OPENAI_BASE_URL value: "http://agentgateway.agentgateway.svc.cluster.local:3000/v1" - name: EMBEDDING_DIM value: "1024" - name: ENABLE_REVIEW_WORKERS value: "true" - name: ENABLE_INTERNAL_CLI value: "false" - name: TZ value: "America/New_York" - name: BETTER_AUTH_SECRET valueFrom: { secretKeyRef: { name: octopus-app, key: better-auth-secret } } - name: OCTOPUS_DATA_KEY valueFrom: { secretKeyRef: { name: octopus-app, key: octopus-data-key } } - name: GITHUB_APP_ID valueFrom: { secretKeyRef: { name: octopus-app, key: github-app-id } } - name: GITHUB_APP_PRIVATE_KEY valueFrom: { secretKeyRef: { name: octopus-app, key: github-app-private-key } } - name: GITHUB_WEBHOOK_SECRET valueFrom: { secretKeyRef: { name: octopus-app, key: github-webhook-secret } } - name: GITHUB_STATE_SECRET valueFrom: { secretKeyRef: { name: octopus-app, key: github-state-secret } } - name: GITHUB_CLIENT_ID valueFrom: { secretKeyRef: { name: octopus-app, key: github-client-id } } - name: GITHUB_CLIENT_SECRET valueFrom: { secretKeyRef: { name: octopus-app, key: github-client-secret } } - name: ADMIN_EMAILS valueFrom: { secretKeyRef: { name: octopus-app, key: admin-emails } } - name: OPENAI_API_KEY valueFrom: { secretKeyRef: { name: octopus-app, key: openai-api-key } } readinessProbe: httpGet: path: /api/health port: 3000 initialDelaySeconds: 10 periodSeconds: 15 resources: requests: cpu: 200m memory: 512Mi limits: memory: 2Gi- Step 2: Confirm the health path exists
Octopus is a Next.js app; verify the readiness path before relying on it.
Run: gh api repos/octopusreview/octopus/git/trees/master?recursive=1 --jq '.tree[].path' | rg "api/health"
Expected: a health route exists. If not, change the probe to a tcpSocket: { port: 3000 } check instead.
- Step 3: Service
argocd/app-configs/octopus/service.yaml:
---apiVersion: v1kind: Servicemetadata: name: octopus namespace: octopusspec: selector: app.kubernetes.io/name: octopus ports: - name: http port: 3000 targetPort: 3000-
Step 4: Validate —
kubectl apply --dry-run=client -f argocd/app-configs/octopus/deployment.yaml -f argocd/app-configs/octopus/service.yaml. Expected: valid. -
Step 5: Commit (
feat(octopus): deployment + service (staged replicas=0)).
Task 10: Internal UI IngressRoute + TLS
Section titled “Task 10: Internal UI IngressRoute + TLS”Files:
- Create:
argocd/app-configs/octopus/certificate.yaml - Create:
argocd/app-configs/octopus/ingress.yaml - Step 1: Certificate (vault-issuer)
argocd/app-configs/octopus/certificate.yaml:
---apiVersion: cert-manager.io/v1kind: Certificatemetadata: name: octopus-tls namespace: octopusspec: secretName: octopus-tls issuerRef: name: vault-issuer kind: ClusterIssuer dnsNames: - octopus.fzymgc.house - octopus.octopus.svc.cluster.local - octopus.octopus.svc - octopus usages: - server auth- Step 2: Internal IngressRoute (router-hosts)
argocd/app-configs/octopus/ingress.yaml:
---apiVersion: traefik.io/v1alpha1kind: IngressRoutemetadata: name: octopus namespace: octopus annotations: router-hosts.fzymgc.house/enabled: "true"spec: entryPoints: - websecure routes: - match: Host(`octopus.fzymgc.house`) kind: Rule services: - name: octopus port: 3000 tls: secretName: octopus-tls-
Step 3: Validate —
kubectl apply --dry-run=client -f argocd/app-configs/octopus/certificate.yaml -f argocd/app-configs/octopus/ingress.yaml. Expected: valid. -
Step 4: Commit (
feat(octopus): internal UI ingress + tls).
Task 11: Kustomization + ArgoCD Application
Section titled “Task 11: Kustomization + ArgoCD Application”Files:
- Create:
argocd/app-configs/octopus/kustomization.yaml - Create:
argocd/cluster-app/templates/octopus.yaml - Step 1: Kustomization
argocd/app-configs/octopus/kustomization.yaml:
---apiVersion: kustomize.config.k8s.io/v1beta1kind: Kustomization
namespace: octopus
resources: - namespace.yaml - qdrant.yaml - secrets.yaml - deployment.yaml - service.yaml - certificate.yaml - ingress.yaml- Step 2: ArgoCD Application
argocd/cluster-app/templates/octopus.yaml (mirror tandoor.yaml):
apiVersion: argoproj.io/v1alpha1kind: Applicationmetadata: name: octopus namespace: argocd annotations: argocd.argoproj.io/sync-wave: "1"spec: project: core-services sources: - repoURL: https://github.com/fzymgc-house/selfhosted-cluster targetRevision: HEAD path: argocd/app-configs/octopus destination: server: https://kubernetes.default.svc namespace: octopus syncPolicy: automated: prune: true selfHeal: true syncOptions: - ServerSideApply=true - CreateNamespace=true - RespectIgnoreDifferences=true ignoreDifferences: - kind: ExternalSecret group: external-secrets.io jqPathExpressions: - .spec.data[].remoteRef.conversionStrategy - .spec.data[].remoteRef.decodingStrategy - .spec.data[].remoteRef.metadataPolicy- Step 3: Validate the full kustomize build
Run: kubectl kustomize argocd/app-configs/octopus | rg -c "kind:"
Expected: all resources render (Namespace, PVC, 2× Deployment, 2× Service, 2× ExternalSecret, Certificate, IngressRoute) with no errors.
- Step 4: Commit (
feat(octopus): kustomization + argocd application).
Task 12: Cloudflare tunnel webhook hostname
Section titled “Task 12: Cloudflare tunnel webhook hostname”Files: Terraform (tf/cloudflare/) variable in the HCP Terraform workspace.
- Step 1: Add
octopustowebhook_services
The webhook_services map value is set as an HCP Terraform workspace variable (not a committed tfvars). Add an entry mirroring argocd:
octopus = { service_url = "http://octopus.octopus.svc.cluster.local:3000"}This produces octopus-wh.fzymgc.net (suffix -wh, domain fzymgc.net) and the tunnel ingress rule routing it to the octopus Service. Confirm whether argocd-wh’s service_url points directly at a Service or via Traefik websecure, and match that pattern (if via Traefik, use the Traefik service URL with http_host_header/origin_server_name set as the existing internal_services entries do).
- Step 2: Plan + apply via HCP Terraform
Per docs/operations/hcp-terraform.md / tf/cloudflare/MANUAL_APPLY.md, run the cloudflare workspace plan and apply.
Expected: a new cloudflare_dns_record.webhook_services["octopus"] + tunnel ingress rule. Verify dig octopus-wh.fzymgc.net resolves to the Cloudflare edge.
- Step 3: Commit any committed TF changes (the var value itself lives in HCP); commit doc/notes if applicable.
Task 13: Go-live wiring (BLOCKS on hl-0sr cutover)
Section titled “Task 13: Go-live wiring (BLOCKS on hl-0sr cutover)”Files: Vault + argocd/app-configs/octopus/deployment.yaml (Modify).
- Step 1: Set the real agentgateway virtual key
Once agentgateway is live (hl-0sr), mint/obtain an inbound virtual key and store it:
vault kv patch secret/fzymgc-house/cluster/octopus openai_api_key="<agentgateway VK>"The octopus-app ExternalSecret refreshes within 5m (or force a sync).
Interim option (if you want Octopus live before hl-0sr cuts over): set OPENAI_BASE_URL to http://litellm.litellm.svc.cluster.local:4000/v1 and openai_api_key to the LiteLLM master key (LiteLLM is OpenAI-compatible). Repoint both to agentgateway at cutover.
- Step 2: Scale the Deployment up
In argocd/app-configs/octopus/deployment.yaml, change replicas: 0 → replicas: 1.
- Step 3: Sync + verify the migrator ran
Run: kubectl -n octopus get pods then kubectl -n octopus logs deploy/octopus -c migrate
Expected: the init container logs prisma migrate deploy applying migrations; the web container reaches Ready.
- Step 4: Commit (
feat(octopus): go-live (replicas=1)).
Task 14: Register models + org defaults
Section titled “Task 14: Register models + org defaults”Files: none (admin UI / runbook; config persists in the octopus DB, which is backed up via CNPG).
- Step 1: First login
Browse to https://octopus.fzymgc.house (on LAN/Tailscale), sign in with GitHub OAuth. Your email is in ADMIN_EMAILS, granting admin.
- Step 2: Register the review model (provider=openai)
In admin model settings, add an availableModel: category=llm, provider=openai, modelId=<agentgateway review alias> (an OpenRouter-backed alias agentgateway maps; must not contain codex — ai-router.usesResponsesApi() routes *codex* ids to the Responses API, which agentgateway does not serve). Mark it active and the org default review model.
- Step 3: Register the embedding model (provider=openai)
Add an availableModel: category=embedding, provider=openai, modelId=<agentgateway bge-m3 alias>. Mark it active and the org default embed model. (Embeddings flow through embeddings.ts → openai SDK → OPENAI_BASE_URL → agentgateway → Ollama bge-m3, 1024-dim, matching EMBEDDING_DIM.)
- Step 4: Do NOT set an org Anthropic key
Leave anthropicApiKey unset so ai-router never routes reviews to api.anthropic.com (agentgateway is OpenAI-compatible, not Anthropic-Messages).
Task 15: End-to-end validation
Section titled “Task 15: End-to-end validation”Files: none.
- Step 1: Webhook delivery
Open a trivial PR on fzymgc-house/selfhosted-cluster. In the GitHub App → Advanced → Recent Deliveries, confirm the pull_request delivery returns 200.
- Step 2: Review posts
Within a few minutes, confirm Octopus posts inline review comments with severity on the PR, and a Check Run appears.
- Step 3: Confirm Qdrant collection dimension
Run: kubectl -n octopus exec deploy/qdrant-octopus -- wget -qO- http://localhost:6333/collections/code_chunks
Expected: the collection exists with vector size 1024 (proves EMBEDDING_DIM took effect and the index matches bge-m3).
- Step 4: Confirm ZDR / gateway routing
In Grafana/agentgateway metrics (per fzymgc-house:grafana), confirm review + embedding calls show up with agentgateway provider/model labels and no direct api.openai.com/api.anthropic.com egress occurred.
Task 16: Documentation
Section titled “Task 16: Documentation”Files:
- Modify:
docs/reference/secrets.md - Modify:
docs/reference/services.md - Create:
docs/operations/octopus.md - Step 1: Secrets reference
Add a row for secret/fzymgc-house/cluster/octopus (keys: better_auth_secret, octopus_data_key, github_app_*, github_client_*, admin_emails, openai_api_key) and secret/fzymgc-house/cluster/postgres/users/main-octopus.
- Step 2: Services catalog
Add Octopus: internal UI https://octopus.fzymgc.house, public webhook https://octopus-wh.fzymgc.net/api/github/webhook, namespace octopus, image ghcr.io/seanb4t/octopus (fork), backing Qdrant + CNPG octopus DB.
- Step 3: Operations runbook
docs/operations/octopus.md: how reviews flow, where models are configured (admin UI), how to rotate the agentgateway VK, how to add more repos (install the App), the fork/upstream status, and rollback (scale to 0; uninstall the App; data isolated to the octopus DB + qdrant-octopus-data PVC).
- Step 4: Lint + commit
Run: rumdl check docs/ (use CI’s pinned v0.1.15).
Expected: no issues. Commit (docs(octopus): secrets + services + runbook).