Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,34 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

publish-chart:
name: Package & push Helm chart to GHCR
runs-on: ubuntu-latest
needs: [publish]
if: startsWith(github.ref, 'refs/tags/')
steps:
- uses: actions/checkout@v4

- uses: azure/setup-helm@v4

- name: Derive chart version
id: version
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"

- name: Log in to GHCR (Helm OCI)
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io \
--username "${{ github.actor }}" \
--password-stdin

- name: Package chart
run: |
helm package charts/openconcho \
--version "${{ steps.version.outputs.VERSION }}" \
--app-version "${{ steps.version.outputs.VERSION }}"

- name: Push chart
run: |
helm push "openconcho-${{ steps.version.outputs.VERSION }}.tgz" \
oci://ghcr.io/${{ github.repository_owner }}/charts
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Frontend UI for self-hosted Honcho instances — browse memories, peers, session
| `packages/web/src/test/` | Vitest unit/integration tests + setup |
| `packages/web/e2e/` | Playwright e2e specs |
| `packages/desktop/` | Tauri shell that bundles the built web app |
| `charts/openconcho/` | Helm 3 chart for self-hosting on Kubernetes (OCI artifact on GHCR) |
| `.claude/rules/` | Coding conventions (auto-loaded; stack-agnostic, applies to all agents) |
| `docs/` | Architecture and references |

Expand Down
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,34 @@ profiles: `make up` runs the `dev` profile (`build: .`), `make prod` runs the
(comma-separated host globs) for when you expose the proxy. Full details and env
vars are in [`docs/docker.md`](docs/docker.md).

### Kubernetes (Helm)

The chart is published as an OCI artifact to GHCR on every tagged release.

```bash
helm install openconcho oci://ghcr.io/offendingcommit/charts/openconcho \
--version 0.14.0 \
--create-namespace --namespace openconcho \
--set honcho.defaultUrl=https://honcho.example.com
```

Enable an Ingress and TLS:

```bash
helm install openconcho oci://ghcr.io/offendingcommit/charts/openconcho \
--version 0.14.0 \
--create-namespace --namespace openconcho \
--set honcho.defaultUrl=https://honcho.example.com \
--set ingress.enabled=true \
--set ingress.className=nginx \
--set 'ingress.hosts[0].host=openconcho.example.com' \
--set 'ingress.hosts[0].paths[0].path=/' \
--set 'ingress.tls[0].secretName=openconcho-tls' \
--set 'ingress.tls[0].hosts[0]=openconcho.example.com'
```

Full chart documentation, configuration reference, and an ArgoCD Application example are in [`charts/openconcho/README.md`](charts/openconcho/README.md).

### Connecting to your instance

1. Enter the base URL of your Honcho instance (e.g. `http://localhost:8000`)
Expand Down
16 changes: 16 additions & 0 deletions charts/openconcho/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
apiVersion: v2
name: openconcho
description: Self-hosted UI for Honcho — browse memories, peers, sessions, conclusions, and chat with memory context.
type: application
version: 0.14.0
appVersion: "0.14.0"
keywords:
- honcho
- memory
- ai
home: https://github.com/offendingcommit/openconcho
sources:
- https://github.com/offendingcommit/openconcho
maintainers:
- name: offendingcommit
url: https://github.com/offendingcommit
229 changes: 229 additions & 0 deletions charts/openconcho/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
# openconcho Helm Chart

