Theia IDE is a modern, AI-native IDE which can be run on Desktop as also in a Docker container as a Web app. With the Docker image you can deploy Theia IDE in a Kubernetes cluster. The deployment is straightforward — until you try to use extensions that open embedded browser windows. This post explains the problem and how to solve it using wildcard certificates with DNS-01 challenges.
The Problem: Webview Subdomains
Theia uses separate origins for webviews to provide sandbox isolation. When an extension (like the Open-BPMN modeler or other diagram editors) opens a webview, Theia generates dynamic subdomain URLs:
https://<uuid>.webview.ide.yourdomain.com
Locally this works fine, but in Kubernetes you’ll see errors like:
<uuid>.webview.ide.yourdomain.com's server IP address could not be found
The browser can’t resolve these dynamic subdomains, and even if DNS is configured with a wildcard entry, the TLS certificate doesn’t cover them.
The Solution: Let’s Encrypt Wildcard Certificates with DNS-01
Standard HTTP-01 challenges can’t issue wildcard certificates. You need DNS-01 challenges, where Let’s Encrypt verifies domain ownership by checking a TXT record. The principle of DNS-01 works as followed:
- The Cert-manager requests a certificate for
*.webview.ide.yourdomain.com - Let’s Encrypt responds: “Create a TXT record
_acme-challenge.webview.ide.yourdomain.comwith valuexyz123“ - A webhook creates the TXT record via your DNS provider’s API
- Let’s Encrypt verifies and issues the certificate
- The webhook cleans up the TXT record
This requires API access to your DNS provider. Popular options with cert-manager support include Cloudflare, AWS Route53, Google Cloud DNS, and Hetzner.
Implementation with Hetzner DNS
The following example uses Hetzner’s official cert-manager webhook, but the approach is similar for other providers.
1. Install the Webhook
Hetzner provides a helm chart to install a webhook to call the Hetzner API
git clone https://github.com/hetzner/cert-manager-webhook-hetzner.git
cd cert-manager-webhook-hetzner
helm install cert-manager-webhook-hetzner ./chart --namespace cert-manager
2. Create API Token Secret
Next you need a API Token to get access to Hetzner API. With this token you can create a kubernetes secret:
kubectl create secret generic hetzner-dns-api-token \
--namespace cert-manager \
--from-literal=api-token="<YOUR-HETZNER-CLOUD-API-TOKEN>"
3. Configure ClusterIssuer
Finally you install a new ClusterIssuer to support wildcard certificates. This is an example using the hetzner cert-manager webhook
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod-dns01
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: your@email.com
privateKeySecretRef:
name: letsencrypt-prod-dns01-key
solvers:
- dns01:
webhook:
groupName: acme.hetzner.com
solverName: hetzner
config:
tokenSecretKeyRef:
name: hetzner-dns-api-token
key: api-token
4. DNS Configuration
In your DNS provider you can now register these records to your existing DNS zone:
| Type | Name | Value |
|---|---|---|
| A/CNAME | ide | Your Ingress IP/hostname |
| A/CNAME | *.webview.ide | Your Ingress IP/hostname |
5. Theia Deployment
So finally you can create the Theia IDE deployment using a ingress with wildcard TLS. See the following example:
apiVersion: apps/v1
kind: Deployment
metadata:
name: theia
spec:
template:
spec:
securityContext:
fsGroup: 101 # theia user group
containers:
- name: theia-ide
image: ghcr.io/eclipse-theia/theia-ide/theia-ide:latest
ports:
- containerPort: 3000
volumeMounts:
- mountPath: /home/theia/.theia-ide
name: theia-storage
subPath: theia-ide
- mountPath: /home/project
name: theia-storage
subPath: project
volumes:
- name: theia-storage
persistentVolumeClaim:
claimName: theia-storage
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: theia-ingress
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod-dns01"
spec:
ingressClassName: nginx
tls:
- hosts:
- ide.yourdomain.com
- "*.webview.ide.yourdomain.com"
secretName: theia-wildcard-tls
rules:
- host: ide.yourdomain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: theia
port:
number: 3000
- host: "*.webview.ide.yourdomain.com"
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: theia
port:
number: 3000
Verification
After deploying, verify the certificate:
$ kubectl get certificate -n my-namespace
NAME READY SECRET AGE
tls-theia-wildcard True tls-theia-wildcard 8h
Once READY shows True, your Theia IDE is fully operational with webview support.
Note: issuing the wildcard certificate may take some minutes!
You can test now your Theia IDE via web browser: https://ide.yourdomain.com
Securing Theia with OAuth2 Authentication
On additional option is to protect your Theia IDE with authentication. While Basic Auth seems like an easy choice, it doesn’t work well with Theia’s dynamic webview subdomains. This is because each new subdomain would require a separate login. A better solution is OAuth2.
Why OAuth2 Instead of Basic Auth?
Basic Authentication is scoped per domain. When Theia opens a webview at <uuid>.webview.ide.yourdomain.com, the browser treats it as a different domain than ide.yourdomain.com. This means users would need to re-authenticate for every extension that opens a webview.
OAuth2 with cookie-based sessions solves this problem. A session cookie set for ide.yourdomain.com is automatically shared with all subdomains like *.webview.ide.yourdomain.com, providing seamless authentication across the entire IDE.
OAuth2 Proxy with Keycloak
The following example uses an OAuth2 Proxy with Keycloak, but this setup works similarly with other identity providers (Google, GitHub, Azure AD, etc.).
1. Create Keycloak Client
In your Keycloak admin console:
- Create a new client with Client ID
theia-ide - Enable Client authentication
- Set Valid redirect URIs to
https://ide.yourdomain.com/oauth2/callback - Set Web origins to
https://ide.yourdomain.ai - Copy the client secret from the Credentials tab
2. Create Secrets
Next create a secret from your client secret:
kubectl create secret generic oauth2-proxy-secret \
--namespace your-namespace \
--from-literal=client-id="theia-ide" \
--from-literal=client-secret="<YOUR-KEYCLOAK-CLIENT-SECRET>" \
--from-literal=cookie-secret="$(openssl rand -hex 16)"
3. Deploy Redis for Session Storage
Keycloak tokens often exceed the 4KB cookie size limit. A solution is a Redis in memory store. Redis stores the session data server-side, keeping cookies small.
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:alpine
ports:
- containerPort: 6379
---
apiVersion: v1
kind: Service
metadata:
name: redis
spec:
selector:
app: redis
ports:
- port: 6379
Deploy OAuth2 Proxy
Now create a OAuth2 Proxy with an Ingres pointing
apiVersion: apps/v1
kind: Deployment
metadata:
name: oauth2-proxy
spec:
replicas: 1
selector:
matchLabels:
app: oauth2-proxy
template:
metadata:
labels:
app: oauth2-proxy
spec:
containers:
- name: oauth2-proxy
image: quay.io/oauth2-proxy/oauth2-proxy:latest
ports:
- containerPort: 4180
env:
- name: OAUTH2_PROXY_CLIENT_ID
valueFrom:
secretKeyRef:
name: oauth2-proxy-secret
key: client-id
- name: OAUTH2_PROXY_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: oauth2-proxy-secret
key: client-secret
- name: OAUTH2_PROXY_COOKIE_SECRET
valueFrom:
secretKeyRef:
name: oauth2-proxy-secret
key: cookie-secret
args:
- --provider=keycloak-oidc
- --oidc-issuer-url=https://your-keycloak.com/realms/your-realm
- --http-address=0.0.0.0:4180
- --upstream=static://200
- --email-domain=*
- --cookie-domain=ide.yourdomain.com
- --whitelist-domain=.ide.yourdomain.com
- --cookie-secure=true
- --cookie-samesite=lax
- --reverse-proxy=true
- --skip-provider-button=true
- --session-store-type=redis
- --redis-connection-url=redis://redis:6379
---
apiVersion: v1
kind: Service
metadata:
name: oauth2-proxy
spec:
selector:
app: oauth2-proxy
ports:
- port: 4180
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: oauth2-proxy-ingress
spec:
ingressClassName: nginx
tls:
- hosts:
- ide.yourdomain.com
secretName: theia-wildcard-tls
rules:
- host: ide.yourdomain.com
http:
paths:
- path: /oauth2
pathType: Prefix
backend:
service:
name: oauth2-proxy
port:
number: 4180
6. Update Theia Ingress
And finally you can add the new auth annotations to your existing Theia ingress:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: theia-ingress
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod-dns01"
nginx.ingress.kubernetes.io/auth-url: "https://ide.yourdomain.com/oauth2/auth"
nginx.ingress.kubernetes.io/auth-signin: "https://ide.yourdomain.com/oauth2/start?rd=$escaped_request_uri"
spec:
# ... rest of your ingress config
This setup will lead to the following Authentication Flow:
- User accesses
ide.yourdomain.com - Nginx Ingress checks authentication via OAuth2 Proxy
- If no valid session exists, user is redirected to Keycloak
- After successful login, session is stored in Redis
- A session cookie enables access to IDE and all webviews
Conclusion
Running Theia IDE in Kubernetes with full extension support requires wildcard certificates for the dynamic webview subdomains. Using DNS-01 challenges with your DNS provider’s API makes this fully automated. Adding OAuth2 authentication with session cookies ensures users only need to log in once, with the session valid across all webview subdomains.
While this example uses Hetzner DNS and Keycloak, the same patterns work with any DNS provider that has a cert-manager webhook and any OAuth2/OIDC identity provider.
