TheoWorks

OpenShift

Run TheoWorks on a Red Hat OpenShift cluster instead of a single machine. One Helm chart, two editions — Free/Pro (the public chart) and Enterprise (the offline bundle or the registry credential, both from the download page).

Note

On any other Kubernetes cluster? Use the Kubernetes guide. Red Hat OpenShift cluster? Use the OpenShift guide. Any other Kubernetes cluster? Use the Kubernetes guide.

Choose your path

| Your situation | What you'll use | |---|---| | Free or Pro | The public chart | | Enterprise, connected cluster | The registry credential | | Enterprise, air-gapped cluster | The offline bundle |

Prerequisites

Common to every path:

  • An OpenShift 4.11+ cluster. No cluster-admin, no custom Security Context Constraint, no root — the pod runs under the cluster's default restricted-v2 policy.
  • Reachability to your GitLab, plus an OAuth application registered on it (client ID, client secret, and redirect URI). Sign-in is always through your own GitLab.
  • A ReadWriteOnce (RWO) storage class. Shared storage is not required.
  • The source network range your ingress traffic arrives from (your ingress controller's pod-network CIDR, or the cluster pod CIDR) — required, see Common values.
  • Free or Pro only: outbound HTTPS to lm.theoworks.io. The public chart connects to the TheoWorks License Server by default, the same way the single-machine launcher does — your plan activates the same way. Enterprise clusters use the offline license instead and need no such access.

Additionally, for the Enterprise edition only: sign in at theoworks.io/download to get the chart and image (see below), and your offline Enterprise license file.

Get the chart and image

Free or Pro

The public chart pulls the public TheoWorks image anonymously — no registry credential, no image pull secret, no sign-in. Its OCI reference (public ECR) is on the download page — the site has the current version; this page's commands install without pinning a version so they always install the current release.

Enterprise, air-gapped cluster

At theoworks.io/download, the Air-gapped cluster card gives you Get the offline bundle: the chart archive, the image as a portable archive, checksums, and the signing key:

text
theoworks-enterprise-X.Y.Z.tgz
theoworks-app-enterprise-X.Y.Z.oci.tar
checksums.txt
cosign.pub

Download all four before the link window closes — the window only has to cover the start of each download; a download already in progress finishes.

Enterprise, connected cluster

At theoworks.io/download, the Connected cluster card gives you Get a registry credential: a registry host, a username, and a temporary password, shown once and copyable without revealing it.

Write your values.yaml

The complete file below is byte-identical to the OpenShift template on the download page — if you started there, this is the same file you already copied.

yaml
route:
  host: theoworks.apps.example.com           # your external hostname
  forwardedAllowIps: "10.128.0.0/14"         # your ingress/pod-network CIDR -- required

config:
  gitlabInstance: "https://gitlab.example.com"                              # your GitLab URL
  oauthRedirectUri: "https://theoworks.apps.example.com/oauth/callback"  # must match the GitLab OAuth app
  maintainerEmail: "you@example.com"        # optional -- their first sign-in becomes the maintainer, skipping first-run setup

secrets:
  oauthClientId: "<your-gitlab-oauth-client-id>"
  oauthClientSecret: "<your-gitlab-oauth-client-secret>"
  sessionKey: "<32 random bytes, base64 -- e.g. `openssl rand -base64 32`>"

Common values

These settings apply to every path.

  • route.tlsStrategy: internal (default) uses a cluster-issued serving certificate — the on-premises-friendly choice, no public DNS, no outbound call to a public certificate authority. own-cert uses a TLS Secret you supply (route.tlsSecretName). acme uses the cluster's certificate manager for a publicly trusted certificate (route.certManagerIssuer) — only for a public DNS name.
  • route.forwardedAllowIps is required. Behind an edge Route, the app sees the ingress router pods' source addresses, not your users' browsers. It only honors forwarded headers from source ranges you name here — your ingress controller's pod-network CIDR, or the cluster pod CIDR (your platform team can supply it). Leaving it empty fails the install with a clear error rather than silently trusting nothing. A wildcard is refused as unsafe.
  • GitLab connection. Register the OAuth application on your GitLab first — the redirect URI you paste into GitLab must match config.oauthRedirectUri exactly. For GitOps workflows, keep these in a secret you manage yourself and set secrets.existingSecret to its name instead.
  • config.maintainerEmail is optional. The first person who signs in from that address becomes the maintainer, skipping the first-run setup screen.

GitLab connection details are never optional: config.gitlabInstance and config.oauthRedirectUri must match the OAuth application exactly, and secrets.oauthClientId/oauthClientSecret/sessionKey must all be set (or provided via secrets.existingSecret) — the app refuses to boot without them.

Storage and resources apply to every path too:

yaml
persistence:
  index:
    size: 2Gi                  # rebuildable search index
    storageClassName: ""       # "" = cluster default
  state:
    size: 256Mi                # small usage state
    storageClassName: ""

resources:
  limits:   { cpu: "2", memory: 2Gi }
  requests: { cpu: "500m", memory: 1Gi }

The pod sizes its internal work pools to the CPU limit you set here, so it behaves the same on a small node and a large one.

Note

The chart intentionally does not set a runAsUser or fsGroup here — the cluster's default restricted policy assigns those. The pod already runs non-root, with a read-only root filesystem and all Linux capabilities dropped.

Enterprise additions

On top of the common values above, Enterprise sets the license -- and, only for an air-gapped cluster, the image location:

yaml
license:
  enabled: true                       # the chart default
  existingSecret: "theoworks-license" # recommended: a secret you manage
  existingSecretKey: "license.json"
  # or, for a first install, inline the envelope instead:
  # content: '{"...": "..."}'

The license is mounted as a file, so rotating it is a secret update — no pod restart.

Connected cluster: keep the chart's image settings. The Enterprise chart already names the released image and pins it by digest — never a tag. Add only the pull secret you create during Install:

yaml
image:
  pullSecrets: ["theoworks-pull"]     # your dockerconfigjson secret(s)

Don't set image.registry, image.repository, image.digest or image.tag for a connected cluster.

Air-gapped cluster: nothing to add here for the image. Install sets the image's registry and digest from your registry and the bundle. If your registry needs credentials to pull, add your own secret under image.pullSecrets.

Pin the image by digest so every rollout runs an exact, verified image — this is the ONE enterprise pin style; nowhere on this page does an enterprise command set image.tag.

If your GitLab or license server presents a certificate from an internal or corporate authority, add a caBundle block (see the chart's bundled values.yaml for its keys) — left at its defaults, nothing extra is mounted.

Install

Each path's install command appears exactly once, here, after the values above.

  1. Create the project.

    bash
    oc new-project theoworks
  2. Set these once:

    bash
    export THEOWORKS_VERSION=X.Y.Z                        # Enterprise -- the version you're installing (the connected-cluster card, or the bundle filenames you downloaded)
    export REGISTRY=your-registry.example.internal        # Enterprise, air-gapped cluster only -- where you load and pull the image from

Free or Pro

bash
helm install theoworks oci://public.ecr.aws/d9m2o0n8/chart/theoworks \
  -f values.yaml

This command doesn't pin --version — the site has no reliable source for "the current free/pro release tag" scoped to a static page, and a placeholder version would fail on paste. Omitting it installs the chart's latest published version, which is the current release.

Enterprise, air-gapped cluster

Verify and load the image into your own registry first, then install from the local chart file:

bash
sha256sum -c checksums.txt

mkdir -p theoworks-app-enterprise-$THEOWORKS_VERSION-oci
tar -xf theoworks-app-enterprise-$THEOWORKS_VERSION.oci.tar -C theoworks-app-enterprise-$THEOWORKS_VERSION-oci
cosign load --dir theoworks-app-enterprise-$THEOWORKS_VERSION-oci $REGISTRY/theoworks-app-enterprise

DIGEST=$(jq -r '.manifests[] | select(.annotations.kind == "dev.cosignproject.cosign/imageIndex") | .digest' theoworks-app-enterprise-$THEOWORKS_VERSION-oci/index.json)
cosign verify --key cosign.pub --insecure-ignore-tlog \
  $REGISTRY/theoworks-app-enterprise@$DIGEST

cosign load takes the image reference positionally (there is no --registry flag); the digest to verify -- and to pin image.digest below -- comes from the extracted OCI layout's index.json, never from anything cosign load prints.

Then install:

bash
helm install theoworks ./theoworks-enterprise-$THEOWORKS_VERSION.tgz \
  --set image.registry=$REGISTRY \
  --set image.digest=$DIGEST \
  -f values.yaml

Enterprise, connected cluster

Sign in to the registry first (the credential's password is meant for helm registry login's interactive prompt — never a command-line flag, never a file):

bash
helm registry login 366258937776.dkr.ecr.eu-central-1.amazonaws.com --username AWS

Paste the password when prompted, then create the image pull secret (the kubelet needs its own — helm registry login above only covers the chart pull). The sequence below never puts the password on the command line, into shell history, or into a file:

bash
printf 'Paste the registry password, then press Enter: '
IFS= read -rs TW_PW; echo
TW_AUTH=$(printf 'AWS:%s' "$TW_PW" | base64 | tr -d '\n')
TW_CFG=$(printf '{"auths":{"%s":{"username":"AWS","password":"%s","auth":"%s"}}}' \
  366258937776.dkr.ecr.eu-central-1.amazonaws.com "$TW_PW" "$TW_AUTH" | base64 | tr -d '\n')
printf '{"apiVersion":"v1","kind":"Secret","metadata":{"name":"theoworks-pull"},"type":"kubernetes.io/dockerconfigjson","data":{".dockerconfigjson":"%s"}}' "$TW_CFG" \
  | oc apply --server-side -f -
unset TW_PW TW_AUTH TW_CFG

Name it in your values with image.pullSecrets: ["theoworks-pull"] (already shown above), then install:

bash
helm install theoworks oci://366258937776.dkr.ecr.eu-central-1.amazonaws.com/charts/theoworks-enterprise \
  --version "$THEOWORKS_VERSION" \
  -f values.yaml

End the registry session:

bash
helm registry logout 366258937776.dkr.ecr.eu-central-1.amazonaws.com

A registry credential expires (at most 12 hours). Recommended: mirror the image into your own registry once (see Day-2) rather than depending on it for every future pull, drain, or scale-out.

Verify

For every path, once installed:

bash
oc wait --for=condition=Ready pod -l app.kubernetes.io/instance=theoworks --timeout=10m

This does not depend on the chart's fullname, so it works the same whichever path you installed. Then confirm the instance is healthy and reachable: open your Route hostname, sign in through your GitLab, open a document, edit it, and save — the commit lands in your GitLab repository.

License and usage

  • Pro: sign in as the maintainer and open Settings ▸ Usage & License. Paste the code from your purchase email into Redemption code, then select Activate Pro. The same page shows current usage against your plan's seats.

  • Enterprise: your license file is stored in a Kubernetes Secret: the one named in license.existingSecret (key license.json by default). To apply a renewed license, update that Secret in place. You don't need helm upgrade or a restart:

    bash
    oc create secret generic theoworks-license \
      --from-file=license.json=license.json \
      --dry-run=client -o yaml | oc apply -f -

    TheoWorks reads the license file every time it checks your license. So the new license takes effect once Kubernetes updates the mounted file, which can take a short while. To confirm, open Settings ▸ Usage & License: it shows Licensed · Enterprise while a valid license is in effect.

    If you put the license inline with license.content instead, replace it in your values file and run helm upgrade (see Day-2 operations).

Day-2 operations

Both the CA bundle and the license are mounted as files — rotate either by updating the underlying secret in place; the mounted file refreshes without a restart.

Free or Pro: the public chart already bakes the current release's image tag, so a plain upgrade picks up the latest release -- you don't need to set image.tag yourself (doing so overrides the chart's baked tag):

bash
helm upgrade theoworks oci://public.ecr.aws/d9m2o0n8/chart/theoworks \
  -f values.yaml

Enterprise, connected cluster: the chart bakes the released image's digest itself, so an upgrade never sets image.digest -- it's the same install command from Install, run again with the new version.

Already running from your own registry? Use Upgrade a mirrored install instead.

First, get a fresh registry credential. The one from your install has expired (registry credentials last at most 12 hours). At theoworks.io/download, in the Connected cluster card, select Get a registry credential (or Get a new credential). Keep that page open: you'll paste its password below. Finish these steps before the time shown on the card.

bash
helm registry login 366258937776.dkr.ecr.eu-central-1.amazonaws.com --username AWS

Refresh the image pull secret so your cluster can pull the new version's image, with the same password-safe sequence Install used:

bash
printf 'Paste the registry password, then press Enter: '
IFS= read -rs TW_PW; echo
TW_AUTH=$(printf 'AWS:%s' "$TW_PW" | base64 | tr -d '\n')
TW_CFG=$(printf '{"auths":{"%s":{"username":"AWS","password":"%s","auth":"%s"}}}' \
  366258937776.dkr.ecr.eu-central-1.amazonaws.com "$TW_PW" "$TW_AUTH" | base64 | tr -d '\n')
printf '{"apiVersion":"v1","kind":"Secret","metadata":{"name":"theoworks-pull"},"type":"kubernetes.io/dockerconfigjson","data":{".dockerconfigjson":"%s"}}' "$TW_CFG" \
  | oc apply --server-side -f -
unset TW_PW TW_AUTH TW_CFG
bash
export THEOWORKS_VERSION=X.Y.Z                        # the new release
helm upgrade theoworks oci://366258937776.dkr.ecr.eu-central-1.amazonaws.com/charts/theoworks-enterprise \
  --version "$THEOWORKS_VERSION" \
  -f values.yaml
bash
helm registry logout 366258937776.dkr.ecr.eu-central-1.amazonaws.com

The pull secret now holds today's credential, which stops working within 12 hours. Pods that are already running aren't affected. But if Kubernetes later moves the pod to a node that doesn't have the image yet, pulling the image fails. To stop depending on this credential, mirror the image into your own registry. After that, an upgrade only needs a fresh credential to fetch the new chart and copy the new image; your running deployment never does.

Enterprise, air-gapped cluster: download and verify the new bundle first -- re-run the air-gapped verify-and-load sequence against it (which reads the new $DIGEST from its index.json) -- then upgrade from the new chart file with the new registry and digest:

bash
export THEOWORKS_VERSION=X.Y.Z                        # the new release
helm upgrade theoworks ./theoworks-enterprise-$THEOWORKS_VERSION.tgz \
  --set image.registry=$REGISTRY \
  --set image.digest=$DIGEST \
  -f values.yaml

On shutdown the pod stops accepting new traffic first and finishes in-flight edits and saves within its shutdown grace period. Because this baseline runs a single pod, plan an upgrade for a quiet period — multi-pod, continuously-available upgrades are on the roadmap.

Mirror the image for a connected cluster

A registry credential is short-lived. If your cluster's pod may reschedule after the credential expires (a redeploy, a node drain), pull secrets created from it will fail the next image pull. Mirror the image into your own internal registry so the running deployment no longer depends on the credential.

First, get a fresh registry credential. The one from your install has expired (registry credentials last at most 12 hours). At theoworks.io/download, in the Connected cluster card, select Get a registry credential (or Get a new credential). Keep that page open: you'll paste its password below. Finish these steps before the time shown on the card.

Set these once:

bash
export INTERNAL_REGISTRY=your-internal-registry.example.internal
export THEOWORKS_VERSION=X.Y.Z                        # the version you run now: the number after "theoworks-enterprise-" in `helm list`

Sign in to copy the image:

bash
skopeo login 366258937776.dkr.ecr.eu-central-1.amazonaws.com --username AWS

If your own registry needs a sign-in to push, run skopeo login "$INTERNAL_REGISTRY" too.

Copy the image you run now into your registry. It is tagged with its version, and its digest stays exactly the same:

bash
IMAGE_DIGEST=$(oc get pod -l app.kubernetes.io/instance=theoworks -o jsonpath='{.items[0].spec.containers[0].image}' | sed 's/.*@//')
skopeo copy --all --preserve-digests \
  docker://366258937776.dkr.ecr.eu-central-1.amazonaws.com/theoworks-app-enterprise@$IMAGE_DIGEST \
  docker://$INTERNAL_REGISTRY/theoworks-app-enterprise:$THEOWORKS_VERSION
skopeo inspect --raw docker://$INTERNAL_REGISTRY/theoworks-app-enterprise@$IMAGE_DIGEST > /dev/null && echo "Your registry serves the image by digest."
skopeo logout 366258937776.dkr.ecr.eu-central-1.amazonaws.com

Copy to a version tag: some registries, OpenShift's own among them, don't keep an image pushed without a tag. TheoWorks still pulls it by digest, and the last check confirms your registry serves it that way. If the check prints nothing, stop: your registry isn't serving the image yet.

Let your cluster sign in to your registry. If your cluster can pull from your registry without a sign-in, skip this step. If your platform team already gave you a pull secret for it in this namespace, skip it too, and use that secret's name below. Otherwise create one (you'll be asked for your registry's username and password):

bash
TW_AUTHDIR=$(mktemp -d)
skopeo login --authfile "$TW_AUTHDIR/auth.json" "${INTERNAL_REGISTRY%%/*}"
oc create secret generic internal-pull \
  --type=kubernetes.io/dockerconfigjson \
  --from-file=.dockerconfigjson="$TW_AUTHDIR/auth.json" \
  --dry-run=client -o yaml | oc apply --server-side -f -
rm -rf "$TW_AUTHDIR"; unset TW_AUTHDIR

This path is safe for any password: skopeo writes the sign-in itself. The sign-in sits for a moment in a private temporary folder (mktemp -d, readable only by you), and the last line deletes it.

Point the chart at your registry. In your values.yaml, under image:, add registry: with your registry's address (the same value as $INTERNAL_REGISTRY), and add your registry's pull secret to image.pullSecrets next to theoworks-pull, for example pullSecrets: ["theoworks-pull", "internal-pull"]. Skip the pull secret if the previous step didn't apply. Don't change the digest; the chart keeps it.

Apply it. Sign in again to fetch the chart: Helm keeps its own sign-in. Paste the same password, or get a new credential first if the time shown on the card has passed. Use helm upgrade, not helm install, which fails on an existing release.

bash
helm registry login 366258937776.dkr.ecr.eu-central-1.amazonaws.com --username AWS
helm upgrade theoworks oci://366258937776.dkr.ecr.eu-central-1.amazonaws.com/charts/theoworks-enterprise \
  --version "$THEOWORKS_VERSION" \
  -f values.yaml
helm registry logout 366258937776.dkr.ecr.eu-central-1.amazonaws.com

Once the pod runs from your registry, theoworks-pull is no longer used, and it holds a credential that expires. At your next upgrade you can remove it from image.pullSecrets. From now on, upgrade with Upgrade a mirrored install (below), not the connected-cluster upgrade above.

Upgrade a mirrored install

Once your cluster runs from your own registry, copy each new version's image there first, then upgrade. Only these steps need a TheoWorks sign-in; your running deployment never does.

First, get a fresh registry credential. The one from your install has expired (registry credentials last at most 12 hours). At theoworks.io/download, in the Connected cluster card, select Get a registry credential (or Get a new credential). Keep that page open: you'll paste its password below. Finish these steps before the time shown on the card.

Set these once, and sign in to fetch the new chart:

bash
export THEOWORKS_VERSION=X.Y.Z                        # the new release
export INTERNAL_REGISTRY=your-internal-registry.example.internal
helm registry login 366258937776.dkr.ecr.eu-central-1.amazonaws.com --username AWS

Paste the password when prompted.

Find the new version's image, read from the new chart, never from the running pod:

bash
NEW_DIGEST=$(helm template theoworks oci://366258937776.dkr.ecr.eu-central-1.amazonaws.com/charts/theoworks-enterprise \
  --version "$THEOWORKS_VERSION" -f values.yaml \
  | grep -o 'theoworks-app-enterprise@sha256:[0-9a-f]*' | head -n 1 | cut -d@ -f2)
echo "New image digest: $NEW_DIGEST"

Helm may also print its own Pulled: and Digest: lines for the chart; ignore them. The line that matters is the one starting New image digest:. If it shows nothing after the colon, stop and check the version number.

Copy it into your registry:

bash
skopeo login 366258937776.dkr.ecr.eu-central-1.amazonaws.com --username AWS
skopeo copy --all --preserve-digests \
  docker://366258937776.dkr.ecr.eu-central-1.amazonaws.com/theoworks-app-enterprise@$NEW_DIGEST \
  docker://$INTERNAL_REGISTRY/theoworks-app-enterprise:$THEOWORKS_VERSION
skopeo inspect --raw docker://$INTERNAL_REGISTRY/theoworks-app-enterprise@$NEW_DIGEST > /dev/null && echo "Your registry serves the new image by digest."
skopeo logout 366258937776.dkr.ecr.eu-central-1.amazonaws.com

This uses the same password. If your own registry needs a sign-in to push, run skopeo login "$INTERNAL_REGISTRY" too. If the check prints nothing, stop before upgrading.

Upgrade:

bash
helm upgrade theoworks oci://366258937776.dkr.ecr.eu-central-1.amazonaws.com/charts/theoworks-enterprise \
  --version "$THEOWORKS_VERSION" \
  -f values.yaml
helm registry logout 366258937776.dkr.ecr.eu-central-1.amazonaws.com

Your values.yaml already points at your registry and its pull secret, so the new pod pulls the new image from there.

Reference

| Setting | What it does | Default | |---|---|---| | image.registry / image.repository | Where the image comes from | (enterprise defaults; Free/Pro overrides) | | image.tag | Override to pin an exact Free/Pro build | (chart default is the current release -- optional, not required) | | image.digest | Exact image for Enterprise (pin by digest) | (required for Enterprise) | | image.pullSecrets | Registry pull secret name(s); none needed for Free/Pro | [] | | license.enabled | Mount an offline license (true Enterprise; false Free/Pro) | true | | route.host | External hostname | (none) | | route.tlsStrategy | internal, own-cert, or acme | internal | | route.forwardedAllowIps | Ingress source CIDR (required) | (none — install fails if empty) | | config.gitlabInstance / oauthRedirectUri | Your GitLab URL and OAuth redirect | (none) | | config.maintainerEmail | Optional — first sign-in from this address becomes the maintainer | "" | | secrets.oauthClientId / oauthClientSecret / sessionKey | GitLab OAuth app credentials + session key | (none) | | license.existingSecret / content | Your offline license (Enterprise) | (none) | | caBundle.enabled + one of content/existingSecret/existingConfigMap | Corporate CA trust | false | | persistence.index.size / persistence.state.size | Volume sizes | 2Gi / 256Mi | | resources.limits | CPU / memory ceiling | cpu: 2, memory: 2Gi |

See the chart's bundled values.yaml for every setting and inline guidance.

Next steps