Helm 3 chart for self-hosting the [openconcho](https://github.com/offendingcommit/openconcho) web UI on Kubernetes.

The chart deploys a single nginx-unprivileged container (port 8080, UID 101) that serves the React SPA and reverse-proxies Honcho API calls under `/api` to avoid browser CORS issues.

## Prerequisites

- Kubernetes 1.25+
- Helm 3.10+
- A running [Honcho](https://github.com/plastic-labs/honcho) instance reachable from within the cluster (or via a configured ingress)

## Installing

Add the chart repository:

```bash
helm registry login ghcr.io --username <github-username> --password <github-token>
```

Install the chart:

```bash
helm install openconcho oci://ghcr.io/offendingcommit/charts/openconcho \
--version 0.14.0 \
--set honcho.defaultUrl=https://honcho.example.com
```

Or with a values file (recommended):

```bash
helm install openconcho oci://ghcr.io/offendingcommit/charts/openconcho \
--version 0.14.0 \
-f my-values.yaml
```

## Upgrading

```bash
helm upgrade openconcho oci://ghcr.io/offendingcommit/charts/openconcho \
--version <new-version> \
-f my-values.yaml
```

## Uninstalling

```bash
helm uninstall openconcho
```

## Configuration

All values with their defaults are documented in [`values.yaml`](values.yaml). Key options:

| Value | Default | Description |
|---|---|---|
| `replicaCount` | `1` | Number of pod replicas |
| `image.repository` | `ghcr.io/offendingcommit/openconcho-web` | Container image |
| `image.tag` | `""` | Tag; defaults to chart `appVersion` |
| `image.pullPolicy` | `IfNotPresent` | Image pull policy |
| `honcho.defaultUrl` | `""` | Honcho URL pre-seeded in the UI |
| `honcho.upstreamAllowlist` | `""` | SSRF guard (comma-separated host globs) |
| `service.type` | `ClusterIP` | `ClusterIP` / `NodePort` / `LoadBalancer` |
| `service.port` | `80` | Service port |
| `ingress.enabled` | `false` | Enable Ingress resource |
| `ingress.className` | `""` | IngressClass name |
| `autoscaling.enabled` | `false` | Enable HorizontalPodAutoscaler |
| `podDisruptionBudget.enabled` | `false` | Enable PodDisruptionBudget |
| `networkPolicy.enabled` | `false` | Enable NetworkPolicy (same-namespace only) |
| `resources.requests.memory` | `32Mi` | Memory request |
| `resources.limits.memory` | `128Mi` | Memory limit |

## Examples

### Minimal (ClusterIP, no ingress)

```yaml
honcho:
defaultUrl: http://honcho.honcho.svc.cluster.local:8000
```

### With Ingress and TLS (cert-manager)

```yaml
honcho:
defaultUrl: https://honcho.example.com

ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: openconcho.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: openconcho-tls
hosts:
- openconcho.example.com
```

### With autoscaling and disruption budget

```yaml
replicaCount: 2

autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 70

podDisruptionBudget:
enabled: true
minAvailable: 1
```

### With NetworkPolicy

> **Note:** When `networkPolicy.enabled=true` and `ingress.enabled=true`, you must add
> a policy that allows traffic from the ingress-controller namespace. Run
> `helm status <release>` for the exact `kubectl edit` command after install.

```yaml
networkPolicy:
enabled: true

ingress:
enabled: true
className: nginx
hosts:
- host: openconcho.example.com
paths:
- path: /
pathType: Prefix
```

### Private registry

```yaml
image:
repository: registry.example.com/myorg/openconcho-web
tag: "0.14.0"
pullPolicy: Always

imagePullSecrets:
- name: registry-credentials
```

## ArgoCD Application

```yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: openconcho
namespace: argocd
spec:
project: default
source:
repoURL: ghcr.io/offendingcommit/charts
chart: openconcho
targetRevision: 0.14.0
helm:
valuesObject:
honcho:
defaultUrl: https://honcho.example.com
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: openconcho.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: openconcho-tls
hosts:
- openconcho.example.com
destination:
server: https://kubernetes.default.svc
namespace: openconcho
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
```

> OCI chart sources require ArgoCD 2.10+ (OCI Helm support GA).

## Helm tests

After install, run the bundled tests to verify the deployment is healthy:

```bash
helm test openconcho
```

Two test pods run and exit 0 on success:

| Test | What it checks |
|---|---|
| `test-healthz` | `GET /healthz` body equals `ok` |
| `test-spa-root` | `GET /` returns HTTP 200 |

Pass `--logs` to see output from failing pods:

```bash
helm test openconcho --logs
```

## Security posture

| Control | Value |
|---|---|
| Run as UID/GID | 101 (nginx-unprivileged) |
| `runAsNonRoot` | `true` |
| `readOnlyRootFilesystem` | `true` |
| Linux capabilities | all dropped |
| `seccompProfile` | `RuntimeDefault` |
| `allowPrivilegeEscalation` | `false` |
| `automountServiceAccountToken` | `false` |
| Writable paths | `/var/cache/nginx`, `/var/run`, `/tmp` (tmpfs) |
40 changes: 40 additions & 0 deletions charts/openconcho/templates/NOTES.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
OpenConcho {{ .Chart.AppVersion }} deployed to namespace {{ .Release.Namespace }}.

{{- if .Values.ingress.enabled }}
Access:
{{- range .Values.ingress.hosts }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ .host }}
{{- end }}
{{- else if eq .Values.service.type "NodePort" }}
Access (NodePort):
export NODE_PORT=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "openconcho.fullname" . }} -o jsonpath="{.spec.ports[0].nodePort}")
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo "http://$NODE_IP:$NODE_PORT"
{{- else if eq .Values.service.type "LoadBalancer" }}
Access (LoadBalancer — IP may take a few minutes):
export LB_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "openconcho.fullname" . }} --template '{{ "{{" }}range (index .status.loadBalancer.ingress 0){{ "}}" }}{{ "{{" }}.{{ "}}" }}{{ "{{" }}end{{ "}}" }}')
echo "http://$LB_IP:{{ .Values.service.port }}"
{{- else }}
Access (port-forward):
kubectl port-forward --namespace {{ .Release.Namespace }} svc/{{ include "openconcho.fullname" . }} 8080:{{ .Values.service.port }}
Then open http://localhost:8080
{{- end }}

Run Helm tests to verify the deployment:
helm test {{ .Release.Name }}

{{- if and .Values.networkPolicy.enabled .Values.ingress.enabled }}

WARNING: NetworkPolicy + Ingress are both enabled.
The default NetworkPolicy allows port {{ .Values.service.containerPort }} only from pods within
namespace {{ .Release.Namespace }}. Ingress controllers typically run in a separate
namespace (ingress-nginx, kube-system, etc.) and will be blocked.
To allow ingress-controller traffic, add a namespaceSelector rule:

kubectl edit networkpolicy --namespace {{ .Release.Namespace }} {{ include "openconcho.fullname" . }}

# Under spec.ingress[0].from, add:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: <ingress-controller-namespace>
{{- end }}
Loading
Loading