Skip to content

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.


In the seanb4t/octopus fork (separate repo):

  • apps/web/lib/qdrant.ts — change the hardcoded VECTOR_SIZE to read EMBEDDING_DIM.
  • apps/web/lib/qdrant.ts — pass apiKey: process.env.QDRANT_API_KEY to the client.
  • apps/web/Dockerfile — add a migrator build stage (bunx prisma migrate deploy).
  • packages/db/package.json — add a db:deploy script (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 — CNPG Database CR.
  • argocd/app-configs/cnpg/users-octopus.yaml — role-credential ExternalSecret.
  • argocd/app-configs/cnpg/postgres-cluster.yaml — add octopus to managed.roles (Modify).
  • argocd/app-configs/cnpg/kustomization.yaml — register the two new files (Modify).
  • argocd/app-configs/octopus/namespace.yaml
  • argocd/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.yaml
  • argocd/app-configs/octopus/certificate.yamloctopus-tls (vault-issuer).
  • argocd/app-configs/octopus/ingress.yaml — internal UI IngressRoute.
  • argocd/app-configs/octopus/kustomization.yaml
  • argocd/cluster-app/templates/octopus.yaml — ArgoCD Application.
  • docs/reference/secrets.md, docs/reference/services.md — docs (Modify).
  • docs/operations/octopus.md — runbook (Create).

Files (in the seanb4t/octopus fork):

  • Modify: apps/web/lib/qdrant.ts
  • Modify: packages/db/package.json
  • Step 1: Fork and clone
Terminal window
gh repo fork octopusreview/octopus --org-or-user seanb4t --clone
cd octopus
git checkout -b feat/configurable-embedding-dim
  • Step 2: Make VECTOR_SIZE configurable

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
Terminal window
git add apps/web/lib/qdrant.ts packages/db/package.json
git 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 migrator
WORKDIR /app/packages/db
CMD ["bun", "run", "db:deploy"]

Prisma 7 note: migrate deploy reads the datasource from the schema’s env("DATABASE_URL") — the --url CLI flag was removed in v7. The migrator therefore needs only the DATABASE_URL env var set (Task 9 initContainer), no --url argument.

  • 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
Terminal window
git add apps/web/Dockerfile
git commit -m "build: add migrator image stage running prisma migrate deploy"

Files (in the fork):

  • Create: .github/workflows/build-image.yaml

  • Step 1: Write the workflow

name: build-image
on:
push:
branches: [feat/configurable-embedding-dim, master]
permissions:
contents: read
packages: write
jobs:
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
Terminal window
git push -u origin feat/configurable-embedding-dim
gh run watch

Expected: 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
Terminal window
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 to managed.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:

Terminal window
vault kv put secret/fzymgc-house/cluster/postgres/users/main-octopus \
username=octopus password="$(openssl rand -base64 24)"
  • Step 2: Create the Database CR

argocd/app-configs/cnpg/db-octopus.yaml:

---
apiVersion: postgresql.cnpg.io/v1
kind: Database
metadata:
name: octopus
namespace: postgres
spec:
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/v1
kind: ExternalSecret
metadata:
name: main-octopus-credentials
namespace: postgres
spec:
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).


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: v1
kind: Namespace
metadata:
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: v1
kind: PersistentVolumeClaim
metadata:
name: qdrant-octopus-data
namespace: octopus
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: longhorn-encrypted
resources:
requests:
storage: 20Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: qdrant-octopus
namespace: octopus
labels:
app.kubernetes.io/name: qdrant-octopus
spec:
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: v1
kind: Service
metadata:
name: qdrant-octopus
namespace: octopus
spec:
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).

Files: none (manual + Vault).

  • Step 1: Create the GitHub App

In GitHub → Settings → Developer settings → GitHub Apps → New:

  • Name: octopus-fzymgc (slug becomes NEXT_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
Terminal window
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).


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/v1
kind: ExternalSecret
metadata:
name: octopus-db
namespace: octopus
spec:
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/v1
kind: ExternalSecret
metadata:
name: octopus-app
namespace: octopus
spec:
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: Validatekubectl 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/v1
kind: Deployment
metadata:
name: octopus
namespace: octopus
labels:
app.kubernetes.io/name: octopus
spec:
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: v1
kind: Service
metadata:
name: octopus
namespace: octopus
spec:
selector:
app.kubernetes.io/name: octopus
ports:
- name: http
port: 3000
targetPort: 3000
  • Step 4: Validatekubectl 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)).


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/v1
kind: Certificate
metadata:
name: octopus-tls
namespace: octopus
spec:
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/v1alpha1
kind: IngressRoute
metadata:
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: Validatekubectl 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/v1beta1
kind: 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/v1alpha1
kind: Application
metadata:
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 octopus to webhook_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:

Terminal window
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: 0replicas: 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)).

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 codexai-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).


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.


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).