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, WSO2 API Manager 4.6.0 uses standard Kubernetes Ingress. An NGINX Ingress Controller must be installed on your OpenShift cluster before deploying.

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.
  4. NGINX Ingress Controller — must be installed on the OpenShift cluster before the Helm chart is deployed.

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> -u <USERNAME> -p <PASSWORD>
    
    oc login <API_SERVER_URL> --token=<TOKEN>
    
  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

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

Step 4 — Install NGINX Ingress Controller

WSO2 API Manager 4.6.0 uses Kubernetes Ingress for external access. Install the NGINX Ingress Controller on your OpenShift cluster:

helm upgrade --install ingress-nginx ingress-nginx \
  --repo https://kubernetes.github.io/ingress-nginx \
  --namespace ingress-nginx \
  --create-namespace \
  --set controller.service.type=LoadBalancer \
  --set controller.admissionWebhooks.enabled=false

OpenShift's default Security Context Constraints (SCCs) block the NGINX controller pod from starting. Grant the privileged SCC to its service account and restart the deployment:

oc adm policy add-scc-to-user privileged system:serviceaccount:ingress-nginx:ingress-nginx
kubectl rollout restart deployment ingress-nginx-controller -n ingress-nginx

Wait for the controller pod to be ready:

kubectl get pods -n ingress-nginx -w

Make sure the deployment is up and running and the pods are ready before proceeding.

Step 5 — 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.6.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.6.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
    • 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 6 — 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 7.

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 7 — 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 8 — Deploy the All-in-One

  1. Download the OpenShift default values file:

    curl -L https://raw.githubusercontent.com/wso2/helm-apim/4.6.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 5:

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

    Enable OpenShift mode:

    kubernetes:
      openshift:
        enabled: true
    

    Note

    The default_openshift_values.yaml has openshift.enabled: false — make sure to set this to true.

    Ingress hostnames — update to match your cluster's DNS:

    kubernetes:
      ingress:
        management:
          hostname: "am.example.com"
        gateway:
          hostname: "gw.example.com"
        websocket:
          hostname: "websocket.example.com"
        websub:
          hostname: "websub.example.com"
    

    Database connection (skip if using H2):

    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>"
    
  3. Deploy:

    helm install apim wso2/wso2am-all-in-one \
      --version 4.6.0-1 \
      --namespace apim \
      --dependency-update \
      -f values.yaml
    
  4. Wait for the pod to be ready:

    oc get pods -n apim -w
    

    The pod should show 1/1 Running before proceeding.

Step 9 — Verify Ingress

The Helm chart creates Kubernetes Ingress objects automatically. Verify they were created:

kubectl get ing -n apim

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

Step 10 — Configure DNS

You need to point your ingress hostnames to the NGINX Ingress Controller's external address.

  1. Get the NGINX controller's external IP:

    kubectl get svc -n ingress-nginx ingress-nginx-controller
    

    Note the EXTERNAL-IP value from the output.

  2. Map the hostnames to that IP:

    CRC does not provision a load balancer, so EXTERNAL-IP will show <pending>. The NGINX NodePorts (30400 for HTTPS) are not forwarded from the Mac to the CRC VM, so the browser cannot reach them directly.

    The recommended workaround is to create OpenShift Routes manually. CRC's built-in HAProxy router handles port 443 traffic natively, so Routes work on standard ports without any extra configuration:

    oc create route passthrough apim-management \
      --service=apim-wso2am-all-in-one-am-service \
      --port=9443 \
      --hostname=am.wso2.com \
      -n apim
    
    oc create route passthrough apim-gateway \
      --service=apim-wso2am-all-in-one-am-service \
      --port=8243 \
      --hostname=gw.wso2.com \
      -n apim
    
    oc create route passthrough apim-websocket \
      --service=apim-wso2am-all-in-one-am-service \
      --port=8099 \
      --hostname=websocket.wso2.com \
      -n apim
    
    oc create route passthrough apim-websub \
      --service=apim-wso2am-all-in-one-am-service \
      --port=8021 \
      --hostname=websub.wso2.com \
      -n apim
    

    Then add the hostnames to your /etc/hosts:

    sudo sh -c 'echo "127.0.0.1 am.wso2.com gw.wso2.com websocket.wso2.com websub.wso2.com" >> /etc/hosts'
    

    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.

Step 11 — Access the Portals

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

If you created Routes manually in Step 10, access the portals on the standard port via the CRC HAProxy router:

Portal URL
Publisher https://am.wso2.com/publisher
Developer Portal https://am.wso2.com/devportal
Admin Portal https://am.wso2.com/admin
Carbon Console https://am.wso2.com/carbon
Gateway https://gw.wso2.com
Portal URL
Publisher https://<kubernetes.ingress.management.hostname>/publisher
Developer Portal https://<kubernetes.ingress.management.hostname>/devportal
Admin Portal https://<kubernetes.ingress.management.hostname>/admin
Carbon Console https://<kubernetes.ingress.management.hostname>/carbon
Gateway https://<kubernetes.ingress.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:
  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
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 5 to each component's base image (wso2/wso2am-acp:4.6.0, wso2/wso2am-tm:4.6.0, wso2/wso2am-universal-gw:4.6.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. Set kubernetes.openshift.enabled: true in each component's values file.

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 5. 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

Ingress not reachable

Symptom: Hostnames don't resolve or the browser cannot connect.

Fix:

  1. Confirm the NGINX Ingress Controller has an external IP:

    kubectl get svc -n ingress-nginx ingress-nginx-controller
    

    If EXTERNAL-IP is <pending>, the load balancer hasn't been provisioned yet. On CRC this may stay pending — use 127.0.0.1 instead.

  2. Confirm the Ingress objects were created:

    kubectl get ing -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.