Skip to content

Deploy WSO2 API Manager on OpenShift

OpenShift is a Kubernetes distribution with stricter security defaults. The core deployment approach — Helm charts, the same patterns (P0–P5) — is identical to standard Kubernetes. The key difference is that OpenShift ignores the UID defined in the Docker image and injects a random UID at runtime, which requires additional file permission configuration in the image and specific security context settings in the Helm values.

For routing, OpenShift supports Route objects (native to OpenShift), Envoy Gateway (the recommended API Gateway), and standard Kubernetes Ingress (deprecated). The default OpenShift values file uses Routes.

OpenShift deployment requires the following before deploying:

  1. A custom Docker image — with GID 0 group-write permissions and the JDBC driver for your database. Standard WSO2 images will fail to start on OpenShift.
  2. An external database — required for distributed deployments (P1–P5). The All-in-One pattern (P0) can use the embedded H2 database for evaluation purposes.
  3. Database schema initialised — if using an external database, run the WSO2 schema scripts against both databases before the pods start.

Quick Start

Step 1 — Install Required Tools

  1. Ensure the following tools are installed on your machine:

    Tool Purpose Install Guide
    oc OpenShift CLI for managing cluster resources Install
    kubectl Kubernetes CLI (used alongside oc) Install
    helm (v3) Package manager for deploying WSO2 Helm charts Install
    docker Required to build and push custom WSO2 images Install
  2. Verify all tools are installed:

    oc version
    helm version
    docker info
    

Step 2 — Log in to OpenShift

  1. Authenticate with the OpenShift CLI:

    oc login <API_SERVER_URL> --token=<TOKEN>
    
    oc login <API_SERVER_URL> -u <USERNAME> -p <PASSWORD>
    
  2. Once authenticated, verify your connection and check your currently selected project:

    # Verify connection
    oc whoami
    
    # Check current project
    oc project
    

Important

Always create a dedicated project for your deployments instead of using the default to ensure proper resource isolation and security policy enforcement. Using a separate project allows you to manage access control and quotas independently from cluster infrastructure services.

  • To create a new project:
    oc new-project <PROJECT-NAME>
    

Creating an OpenShift project automatically creates the corresponding Kubernetes namespace and switches the current context to it.

Step 3 — Add the WSO2 Helm Repository

  1. Add the WSO2 Helm repository and update it:

    helm repo add wso2 https://helm.wso2.com && helm repo update
    

Step 4 — Build an OpenShift-Compatible Docker Image

Standard WSO2 images run as a fixed UID (wso2carbon, UID 802). OpenShift injects a random UID at runtime, so any directories the server writes to must have group-write permissions with root group (GID 0) ownership.

Why GID 0?

OpenShift assigns a random UID to the container process but always uses GID 0 (root group). By granting group-write access to the root group, the container can write to its directories regardless of which UID OpenShift assigns. See Red Hat's guide to OpenShift and UIDs and group ownership and file permission for more detail.

  1. Create a Dockerfile for the All-in-One image:

    FROM wso2/wso2am:4.7.0
    
    # Grant root group write access for OpenShift arbitrary UID support
    USER root
    RUN chgrp -R 0 /home/wso2carbon && chmod -R g=u /home/wso2carbon
    USER wso2carbon
    

    WSO2 images do not include a JDBC driver. Add the driver for your database:

    FROM wso2/wso2am:4.7.0
    
    # Grant root group write access for OpenShift arbitrary UID support
    USER root
    RUN chgrp -R 0 /home/wso2carbon && chmod -R g=u /home/wso2carbon
    USER wso2carbon
    
    # Add MySQL JDBC driver — replace URL for other databases
    ADD --chown=wso2carbon:wso2 \
      https://repo1.maven.org/maven2/mysql/mysql-connector-java/8.0.28/mysql-connector-java-8.0.28.jar \
      ${WSO2_SERVER_HOME}/repository/components/lib/
    
  2. Build and push the image, replacing <REGISTRY> and <TAG> with your values:

    docker buildx build --platform linux/amd64 -t <REGISTRY>/wso2am-ocp:<TAG> .
    docker push <REGISTRY>/wso2am-ocp:<TAG>
    

    Matching your cluster architecture

    The --platform flag must match the architecture of your cluster nodes:

    • Managed clusters (AKS, ROSA, ARO) — use linux/amd64 by default
    • ARM based VMs — use linux/arm64

    Using the wrong platform will cause ErrImagePull when the pod starts. You can check a node's architecture with kubectl get nodes -o wide

  3. Get the image digest — you will need it when configuring your values file:

    docker inspect <REGISTRY>/wso2am-ocp:<TAG> \
      --format='{{index .RepoDigests 0}}'
    

