Skip to content

Commit 6746a01

Browse files
committed
feat: add Claude Code UI deployment with Keycloak OAuth
- Dockerfile for building claudecodeui image - Kubernetes manifests (deployment, service, PVCs) - OAuth2-proxy for Keycloak authentication - ArgoCD applications - Keycloak client setup job - HTTPRoute for claude.etcd.me - DNS record in Terraform - Secrets template entries
1 parent d47dad2 commit 6746a01

16 files changed

Lines changed: 569 additions & 1 deletion

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Claude Code UI - Web interface for Claude Code CLI
2+
apiVersion: argoproj.io/v1alpha1
3+
kind: Application
4+
metadata:
5+
name: claudecodeui
6+
namespace: argocd
7+
annotations:
8+
argocd.argoproj.io/sync-wave: "5"
9+
spec:
10+
project: default
11+
source:
12+
repoURL: https://github.com/sofianedjerbi/homelab
13+
targetRevision: HEAD
14+
path: argocd/base/resources/claudecodeui
15+
destination:
16+
server: https://kubernetes.default.svc
17+
namespace: claudecodeui
18+
syncPolicy:
19+
automated:
20+
prune: true
21+
selfHeal: true
22+
syncOptions:
23+
- CreateNamespace=true
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# oauth2-proxy for Claude Code UI authentication via Keycloak
2+
apiVersion: argoproj.io/v1alpha1
3+
kind: Application
4+
metadata:
5+
name: claudecodeui-oauth2-proxy
6+
namespace: argocd
7+
annotations:
8+
argocd.argoproj.io/sync-wave: "5"
9+
spec:
10+
project: default
11+
source:
12+
repoURL: https://oauth2-proxy.github.io/manifests
13+
chart: oauth2-proxy
14+
targetRevision: 7.12.6
15+
helm:
16+
valuesObject:
17+
replicaCount: 1
18+
config:
19+
clientID: claudecodeui
20+
cookieSecret: "" # Will be set from existingSecret
21+
existingSecret: claudecodeui-oauth2-proxy
22+
extraArgs:
23+
provider: keycloak-oidc
24+
oidc-issuer-url: https://auth.cluster.local/realms/homelab
25+
oidc-extra-audience: account
26+
code-challenge-method: S256
27+
email-domain: "*"
28+
cookie-secure: "true"
29+
upstream: "http://claudecodeui.claudecodeui.svc.cluster.local:3001"
30+
http-address: "0.0.0.0:4180"
31+
reverse-proxy: "true"
32+
skip-provider-button: "true"
33+
pass-access-token: "true"
34+
pass-authorization-header: "true"
35+
set-authorization-header: "true"
36+
set-xauthrequest: "true"
37+
service:
38+
type: ClusterIP
39+
portNumber: 4180
40+
resources:
41+
requests:
42+
cpu: 10m
43+
memory: 32Mi
44+
limits:
45+
cpu: 100m
46+
memory: 64Mi
47+
destination:
48+
server: https://kubernetes.default.svc
49+
namespace: claudecodeui
50+
syncPolicy:
51+
automated:
52+
prune: true
53+
selfHeal: true
54+
syncOptions:
55+
- CreateNamespace=true

