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).
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-v2policy. - 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:
theoworks-enterprise-X.Y.Z.tgz
theoworks-app-enterprise-X.Y.Z.oci.tar
checksums.txt
cosign.pubDownload 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.
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-certuses a TLS Secret you supply (route.tlsSecretName).acmeuses the cluster's certificate manager for a publicly trusted certificate (route.certManagerIssuer) — only for a public DNS name.route.forwardedAllowIpsis 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.oauthRedirectUriexactly. For GitOps workflows, keep these in a secret you manage yourself and setsecrets.existingSecretto its name instead. config.maintainerEmailis 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:
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.
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:
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:
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.
-
Create the project.
oc new-project theoworks -
Set these once:
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
helm install theoworks oci://public.ecr.aws/d9m2o0n8/chart/theoworks \
-f values.yamlThis 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:
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@$DIGESTcosign 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:
helm install theoworks ./theoworks-enterprise-$THEOWORKS_VERSION.tgz \
--set image.registry=$REGISTRY \
--set image.digest=$DIGEST \
-f values.yamlEnterprise, 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):
helm registry login 366258937776.dkr.ecr.eu-central-1.amazonaws.com --username AWSPaste 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:
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_CFGName it in your values with image.pullSecrets: ["theoworks-pull"] (already
shown above), then install:
helm install theoworks oci://366258937776.dkr.ecr.eu-central-1.amazonaws.com/charts/theoworks-enterprise \
--version "$THEOWORKS_VERSION" \
-f values.yamlEnd the registry session:
helm registry logout 366258937776.dkr.ecr.eu-central-1.amazonaws.comA 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:
oc wait --for=condition=Ready pod -l app.kubernetes.io/instance=theoworks --timeout=10mThis 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(keylicense.jsonby default). To apply a renewed license, update that Secret in place. You don't needhelm upgradeor a restart: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.contentinstead, replace it in your values file and runhelm 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):
helm upgrade theoworks oci://public.ecr.aws/d9m2o0n8/chart/theoworks \
-f values.yamlEnterprise, 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.
helm registry login 366258937776.dkr.ecr.eu-central-1.amazonaws.com --username AWSRefresh the image pull secret so your cluster can pull the new version's image, with the same password-safe sequence Install used:
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_CFGexport 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.yamlhelm registry logout 366258937776.dkr.ecr.eu-central-1.amazonaws.comThe 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:
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.yamlOn 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:
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:
skopeo login 366258937776.dkr.ecr.eu-central-1.amazonaws.com --username AWSIf 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:
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.comCopy 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):
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_AUTHDIRThis 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.
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.comOnce 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:
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 AWSPaste the password when prompted.
Find the new version's image, read from the new chart, never from the running pod:
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:
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.comThis 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:
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.comYour 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
- Kubernetes guide — any other Kubernetes cluster.
- Self-hosting — the single-machine launcher path.
- Install guide — every way to run TheoWorks.