Step 5 — Set Up the Database

The All-in-One pattern uses an embedded H2 database by default, which is suitable for evaluation. For production deployments, set up an external database before the pods start.

If you are using H2, skip this step and proceed to Step 6.

For production, follow the Setting Up Databases guide to:

  1. Set up a database instance accessible from your cluster
  2. Obtain the schema scripts for your database type
  3. Run the scripts to initialise both databases

Step 6 — Create the Keystore Secret

The Helm chart mounts a Kubernetes secret named apim-keystore-secret as a volume into the pods. The pods will not start if this secret does not exist.

  1. Extract the default keystores from your image and create the secret:

    mkdir -p keystores
    
    docker run --rm -v "$(pwd)/keystores:/keystores" --entrypoint bash \
      <REGISTRY>/wso2am-ocp:<TAG> \
      -c "cp \${WSO2_SERVER_HOME}/repository/resources/security/wso2carbon.jks \
             \${WSO2_SERVER_HOME}/repository/resources/security/client-truststore.jks \
             /keystores/"
    
    kubectl create secret generic apim-keystore-secret \
      --from-file=wso2carbon.jks=keystores/wso2carbon.jks \
      --from-file=client-truststore.jks=keystores/client-truststore.jks \
      -n apim
    
  2. Verify the secret was created:

    kubectl get secret apim-keystore-secret -n apim
    

Note

  • Make sure to create the secret inside the namespace which will be used for installing the API Manager.
  • The commands above use the default WSO2 keystores which are suitable for evaluation only. For production-level keystore setup, refer to Configuring Keystores in WSO2 API Manager.

Step 7 — Deploy the All-in-One

  1. Download the OpenShift default values file:

    curl -L https://raw.githubusercontent.com/wso2/helm-apim/4.7.x/all-in-one/default_openshift_values.yaml \
      -o values.yaml
    
  2. Open values.yaml and update the following sections:

    Custom image — point to the OpenShift-compatible image you built in Step 4:

    wso2:
      deployment:
        image:
          registry: "<YOUR_REGISTRY>"
          repository: "wso2am-ocp"
          tag: "<YOUR_TAG>"
          digest: "sha256:..."
    

    Database connection:

    wso2:
      apim:
        configurations:
          databases:
            apim_db:
              url: "<JDBC_URL_FOR_APIM_DB>"
              username: "<DB_USERNAME>"
              password: "<DB_PASSWORD>"
            shared_db:
              url: "<JDBC_URL_FOR_SHARED_DB>"
              username: "<DB_USERNAME>"
              password: "<DB_PASSWORD>"
    

    Route hostnames — update the hostnames to match your cluster's DNS:

    Prefer Ingress over Routes?

    The default_openshift_values.yaml includes both a kubernetes.route block and a kubernetes.ingress block. Routes are enabled by default and ingress is disabled. If you have an NGINX Ingress Controller installed on your OpenShift cluster and prefer to use Ingress, set kubernetes.route.management.enabled, kubernetes.route.gateway.enabled, etc. to false, and set the corresponding kubernetes.ingress.* entries to true.

    kubernetes:
      route:
        management:
          hostname: "am.example.com"
        gateway:
          hostname: "gw.example.com"
        websocket:
          hostname: "websocket.example.com"
        websub:
          hostname: "websub.example.com"
    
  3. Deploy:

    helm install apim wso2/wso2am-all-in-one \
      --version 4.7.0-1 \
      --namespace apim \
      --dependency-update \
      -f values.yaml \
      --set wso2.apim.configurations.encryption.key=$(openssl rand -hex 32)
    

    Encryption key is mandatory

    WSO2 API Manager 4.7.0 requires a 256-bit encryption key to be set before the first startup. The command above generates one automatically using openssl. If you are deploying to a shared or production environment, generate the key separately and store it securely — you will need the same key if you redeploy or scale the deployment. For more information, see Configuring Encryption Key.

  4. Wait for the pod to be ready:

    oc get pods -n apim -w
    

    The pod should show 1/1 Running before proceeding.

