Deploy OpenCode as a web application on Red Hat OpenShift, secured with OpenShift OAuth Proxy and backed by a vLLM inference server running on the same cluster.
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ OpenShift Cluster │
│ │
│ Namespace: opencode Namespace: vllm (or KServe / RHOAI) │
│ ┌───────────────────────────────────────┐ ┌─────────────────────────────────────┐ │
│ │ │ │ │ │
│ │ ┌─────────────────────────────────┐ │ │ ┌───────────────────────────────┐ │ │
│ │ │ Route (reencrypt TLS) │ │ │ │ vLLM Inference Server │ │ │
│ │ │ opencode-web │ │ │ │ │ │ │
│ │ └──────────────┬──────────────────┘ │ │ │ ◆ OpenAI-compatible API │ │ │
│ │ │ │ │ │ ◆ /v1/chat/completions │ │ │
│ │ ▼ :8443 │ │ │ ◆ GPU-accelerated │ │ │
│ │ ┌─────────────────────────────────┐ │ │ │ │ │ │
│ │ │ Service (ClusterIP) │ │ │ └───────────────┬───────────────┘ │ │
│ │ │ opencode-web │ │ │ │ │ │
│ │ └──────────────┬──────────────────┘ │ │ ┌───────────────┴───────────────┐ │ │
│ │ │ │ │ │ Service (ClusterIP) │ │ │
│ │ ┌──────────────┴──────────────────┐ │ │ │ e.g. vllm-svc:80/v1 │ │ │
│ │ │ Pod: opencode-web │ │ │ └───────────────────────────────┘ │ │
│ │ │ │ │ │ │ │
│ │ │ ┌──────────────┐ ┌──────────┐ │ │ └─────────────────────────────────────┘ │
│ │ │ │ OAuth Proxy │ │ OpenCode │ │ │ ▲ │
│ │ │ │ :8443 (TLS) │─│ Web │ │ │ │ │
│ │ │ │ │ │ :8003 │──┼──┼── API calls ───────┘ │
│ │ │ │ ◆ OCP auth │ │ │ │ │ (cluster-internal) │
│ │ │ │ ◆ SAR check │ │ ◆ UI │ │ │ │
│ │ │ │ ◆ Cookie │ │ ◆ Agent │ │ │ │
│ │ │ └──────────────┘ └────┬─────┘ │ │ │
│ │ │ │ │ │ │
│ │ │ ┌─────────┴──────┐ │ │ │
│ │ │ │ PVC (10Gi) │ │ │ │
│ │ │ │ /home/opencode │ │ │ │
│ │ │ │ /workspace │ │ │ │
│ │ │ └────────────────┘ │ │ │
│ │ └─────────────────────────────────┘ │ │
│ │ │ │
│ └───────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────┘
▲
│ HTTPS
│
┌────┴────┐
│ User │
│ Browser │
└─────────┘
- A vLLM inference server runs on the same OpenShift cluster (e.g. via KServe, RHOAI, or a standalone Deployment with GPU nodes), exposing an OpenAI-compatible API on a cluster-internal Service.
- A user navigates to the OpenShift Route in their browser.
- The OAuth Proxy sidecar intercepts the request, authenticates the user against the OpenShift OAuth server, and enforces RBAC (the user must have
getaccess toservicesin theopencodenamespace). - Once authenticated, the request is forwarded over
localhostto the OpenCode Web container on port8003. - OpenCode Web serves the coding assistant UI and sends inference requests to the vLLM service over the cluster-internal network -- traffic never leaves the cluster.
- The workspace is persisted on a PVC so files survive pod restarts.
- OpenShift 4.x cluster with cluster-admin access (or sufficient RBAC to create namespaces, service accounts, routes, and secrets)
ocCLI installed and authenticated (oc login)- A vLLM inference server already running on the cluster (e.g. deployed via KServe, Red Hat OpenShift AI, or a standalone Deployment on GPU nodes) with a reachable cluster-internal Service URL
.
├── LICENSE # Apache License 2.0
├── README.md # This file
├── manifests/ # Base resources (web mode with OAuth proxy)
│ ├── kustomization.yaml
│ ├── namespace.yaml
│ ├── serviceaccount.yaml
│ ├── entrypoint.sh # Container entrypoint (config substitution, MCP merge, git init)
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── route.yaml
│ ├── pvc.yaml
│ └── config-template.json
└── overlays/
├── cli/ # CLI mode — no OAuth proxy, attach via oc exec
│ ├── kustomization.yaml
│ ├── deployment-patch.yaml
│ └── remove-route.yaml
└── example/ # Template for user customization (namespace, model, storage)
└── kustomization.yaml
# 1. Clone the repository
git clone https://github.com/aicatalyst-team/opencode-openshift.git
cd opencode-openshift
# 2. Configure the vLLM endpoint (see "Configuration" below)
# Edit manifests/kustomization.yaml with your vLLM service URL and model
# 3. Deploy to OpenShift
oc apply -k manifests/
# 4. Wait for the rollout
oc -n opencode rollout status deployment/opencode-web
# 5. Get the route URL
oc -n opencode get route opencode-web -o jsonpath='https://{.spec.host}{"\n"}'Open the printed URL in your browser. You will be redirected to the OpenShift login page, and after authentication you will land in the OpenCode web UI.
The pre-built image is published at quay.io/opendatahub/odh-opencode-rhel9:latest. It is based on UBI 9 minimal and contains OpenCode v1.4.4 built from source.
| Layer | Purpose |
|---|---|
| UBI 9 minimal base | RHEL-compatible minimal image |
| OpenCode v1.4.4 | Built from source (opendatahub-io/opencode) |
| ca-certificates, git, diffutils, findutils, gzip, jq, make, openssh-clients, patch, procps-ng, tar, vim-minimal, which | Common CLI tools for development workflows |
| Python 3 + uv | Python environment and fast package manager |
All runtime configuration is managed through the manifests/kustomization.yaml file.
Edit the secretGenerator section to point to your cluster-internal vLLM service:
secretGenerator:
- name: opencode-web-secret
literals:
- BASE_URL=http://<vllm-service>.<namespace>.svc.cluster.local/v1 # Cluster-internal vLLM URL
- API_KEY=<your-api-key> # API key (use "token" if auth is disabled)
- MODEL_NAME=<your-model-name> # Model loaded in vLLM (e.g. RedHatAI/Qwen3-Next-80B-A3B-Instruct-FP8)
options:
disableNameSuffixHash: trueThe BASE_URL should use the cluster-internal DNS name of your vLLM Service (e.g. http://vllm-svc.vllm.svc.cluster.local/v1). If vLLM is served via KServe or RHOAI, use the internal InferenceService URL.
These values are injected into the pod as environment variables and substituted into the config template at startup.
Generate a new session secret for the OAuth Proxy:
# Generate a random base64-encoded 32-byte secret
python3 -c "import os, base64; print(base64.b64encode(os.urandom(32)).decode())"Replace the session_secret value in kustomization.yaml:
- name: opencode-web-proxy-cookie
type: Opaque
literals:
- session_secret=<your-generated-secret>
options:
disableNameSuffixHash: trueThe default PVC requests 10Gi. The storage class is configured via a Kustomize patch in manifests/kustomization.yaml. Update the patches section to match your cluster's storage class:
patches:
- target:
kind: PersistentVolumeClaim
name: opencode-web-pvc
patch: |-
- op: add
path: /spec/storageClassName
value: <your-storage-class> # e.g. gp3-csi, ocs-storagecluster-cephfsThe manifests/config-template.json defines the OpenCode provider configuration. It uses the @ai-sdk/openai-compatible SDK which maps directly to vLLM's OpenAI-compatible API. Placeholders (${BASE_URL}, ${API_KEY}, ${MODEL_NAME}) are substituted at container startup from the secret environment variables.
To add additional providers or change settings, edit this file following the OpenCode configuration schema.
The template includes both vllm and ogx providers. By default only vllm is enabled. To use OGX (formerly Llama Stack), change enabled_providers to ["ogx"] and set BASE_URL to your OGX endpoint (port 8321).
MCP servers can be injected at deploy time by creating a ConfigMap named opencode-web-mcp:
# Create a JSON file with your MCP server definitions
# Format follows OpenCode's mcp config schema: https://opencode.ai/docs/mcp-servers/
cat > mcp-servers.json <<EOF
{
"my-local-server": {
"type": "local",
"command": ["npx", "-y", "@my-org/my-mcp-server"],
"enabled": true
},
"my-remote-server": {
"type": "remote",
"url": "https://mcp.example.com/sse",
"enabled": true
}
}
EOF
# Create the ConfigMap
oc -n opencode create configmap opencode-web-mcp --from-file=mcp-servers.jsonThe entrypoint script merges MCP server definitions into the OpenCode config's mcp field at startup. If no opencode-web-mcp ConfigMap exists, OpenCode starts without MCP servers (the volume mount is optional).
Note: Command-based MCP servers (type
local) require the executable to exist in the container image. The base image includesgit,bash,python3, andjq. For servers requiringnpxor other runtimes, you'll need to extend the image.
OpenCode session history persists across pod restarts. The entrypoint automatically redirects OpenCode's data directory to persistent storage on the PVC.
| Default Path | Redirected To | Purpose |
|---|---|---|
~/.config/opencode/ |
/opt/app-root/workspace/.opencode/config/opencode/ |
Configuration, settings |
~/.local/share/opencode/ |
/opt/app-root/workspace/.opencode/data/opencode/ |
Session history, database |
~/.local/state/opencode/ |
/opt/app-root/workspace/.opencode/state/opencode/ |
Locks, runtime state |
The entrypoint creates symlinks from default XDG locations to PVC-backed paths. For config and data, this works automatically. For state, the deployment sets XDG_STATE_HOME explicitly (see note below).
Note: The container image creates
~/.local/state/with 755 permissions, which prevents symlink creation under OpenShift's random UID (the root group cannot write to 755 directories). The deployment manifests setXDG_STATE_HOMEas a workaround. If this is fixed upstream in the container image (by using 775 permissions), the environment variable can be removed.
# List previous sessions
oc exec deployment/opencode-cli -- opencode session list
# Resume the most recent session
oc exec -it deployment/opencode-cli -- opencode --continue
# Resume a specific session
oc exec -it deployment/opencode-cli -- opencode --session <session-id>Override the default location by setting the OPENCODE_DATA_DIR environment variable in the deployment:
env:
- name: OPENCODE_DATA_DIR
value: /opt/app-root/workspace/my-custom-dirThe entrypoint creates config/opencode/ and data/opencode/ subdirectories within this path.
Skills extend OpenCode with custom instructions. No skills are included by default — you create and inject your own.
Skills are auto-discovered from ~/.config/opencode/skills/. The skills ConfigMap is mounted at /etc/opencode-skills/ and the entrypoint symlinks it into the config directory. Each skill must be in a subdirectory containing a SKILL.md file with YAML frontmatter.
1. Create a SKILL.md file:
---
name: code-review
description: Analyze code for correctness, security, and performance issues
---
# Code Review
When reviewing code, analyze for:
1. **Correctness** - Logic errors, edge cases, off-by-one errors
2. **Security** - Input validation, injection risks, hardcoded secrets
3. **Performance** - Unnecessary loops, N+1 queries, missing indexes2. Create a ConfigMap from your skill files:
oc create configmap opencode-web-skills \
--from-file=code-review-skill=./skills/code-review/SKILL.md3. Add an items mapping to the skills volume in your deployment manifest:
The items mapping creates the subdirectory structure OpenCode expects:
volumes:
- name: skills
configMap:
name: opencode-web-skills
optional: true
items:
- key: code-review-skill
path: code-review/SKILL.md4. Restart the deployment:
oc rollout restart deployment/opencode-weboc login --server=https://api.<cluster-domain>:6443Ensure all values in manifests/kustomization.yaml are correct for your environment (see Configuration above).
oc apply -k manifests/This creates the following resources in the opencode namespace:
| Resource | Name | Purpose |
|---|---|---|
| Namespace | opencode |
Isolated project for the deployment |
| ServiceAccount | opencode-web |
Identity for OAuth proxy integration |
| ConfigMap | opencode-web-config |
OpenCode configuration template |
| Secret | opencode-web-secret |
vLLM endpoint credentials |
| Secret | opencode-web-proxy-cookie |
OAuth proxy session cookie secret |
| Secret | opencode-web-tls |
Auto-generated serving certificate |
| PVC | opencode-web-pvc |
Persistent workspace storage (10Gi) |
| Deployment | opencode-web |
Pod with oauth-proxy + opencode-web containers |
| Service | opencode-web |
Internal cluster networking |
| Route | opencode-web |
External HTTPS endpoint |
# Check pod status
oc -n opencode get pods
# Check logs for the opencode container
oc -n opencode logs deployment/opencode-web -c opencode-web
# Check logs for the oauth proxy
oc -n opencode logs deployment/opencode-web -c oauth-proxy
# Get the external URL
oc -n opencode get route opencode-web -o jsonpath='https://{.spec.host}{"\n"}'Open the route URL in your browser. You will be prompted to log in with your OpenShift credentials. After authentication, the OpenCode web UI will load.
Note: The OAuth proxy enforces that the authenticated user has
getpermission onservicesin theopencodenamespace. Grant access with:oc -n opencode adm policy add-role-to-user view <username>
The deployment is configured for the restricted-v2 Security Context Constraint:
runAsNonRoot: true— containers never run as rootallowPrivilegeEscalation: false— no privilege escalationseccompProfile: RuntimeDefault— default seccomp profilecapabilities: drop: [ALL]— no Linux capabilities
No special SCC grants are required.
The kustomization.yaml file contains plaintext secret values for convenience. For production deployments, replace the inline secrets with a proper secrets management solution:
- Sealed Secrets for GitOps workflows
- External Secrets Operator for integration with Vault, AWS Secrets Manager, etc.
- OpenShift Secrets Store CSI Driver for direct volume-mounted secrets
TLS is handled automatically:
- The Service annotation
service.beta.openshift.io/serving-cert-secret-nametriggers OpenShift to generate a serving certificate into theopencode-web-tlssecret. - The Route uses
reencrypttermination, meaning traffic is encrypted end-to-end from the user to the OAuth Proxy.
The OAuth Proxy enforces a Subject Access Review (SAR) check. Only users who can get services in the opencode namespace are allowed through. This ties access control to standard OpenShift RBAC.
oc -n opencode describe pod -l app=opencodeCommon causes:
- PVC not bound -- check that the configured storage class exists:
oc get storageclass - Image pull error -- verify the image reference and ensure the cluster can pull from
quay.io/opendatahub/odh-opencode-rhel9
The authenticated user lacks the required RBAC permission. Grant it:
oc -n opencode adm policy add-role-to-user view <username>Check that the vLLM service is running and reachable from within the cluster:
# Verify the vLLM service exists and has endpoints
oc get svc -A | grep vllm
# Test connectivity from the opencode pod to the vLLM service
oc -n opencode exec deployment/opencode-web -c opencode-web -- \
curl -s -o /dev/null -w '%{http_code}' "$BASE_URL/models"If vLLM is in a different namespace, ensure that no NetworkPolicy is blocking cross-namespace traffic. The BASE_URL must use the full cluster-internal DNS: http://<service>.<namespace>.svc.cluster.local/v1.
oc -n opencode exec deployment/opencode-web -c opencode-web -- \
printenv OPENCODE_CONFIG_CONTENT | jq .Remove all deployed resources:
oc delete -k manifests/This deletes the namespace and all resources within it.
Apache License 2.0 -- see LICENSE for details.