argocd/base/kustomization.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ resources:
3636
- apps/playwright-mcp/playwright-mcp.yaml
3737
# n8n MCP server
3838
- apps/n8n-mcp/n8n-mcp.yaml
39+
# Claude Code UI
40+
- apps/claudecodeui/claudecodeui.yaml
41+
- apps/claudecodeui/oauth2-proxy.yaml
3942
# Resources (only those NOT managed by child apps)
4043
- resources/cert-manager/cluster-issuer.yaml
4144
# Keycloak via operator CRDs (no child app for these)
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Claude Code UI deployment
2+
apiVersion: apps/v1
3+
kind: Deployment
4+
metadata:
5+
name: claudecodeui
6+
namespace: claudecodeui
7+
labels:
8+
app.kubernetes.io/name: claudecodeui
9+
annotations:
10+
reloader.stakater.com/auto: "true"
11+
spec:
12+
replicas: 1
13+
selector:
14+
matchLabels:
15+
app.kubernetes.io/name: claudecodeui
16+
template:
17+
metadata:
18+
labels:
19+
app.kubernetes.io/name: claudecodeui
20+
spec:
21+
securityContext:
22+
runAsNonRoot: true
23+
runAsUser: 1000
24+
runAsGroup: 1000
25+
fsGroup: 1000
26+
containers:
27+
- name: claudecodeui
28+
image: ghcr.io/sofianedjerbi/claudecodeui:latest
29+
ports:
30+
- containerPort: 3001
31+
name: http
32+
protocol: TCP
33+
env:
34+
- name: PORT
35+
value: "3001"
36+
- name: NODE_ENV
37+
value: "production"
38+
- name: DATABASE_PATH
39+
value: "/data/auth.db"
40+
# Anthropic API key for Claude CLI
41+
- name: ANTHROPIC_API_KEY
42+
valueFrom:
43+
secretKeyRef:
44+
name: claudecodeui-secrets
45+
key: anthropic-api-key
46+
resources:
47+
requests:
48+
cpu: 100m
49+
memory: 256Mi
50+
limits:
51+
cpu: "1"
52+
memory: 1Gi
53+
volumeMounts:
54+
- name: data
55+
mountPath: /data
56+
- name: claude-home
57+
mountPath: /home/node/.claude
58+
livenessProbe:
59+
httpGet:
60+
path: /api/health
61+
port: 3001
62+
initialDelaySeconds: 30
63+
periodSeconds: 30
64+
timeoutSeconds: 10
65+
readinessProbe:
66+
httpGet:
67+
path: /api/health
68+
port: 3001
69+
initialDelaySeconds: 10
70+
periodSeconds: 10
71+
timeoutSeconds: 5
72+
volumes:
73+
- name: data
74+
persistentVolumeClaim:
75+
claimName: claudecodeui-data
76+
- name: claude-home
77+
persistentVolumeClaim:
78+
claimName: claudecodeui-claude
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Claude Code UI resources kustomization
2+
apiVersion: kustomize.config.k8s.io/v1beta1
3+
kind: Kustomization
4+
5+
resources:
6+
- namespace.yaml
7+
- deployment.yaml
8+
- service.yaml
9+
- pvc.yaml
10+
- route.yaml
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Claude Code UI namespace
2+
apiVersion: v1
3+
kind: Namespace
4+
metadata:
5+
name: claudecodeui
6+
labels:
7+
app.kubernetes.io/name: claudecodeui
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Claude Code UI persistent storage
2+
---
3+
apiVersion: v1
4+
kind: PersistentVolumeClaim
5+
metadata:
6+
name: claudecodeui-data
7+
namespace: claudecodeui
8+
labels:
9+
app.kubernetes.io/name: claudecodeui
10+
spec:
11+
accessModes:
12+
- ReadWriteOnce
13+
resources:
14+
requests:
15+
storage: 1Gi
16+
---
17+
apiVersion: v1
18+
kind: PersistentVolumeClaim
19+
metadata:
20+
name: claudecodeui-claude
21+
namespace: claudecodeui
22+
labels:
23+
app.kubernetes.io/name: claudecodeui
24+
spec:
25+
accessModes:
26+
- ReadWriteOnce
27+
resources:
28+
requests:
29+
storage: 5Gi
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Claude Code UI HTTPRoute - routes through oauth2-proxy for authentication
2+
apiVersion: gateway.networking.k8s.io/v1
3+
kind: HTTPRoute
4+
metadata:
5+
name: claudecodeui-route
6+
namespace: claudecodeui
7+
annotations:
8+
argocd.argoproj.io/sync-wave: "6"
9+
spec:
10+
parentRefs:
11+
- name: main-gateway
12+
namespace: default
13+
sectionName: https
14+
hostnames:
15+
- claude.cluster.local
16+
rules:
17+
# Health check endpoint - bypasses oauth2-proxy
18+
- matches:
19+
- path:
20+
type: Exact
21+
value: /api/health
22+
backendRefs:
23+
- name: claudecodeui
24+
port: 3001
25+
# SSO logout redirect
26+
- matches:
27+
- path:
28+
type: Exact
29+
value: /signout
30+
filters:
31+
- type: RequestRedirect
32+
requestRedirect:
33+
path:
34+
type: ReplaceFullPath
35+
replaceFullPath: /oauth2/sign_out
36+
statusCode: 302
37+
# All other requests go to oauth2-proxy
38+
- matches:
39+
- path:
40+
type: PathPrefix
41+
value: /
42+
backendRefs:
43+
- name: claudecodeui-oauth2-proxy
44+
port: 4180
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Claude Code UI service
2+
apiVersion: v1
3+
kind: Service
4+
metadata:
5+
name: claudecodeui
6+
namespace: claudecodeui
7+
labels:
8+
app.kubernetes.io/name: claudecodeui
9+
spec:
10+
type: ClusterIP
11+
ports:
12+
- port: 3001
13+
targetPort: 3001
14+
protocol: TCP
15+
name: http
16+
selector:
17+
app.kubernetes.io/name: claudecodeui
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# Job to create claudecodeui client in Keycloak - etcd.me overlay
2+
apiVersion: batch/v1
3+
kind: Job
4+
metadata:
5+
name: keycloak-claudecodeui-client-init
6+
namespace: keycloak
7+
annotations:
8+
argocd.argoproj.io/sync-wave: "4"
9+
spec:
10+
ttlSecondsAfterFinished: 300
11+
template:
12+
spec:
13+
restartPolicy: OnFailure
14+
securityContext:
15+
runAsNonRoot: true
16+
seccompProfile:
17+
type: RuntimeDefault
18+
containers:
19+
- name: setup
20+
image: quay.io/keycloak/keycloak:26.0
21+
securityContext:
22+
allowPrivilegeEscalation: false
23+
readOnlyRootFilesystem: true
24+
capabilities:
25+
drop: ["ALL"]
26+
volumeMounts:
27+
- name: tmp
28+
mountPath: /tmp
29+
- name: kcadm-config
30+
mountPath: /opt/keycloak/.keycloak
31+
resources:
32+
requests:
33+
cpu: 50m
34+
memory: 128Mi
35+
limits:
36+
cpu: 200m
37+
memory: 256Mi
38+
command:
39+
- /bin/bash
40+
- -c
41+
- |
42+
set -e
43+
44+
# Wait for Keycloak to be ready (retry login)
45+
for i in $(seq 1 30); do
46+
if /opt/keycloak/bin/kcadm.sh config credentials \
47+
--server http://keycloak-service.keycloak.svc.cluster.local:8080 \
48+
--realm master \
49+
--user "$KEYCLOAK_ADMIN" \
50+
--password "$KEYCLOAK_ADMIN_PASSWORD"; then
51+
echo "Logged in to Keycloak"
52+
break
53+
fi
54+
echo "Waiting for Keycloak... ($i/30)"
55+
sleep 5
56+
done
57+
58+
# Verify login worked
59+
/opt/keycloak/bin/kcadm.sh config credentials \
60+
--server http://keycloak-service.keycloak.svc.cluster.local:8080 \
61+
--realm master \
62+
--user "$KEYCLOAK_ADMIN" \
63+
--password "$KEYCLOAK_ADMIN_PASSWORD"
64+
65+
# Check if client already exists
66+
if /opt/keycloak/bin/kcadm.sh get clients -r homelab -q clientId=claudecodeui | grep -q '"clientId" : "claudecodeui"'; then
67+
echo "claudecodeui client already exists, updating..."
68+
CLIENT_ID=$(/opt/keycloak/bin/kcadm.sh get clients -r homelab -q clientId=claudecodeui --fields id | grep -o '"id" : "[^"]*"' | cut -d'"' -f4)
69+
/opt/keycloak/bin/kcadm.sh update clients/$CLIENT_ID -r homelab \
70+
-s "secret=$CLAUDECODEUI_CLIENT_SECRET" \
71+
-s 'attributes={"pkce.code.challenge.method":"S256","post.logout.redirect.uris":"https://claude.etcd.me/*"}'
72+
echo "Client updated"
73+
else
74+
echo "Creating claudecodeui client..."
75+
/opt/keycloak/bin/kcadm.sh create clients -r homelab \
76+
-s clientId=claudecodeui \
77+
-s name="Claude Code UI" \
78+
-s enabled=true \
79+
-s publicClient=false \
80+
-s clientAuthenticatorType=client-secret \
81+
-s "secret=$CLAUDECODEUI_CLIENT_SECRET" \
82+
-s 'redirectUris=["https://claude.etcd.me/oauth2/callback"]' \
83+
-s 'webOrigins=["https://claude.etcd.me"]' \
84+
-s standardFlowEnabled=true \
85+
-s directAccessGrantsEnabled=false \
86+
-s protocol=openid-connect \
87+
-s 'attributes={"pkce.code.challenge.method":"S256","post.logout.redirect.uris":"https://claude.etcd.me/*"}'
88+
echo "claudecodeui client created"
89+
fi
90+
91+
echo "Done!"
92+
env:
93+
- name: KEYCLOAK_ADMIN
94+
valueFrom:
95+
secretKeyRef:
96+
name: keycloak-initial-admin
97+
key: username
98+
- name: KEYCLOAK_ADMIN_PASSWORD
99+
valueFrom:
100+
secretKeyRef:
101+
name: keycloak-initial-admin
102+
key: password
103+
- name: CLAUDECODEUI_CLIENT_SECRET
104+
valueFrom:
105+
secretKeyRef:
106+
name: claudecodeui-client-secret
107+
key: client-secret
108+
volumes:
109+
- name: tmp
110+
emptyDir: {}
111+
- name: kcadm-config
112+
emptyDir: {}

0 commit comments

Comments
 (0)