Step 8 — Verify Routes

The Helm chart creates OpenShift Route objects automatically. Verify they were created:

oc get routes -n apim

You should see routes for management, gateway, websocket, and websub with the hostnames you configured.

Step 9 — Configure DNS

You need to point your route hostnames to the correct address depending on your environment.

The CRC VM is always reachable at 127.0.0.1 from your local machine. Add the route hostnames to your /etc/hosts file:

sudo sh -c 'echo "127.0.0.1 am.wso2.com gw.wso2.com websocket.wso2.com websub.wso2.com" >> /etc/hosts'
  1. Get the router's external IP:

    oc get svc router-default -n openshift-ingress
    

    Note the EXTERNAL-IP value from the output.

  2. For quick testing, add the external address to your /etc/hosts file:

    <EXTERNAL-IP> am.wso2.com gw.wso2.com websocket.wso2.com websub.wso2.com
    

For a production setup, create DNS records in your DNS provider mapping the hostnames to the external IP instead of using /etc/hosts.

Note

These are the default hostnames. If you customised the hostnames in your values.yaml, use those values here instead (kubernetes.route.management.hostname, kubernetes.route.gateway.hostname, etc.).

Step 10 — Access the Portals

Once DNS is configured, open the following URLs in your browser:

Portal URL
Publisher https://<kubernetes.route.management.hostname>/publisher
Developer Portal https://<kubernetes.route.management.hostname>/devportal
Admin Portal https://<kubernetes.route.management.hostname>/admin
Carbon Console https://<kubernetes.route.management.hostname>/carbon
Gateway https://<kubernetes.route.gateway.hostname>

Replace the hostname placeholders with the actual values from your values.yaml. With default values, the management hostname is am.wso2.com and the gateway hostname is gw.wso2.com.

Default credentials: admin / admin


Advanced Configuration

1. OpenShift Security Context

Every component deployed on OpenShift requires the following block in its values file. The default_openshift_values.yaml used in the Quick Start already includes this — you only need to add it manually when creating values files for distributed deployments.

kubernetes:
  openshift:
    enabled: true
  securityContext:
    # Allow OpenShift to assign arbitrary UIDs
    runAsUser: null
    seLinux:
      enabled: false
      level: ""
    seccompProfile:
      type: RuntimeDefault
      localhostProfile: ""
  # AppArmor is not available on OpenShift (which uses SELinux instead)
  enableAppArmor: false
  configMaps:
    scripts:
      # Startup scripts mounted via ConfigMap require execute permission
      defaultMode: "0457"
Setting Why it's needed
openshift.enabled: true Enables OpenShift-specific filesystem adjustments during deployment
runAsUser: null Lets OpenShift assign its random UID instead of the image's fixed UID
enableAppArmor: false AppArmor is not supported on OpenShift
seLinux.enabled: false Leave off unless your cluster has SELinux policies configured for WSO2
configMaps.scripts.defaultMode: "0457" Startup scripts mounted as a ConfigMap need execute permission

2. Distributed Deployments

For distributed patterns, follow the corresponding Kubernetes pattern guide and apply the OpenShift-specific changes described in this page on top.

Pattern Guide
Pattern 1 — HA All-in-One am-pattern-1-all-in-one-ha.md
Pattern 2 — All-in-One + Gateway am-pattern-2-all-in-one-gw.md
Pattern 3 — ACP + TM + Gateway am-pattern-3-acp-tm-gw.md
Pattern 4 — ACP + TM + Gateway + KM am-pattern-4-acp-tm-gw-km.md
Pattern 5 — All-in-One + Gateway + KM am-pattern-5-all-in-one-gw-km.md

