# How to Deploy OpenWork Server to Kubernetes Using Helm Charts

> Deploy OpenWork server to Kubernetes easily using Helm charts. Install the openwork-ee chart from the OCI registry and configure your values file for a seamless deployment.

- Repository: [Different AI/openwork](https://github.com/different-ai/openwork)
- Tags: how-to-guide
- Published: 2026-08-15

---

**Deploy OpenWork EE to Kubernetes by installing the `openwork-ee` Helm chart from the OCI registry at `ghcr.io/different-ai/charts/openwork-ee`, configuring your cloud-specific values file, and running `helm upgrade --install`.**

The `different-ai/openwork` repository provides a production-ready Helm chart that packages all server-side components—**`den-api`** (core control-plane), **`den-web`** (web UI), **optional inference proxy**, database **migration jobs**, and **observability**—into a single deployable unit. This guide walks through the complete deployment process using the chart templates in `packaging/helm/openwork-ee/templates` and the configurable values hierarchy defined in [`packaging/helm/openwork-ee/values.yaml`](https://github.com/different-ai/openwork/blob/main/packaging/helm/openwork-ee/values.yaml).

---

## What the OpenWork Helm Chart Deploys

The **openwork-ee** chart renders the following Kubernetes resources:

| Component | Port | Purpose |
|-----------|------|---------|
| `den-api` | 8788 | Core API for authentication, MCP, and organization management |
| `den-web` | 3005 | Web UI that the desktop application communicates with |
| `inference` (optional) | 8791 | AI inference proxy for LLM features |
| `migration Job` | — | Pre-install hook that bootstraps MySQL schema and runs migrations |
| ConfigMap / Secret | — | Runtime configuration, database credentials, custom CA certificates |
| Ingress / Service | — | External exposure via LoadBalancer or Ingress controller |
| Observability | — | Optional OpenTelemetry or Sentry export configuration |

All templates live under `packaging/helm/openwork-ee/templates`. The chart follows this rendering sequence:

1. **Secret generation** — creates or references an existing secret with keys defined in `secret.keys`
2. **Custom CA mounting** — when `customCa.enabled=true`, mounts certificates and sets `NODE_EXTRA_CA_CERTS` for strict TLS (e.g., with MySQL)
3. **Migration hook** — runs as a pre-install/upgrade Job to prepare the database before services start
4. **Observability injection** — configures `den-api` and `den-web` to export traces and logs based on `observability.backend`

---

## Preparing Your Helm Values File

Start from a cloud-provider example in `packaging/helm/openwork-ee/examples/` rather than building from scratch.

### AWS LoadBalancer Example

The file [`packaging/helm/openwork-ee/examples/values.aws-load-balancer.yaml`](https://github.com/different-ai/openwork/blob/main/packaging/helm/openwork-ee/examples/values.aws-load-balancer.yaml) demonstrates AWS-specific service annotations and SSL certificate integration:

```yaml

# values.aws.yaml — adapted from the AWS example

image:
  tag: "REPLACE_OPENWORK_VERSION"

config:
  public:
    webOrigin: "https://openwork.example.com"
    apiOrigin: "https://api.openwork.example.com"
    corsOrigins: "https://openwork.example.com,https://api.openwork.example.com"
    betterAuthTrustedOrigins: "https://openwork.example.com"
    webAppHosts: "openwork.example.com"
    bootstrapAdminEmails: "admin@example.com"

secret:
  values:
    databaseUrl: "mysql://openwork:CHANGE_ME@my-rds-endpoint:3306/openwork_den?sslaccept=accept"
    betterAuthSecret: "CHANGE_ME_32_CHARS_MINIMUM_BETTER_AUTH"
    denDbEncryptionKey: "CHANGE_ME_32_CHARS_MINIMUM_DB_ENCRYPTION"
    emailFrom: "OpenWork <no-reply@example.com>"
    smtpHost: "smtp.example.com"
    smtpPort: "587"
    smtpUser: "openwork@example.com"
    smtpPass: "REPLACE_SMTP_PASSWORD"
    smtpSecure: "false"

denWeb:
  service:
    type: LoadBalancer
    port: 443
    annotations:
      service.beta.kubernetes.io/aws-load-balancer-ssl-cert: arn:aws:acm:...
      service.beta.kubernetes.io/aws-load-balancer-ssl-ports: "443"

denApi:
  service:
    type: LoadBalancer
    port: 443
    annotations:
      service.beta.kubernetes.io/aws-load-balancer-ssl-cert: arn:aws:acm:...
      service.beta.kubernetes.io/aws-load-balancer-ssl-ports: "443"

```

Critical placeholders to replace:
- `REPLACE_OPENWORK_VERSION` — chart version from the OCI registry
- `CHANGE_ME_*` — all secret values (32+ characters recommended for encryption keys)
- `my-rds-endpoint` — your managed MySQL endpoint
- ACM certificate ARN — for TLS termination at the LoadBalancer

### Azure and GCP Equivalents

- **Azure AKS**: Use [`values.azure-ingress.yaml`](https://github.com/different-ai/openwork/blob/main/values.azure-ingress.yaml) with `ingress-nginx` or Application Gateway annotations
- **GCP GKE**: Use [`values.gcp-ingress.yaml`](https://github.com/different-ai/openwork/blob/main/values.gcp-ingress.yaml) with Google-managed SSL certificates

Reference the dedicated guides in [`docs/azure-aks-helm.md`](https://github.com/different-ai/openwork/blob/main/docs/azure-aks-helm.md) and [`docs/gcp-gke-helm.md`](https://github.com/different-ai/openwork/blob/main/docs/gcp-gke-helm.md) for provider-specific networking setup.

---

## Installing the OpenWork Helm Chart

### Method 1: OCI Registry (Recommended)

The chart is published as an OCI artifact at `oci://ghcr.io/different-ai/charts/openwork-ee`:

```bash
helm upgrade --install openwork-ee oci://ghcr.io/different-ai/charts/openwork-ee \
  --version REPLACE_OPENWORK_VERSION \
  --namespace openwork-ee \
  --create-namespace \
  -f values.aws.yaml

```

### Method 2: Local Chart Checkout

For air-gapped environments or custom template modifications:

```bash
helm upgrade --install openwork-ee ./packaging/helm/openwork-ee \
  --namespace openwork-ee \
  --create-namespace \
  -f values.aws.yaml

```

### Optional: Pre-Flight Rendering

Validate manifests before installation:

```bash
helm template openwork-ee oci://ghcr.io/different-ai/charts/openwork-ee \
  --version REPLACE_OPENWORK_VERSION \
  -f values.aws.yaml > rendered.yaml

# Verify secrets are properly templated and database URL is correct

grep -E 'DATABASE_URL|BETTER_AUTH_SECRET|DEN_DB_ENCRYPTION_KEY' rendered.yaml

```

---

## Authenticating to Private Container Registry

If your deployment pulls images from private GHCR repositories, create a pull secret:

```bash
helm registry login ghcr.io
kubectl create secret docker-registry ghcr-pull-secret \
  --namespace openwork-ee \
  --docker-server=ghcr.io \
  --docker-username=<github-user> \
  --docker-password=<github-token>

```

Reference this secret in your values file under `image.imagePullSecrets` as documented in [`packaging/helm/openwork-ee/values.yaml`](https://github.com/different-ai/openwork/blob/main/packaging/helm/openwork-ee/values.yaml).

---

## Verifying the OpenWork Deployment

### Check Pod Status

```bash
kubectl get pods -n openwork-ee

```

Expect: `den-api-*`, `den-web-*`, and completed `migration-*` pods in `Ready` state.

### Verify Service Endpoints

```bash
kubectl get svc -n openwork-ee

```

Note the `EXTERNAL-IP` values for LoadBalancer services, or configure DNS to point to your Ingress hosts.

### Health Check Endpoints

```bash

# API readiness probe

curl -fsS https://api.openwork.example.com/ready

# Web UI health via API proxy

curl -fsS https://openwork.example.com/api/ready

```

Both endpoints return HTTP 200 when services are fully initialized.

---

## Post-Deployment: Bootstrapping the First Organization

For **single-organization mode**, add tenancy configuration before the first user sign-in:

```yaml
config:
  tenancy:
    mode: "single_org"
    singleOrgName: "Acme"
    singleOrgSlug: "acme"
    ownerEmails: "admin@acme.com"
    requireEmailVerification: "false"
  public:
    bootstrapAdminEmails: "admin@acme.com"

```

Apply with `helm upgrade` and navigate to `https://openwork.example.com`. The first sign-up with `admin@acme.com` automatically creates the singleton organization with full admin privileges.

---

## Enabling Observability

### OpenTelemetry Configuration

```yaml
observability:
  backend: otel
  otel:
    endpoint: "http://otel-collector.observability.svc.cluster.local:4318"
    headers:
      existingSecret: openwork-otel-headers
      key: OTEL_EXPORTER_OTLP_HEADERS

```

Create the referenced secret:

```bash
kubectl create secret generic openwork-otel-headers \
  --namespace openwork-ee \
  --from-literal=OTEL_EXPORTER_OTLP_HEADERS='Authorization=Bearer <token>'

```

`den-api` and `den-web` will now emit distributed traces to your collector. Alternative: set `observability.backend: sentry` and provide `sentry.dsn` for error tracking.

---

## Advanced: Custom CA for Private TLS

When connecting to MySQL or internal services with private PKI, enable the custom CA feature:

```yaml
customCa:
  enabled: true
  crt: |
    -----BEGIN CERTIFICATE-----
    MIIFXTCCA0WgAwIBAgIQVf...
    -----END CERTIFICATE-----

```

The chart mounts this certificate into all Node.js processes and sets `NODE_EXTRA_CA_CERTS`, ensuring TLS verification succeeds without disabling security. See `packages/docs/start-here/certificate-trust-and-proxies.mdx` for detailed CA chain handling.

---

## Summary

- **Get the chart** from `oci://ghcr.io/different-ai/charts/openwork-ee` or clone `different-ai/openwork` for local use
- **Start from examples** in `packaging/helm/openwork-ee/examples/` matched to your cloud provider
- **Configure secrets** in `secret.values` — never commit the actual values file with real credentials
- **Deploy with** `helm upgrade --install` in a dedicated namespace
- **Verify health** via `/ready` endpoints before routing production traffic
- **Bootstrap tenancy** via `config.tenancy` for immediate organization setup

---

## Frequently Asked Questions

### How do I upgrade OpenWork without downtime?

Run `helm upgrade --install` with the new version tag. The chart uses **RollingUpdate** strategies for `den-api` and `den-web` Deployments, and migration Jobs run as pre-upgrade hooks. Ensure your [`values.yaml`](https://github.com/different-ai/openwork/blob/main/values.yaml) remains compatible—review the chart's [`README.md`](https://github.com/different-ai/openwork/blob/main/README.md) for breaking changes between versions.

### Can I use an external secret manager instead of Helm values?

Yes. Set `secret.existingSecretName` to reference a pre-created Kubernetes secret, then omit `secret.values` from your values file. The chart expects keys matching those defined in `secret.keys` within [`packaging/helm/openwork-ee/values.yaml`](https://github.com/different-ai/openwork/blob/main/packaging/helm/openwork-ee/values.yaml).

### What database does OpenWork require?

OpenWork EE requires **MySQL 8.0+**. The `migration Job` automatically creates tables and runs schema migrations on startup. Configure connectivity via `secret.values.databaseUrl` with proper SSL parameters for production—use `customCa` if your database uses private certificates.

### How do I expose OpenWork without a cloud LoadBalancer?

Use [`values.yaml`](https://github.com/different-ai/openwork/blob/main/values.yaml) configurations that set `service.type: ClusterIP` and define `ingress` rules instead. The chart supports any Kubernetes Ingress controller—configure `annotations` for cert-manager, external-dns, or your preferred tooling. Examples for nginx-ingress and Traefik are referenced in the cloud-specific documentation files.