For each component in your chosen pattern:

  1. Build an OpenShift-compatible image — apply the same USER root / chgrp / chmod / USER wso2carbon pattern from Step 4 to each component's base image (wso2/wso2am-acp:4.7.0, wso2/wso2am-tm:4.7.0, wso2/wso2am-universal-gw:4.7.0). TM and Gateway do not need the JDBC driver.
  2. Add the security context block from Section 1 to each component's values file.
  3. Use the same encryption key across all components — generate it once and pass it to every helm install command:

    export APIM_ENCRYPTION_KEY=$(openssl rand -hex 32)
    

Example — Pattern 3 (ACP + TM + Gateway):

Create a values file for each component. For the ACP (values-acp.yaml):

kubernetes:
  openshift:
    enabled: true
  securityContext:
    runAsUser: null
    seLinux:
      enabled: false
      level: ""
    seccompProfile:
      type: RuntimeDefault
      localhostProfile: ""
  enableAppArmor: false
  configMaps:
    scripts:
      defaultMode: "0457"

wso2:
  deployment:
    image:
      registry: "<YOUR_REGISTRY>"
      repository: "<YOUR_ACP_IMAGE>"
      tag: "<YOUR_TAG>"
      digest: "sha256:..."
  apim:
    configurations:
      databases:
        apim_db:
          url: "<JDBC_URL_FOR_APIM_DB>"
          username: "<DB_USERNAME>"
          password: "<DB_PASSWORD>"
        shared_db:
          url: "<JDBC_URL_FOR_SHARED_DB>"
          username: "<DB_USERNAME>"
          password: "<DB_PASSWORD>"

Create equivalent values-tm.yaml and values-gw.yaml with the security context block and the custom image reference — TM and GW do not require database configuration.

Deploy in order:

export APIM_ENCRYPTION_KEY=$(openssl rand -hex 32)

helm install apim-acp wso2/wso2am-acp \
  --version 4.7.0-1 \
  --namespace apim --create-namespace \
  --dependency-update \
  -f values-acp.yaml \
  --set wso2.apim.configurations.encryption.key=$APIM_ENCRYPTION_KEY

helm install apim-tm wso2/wso2am-tm \
  --version 4.7.0-1 \
  --namespace apim \
  --dependency-update \
  -f values-tm.yaml \
  --set wso2.apim.configurations.encryption.key=$APIM_ENCRYPTION_KEY

helm install apim-gw wso2/wso2am-universal-gw \
  --version 4.7.0-1 \
  --namespace apim \
  --dependency-update \
  -f values-gw.yaml \
  --set wso2.apim.configurations.encryption.key=$APIM_ENCRYPTION_KEY

Troubleshooting

Permission denied errors

Symptom: Pod fails to start with Permission denied in the logs.

Cause: The container process cannot write to a directory because the image was not prepared with GID 0 group ownership.

Fix: Ensure your Dockerfile includes the chgrp -R 0 and chmod -R g=u steps from Step 4. Check the pod logs and the applied security context:

oc logs <pod-name> -n apim
oc get pod <pod-name> -n apim -o yaml | grep -A 10 securityContext

Image pull errors

Symptom: Pod stuck in ImagePullBackOff or ErrImagePull.

Fix: Create an image pull secret and link it to the service account:

oc create secret docker-registry registry-credentials \
  --docker-server=<YOUR_REGISTRY> \
  --docker-username=<USERNAME> \
  --docker-password=<PASSWORD> \
  -n apim

oc secrets link default registry-credentials --for=pull -n apim

Volume mount errors

Symptom: Pod crashes with volume-related errors.

Fix: Check that PersistentVolumeClaims are bound:

oc get pvc -n apim

If a volume fails due to fsGroup permissions, patch the deployment:

oc patch deployment <deployment-name> -n apim \
  -p '{"spec":{"template":{"spec":{"securityContext":{"fsGroup":0}}}}}'

Inter-component communication failures

Symptom: Components cannot reach each other (e.g. GW cannot connect to ACP).

Cause: OpenShift may enforce network policies that block cross-pod traffic within the namespace.

Fix: Check for blocking policies and apply a permissive one for all APIM pods:

oc get netpol -n apim
oc apply -f - <<EOF
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-apim-internal
  namespace: apim
spec:
  podSelector: {}
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: apim
EOF

This allows all pods within the apim namespace to communicate with each other.