Portworx Enterprise Deployment on StarlingX

Overview

Portworx is an enterprise-grade, Kubernetes-native storage and data management platform. It automates persistent storage, high availability, data protection, and disaster recovery for containers and virtual machines across multi-cloud and on-premises environments.

This guide installs Portworx on StarlingX and runs it as a persistent-storage backend. The Portworx cluster install is tested for both PX-StoreV1 (Btrfs) and PX-StoreV2 (DM-thin) datastores.

Note

PX-StoreV1 is the legacy Portworx storage backend that leverages Btrfs, a Linux copy-on-write filesystem.

PX-StoreV2 is the newer Portworx storage engine that uses Linux DM-thin for improved performance and scalability.

Portworx requires the px kernel module (px-fuse), which is not included in the stock kernel. The KMM application, part of the StarlingX release, is used to build and load it on every Portworx worker — storage and application.

Both online and air-gapped installation paths are documented below. Air-gapped steps are marked with Air-gapped and can be skipped for online deployments.

Note

If upgrading StarlingX, then install Portworx after the upgrade is finalized, as Upgrade, downgrade and Backup/Restore of Portworx are not supported on StarlingX.

Prerequisites

StarlingX must be installed and fully deployed before performing this procedure.

  • Standard deployment: two controllers, three storage nodes (minimum requirement for Portworx), and one application worker.

  • Portworx storage nodes can only be workers.

  • At least 8 application CPU cores per storage worker. See Test Environment Configuration.

    Note

    StarlingX reserves the platform core(s), so size each worker with enough CPU to leave at least 8 application cores (for example 10 CPU yields 9 application cores). Verify with system host-cpu-list <worker>.

  • 4 GB (min) to 8 GB (desired) of memory available for Portworx per storage worker.

  • One dedicated disk per storage worker for the Portworx data and metadata partitions (/dev/sdc in this guide). 256 GB was used for testing.

    Note

    The disk must be large enough to hold the metadata partition (64 GB) and the data partition (the remainder). If the disk is too small, the install fails. The application worker needs no data disk.

  • A dedicated cluster-host interface on every storage worker.

    Note

    The cluster-host interface must be separate from the management interface.

  • Connectivity:

    • Online: Active Internet connection to clone the px-fuse source and pull the Portworx Helm chart.

    • Air-gapped: An Internet-connected staging machine to pre-stage the build bases, px-fuse source, and images, plus a means to transfer files to the air-gapped system.

Test Environment Configuration

Resource

Controller (x2)

Storage worker (x3)

Application worker (x1)

Hostname

controller-0, controller-1

worker-0, worker-1, worker-2

worker-3

Role

Control plane

Portworx storage

Portworx compute

CPU

8

10

4

VM memory

20 GB

16 GB

12 GB

Disks

sda (100 GB), sdb (5 GB)

sda (100 GB), sdb (5 GB), sdc (256 GB (Portworx))

sda (100 GB), sdb (5 GB)

Air-Gapped Preparation

Note

Skip the steps in this section if it is an online installation.

Procedure

Perform the following steps on an Internet-connected staging machine.

  1. Fetch base docker images and px-fuse source.

    KMM_BUILDER_TAG="stx.13.0-v1.0.0"
    # Bake autoconf into the stock kmm-builder image and tag it for the local registry.
    cat > Dockerfile.kmm-builder-base <<EOF
    FROM docker.io/starlingx/kmm-builder:${KMM_BUILDER_TAG}
    RUN apt-get update && apt-get install -y autoconf && rm -rf /var/lib/apt/lists/*
    EOF
    docker build -f Dockerfile.kmm-builder-base \
        -t registry.local:9001/starlingx/kmm-builder:${KMM_BUILDER_TAG} .
    
    # Bake kmod into debian:trixie-slim and tag it for the local registry.
    cat > Dockerfile.trixie-base <<'EOF'
    FROM debian:trixie-slim
    RUN apt-get update && apt-get install -y kmod && rm -rf /var/lib/apt/lists/*
    EOF
    docker build -f Dockerfile.trixie-base \
        -t registry.local:9001/debian:trixie-slim .
    
    # Clone the px-fuse source and pack it for transfer.
    git clone https://github.com/portworx/px-fuse.git
    
  2. Identify KMM application images.

    # Prerequisites:
    # 1. Copy the chart from the controller:
    #    scp controller-0:/usr/local/share/applications/helm/kernel-module-management-*.tgz .
    # 2. Extract the charts directory:
    #    tar xzf kernel-module-management-*.tgz
    # 3. cd into the extracted charts directory
    
    # List KMM images referenced by the KMM application chart
    helm template kernel-module-management-*.tgz \
    | grep -Eo '^[[:space:]]*(-[[:space:]]+)?(image|value):[[:space:]]+"?[^"[:space:]]+/[^"[:space:]]+:[^"[:space:]]+' \
    | sed -E 's/^.*(image|value):[[:space:]]+"?//' \
    | sort -u
    
    # Save the KMM image list, one per line
    IMAGES_KMM_FILE=images-kmm.txt
    cat > "$IMAGES_KMM_FILE" <<'EOF'
    gcr.io/k8s-staging-kmm/kernel-module-management-operator:v20260415-v2.6.0
    gcr.io/k8s-staging-kmm/kernel-module-management-worker:v20260415-v2.6.0
    gcr.io/k8s-staging-kmm/kernel-module-management-signimage:v20260415-v2.6.0
    gcr.io/k8s-staging-kmm/kernel-module-management-webhook-server:v20260415-v2.6.0
    gcr.io/kaniko-project/executor:236ba5690eda9170d0157aa8137ebbeb09d38685
    EOF
    
  3. Fetch the Portworx Helm Chart and discover container images.

    PX_VERSION="3.6.0"
    K8S_VERSION="1.35.2" # K8s version on the controller (kubectl version)
    
    helm repo add portworx https://raw.githubusercontent.com/portworx/helm/master/stable
    helm repo update
    helm pull portworx/portworx --version 9.0.0
    
    # Images from the portworx version manifest
    IMAGES_FROM_YAML_FILE=images-from-yaml.txt
    curl -fsSL -o versions.yaml \
        "https://install.portworx.com/${PX_VERSION}/version?kbver=${K8S_VERSION}"
    grep -Eo '\S+/\S+:\S+' versions.yaml | sort -u > "$IMAGES_FROM_YAML_FILE"
    
    # Images from the portworx air-gapped install script
    IMAGES_FROM_SH_FILE=images-from-sh.txt
    curl -fsSL -o px-ag-install.sh "https://install.portworx.com/${PX_VERSION}/air-gapped"
    eval "$(grep '^IMAGES=' px-ag-install.sh)"
    printf '%s\n' $IMAGES | sort -u > "$IMAGES_FROM_SH_FILE"
    
    # Images from the portworx Helm chart
    IMAGES_FROM_HELM_CHART=images-from-helm.txt
    helm template portworx/portworx --version 9.0.0 \
        | grep -Eo '\S+/\S+:\S+' | tr -d '"' | sort -u > "$IMAGES_FROM_HELM_CHART"
    

    Important

    The Portworx Helm chart bundles an alpine/kubectl image (e.g. alpine/kubectl:1.36.0). This must match the Kubernetes Server Version on the target controller, hence replace the kubectl image tag in $IMAGES_FROM_HELM_CHART with the correct version:

    # Replace the kubectl image version to match target cluster
    sed -i "s|alpine/kubectl:.*|alpine/kubectl:${K8S_VERSION}|" "$IMAGES_FROM_HELM_CHART"
    cat "$IMAGES_FROM_HELM_CHART"
    
  4. Export artifacts for transfer to the target system.

    KMM_BUILDER_TAG="stx.13.0-v1.0.0"
    SRC_DIR="$PWD"
    mkdir -p px-airgap && cd px-airgap
    
    # Base images for the controller build
    sudo skopeo copy --override-arch amd64 --override-os linux \
        "docker-daemon:registry.local:9001/starlingx/kmm-builder:${KMM_BUILDER_TAG}" \
        "docker-archive:kmm-builder-base.tar:registry.local:9001/starlingx/kmm-builder:${KMM_BUILDER_TAG}"
    sudo skopeo copy --override-arch amd64 --override-os linux \
        "docker-daemon:registry.local:9001/debian:trixie-slim" \
        "docker-archive:trixie-slim.tar:registry.local:9001/debian:trixie-slim"
    
    # Portworx + KMM images, read from the three list files created in previous steps
    for list in "$IMAGES_FROM_SH_FILE" "$IMAGES_FROM_YAML_FILE" "$IMAGES_KMM_FILE" "$IMAGES_FROM_HELM_CHART"; do
        while read -r img; do
        [ -n "$img" ] || continue
        ref="$img"
        ref="${ref#docker.io/}"; ref="${ref#registry.k8s.io/}"
        file="$(echo "$ref" | tr '/:' '__').tar"
        skopeo copy --override-arch amd64 --override-os linux \
            "docker://$img" "docker-archive:$file:registry.local:9001/$ref" || echo "FAIL $img"
        done < "$SRC_DIR/$list"
    done
    
    cd "$SRC_DIR"
    tar czf portworx-airgap-bundle.tar.gz \
        px-airgap/ px-fuse/ versions.yaml portworx-9.0.0.tgz
    

    Transfer portworx-airgap-bundle.tar.gz to the air-gapped StarlingX controller.

  5. Load the artifacts into registry.local (controller-0) and perform the following steps on the active controller (controller-0).

    tar xzf portworx-airgap-bundle.tar.gz
    
    USERNAME="sysinv"
    PASSWORD=$(keyring get sysinv services)
    echo "$PASSWORD" | sudo docker login registry.local:9001 -u "$USERNAME" --password-stdin
    
    # Load and push container images
    for img_tar in px-airgap/*.tar;
     do sudo docker load -i "$img_tar";
    done
    
    sudo docker images --format '{{.Repository}}:{{.Tag}}' \
        | grep '^registry.local:9001' \
        | while read -r img; do sudo docker push "$img"; done
    
    # Push the Helm chart as an OCI artifact
    echo "$PASSWORD" | helm registry login registry.local:9001 -u "$USERNAME" --password-stdin
    helm push portworx-9.0.0.tgz oci://registry.local:9001/helm-charts
    
    # Verify the bases and chart are present
    system registry-image-tags starlingx/kmm-builder
    system registry-image-tags debian
    helm show chart oci://registry.local:9001/helm-charts/portworx --version 9.0.0
    

Build px-fuse Kernel Module

Procedure

Perform the following steps on the active controller (controller-0).

  1. Install the KMM application.

    # Upload the KMM application and wait until it is 'uploaded'
    system application-upload \
        /usr/local/share/applications/helm/kernel-module-management-*.tgz
    watch -n 10 system application-show kernel-module-management
    
    # Local-registry credentials, base64-encoded for the docker config
    USERNAME="sysinv"
    PASSWORD=$(keyring get sysinv services)
    DOCKER_CREDENTIALS=$(echo -n "${USERNAME}":"${PASSWORD}" | base64)
    
    # Build a docker auth config so KMM can pull from registry.local
    cat >/tmp/docker-config.json <<EOF
    {
        "auths": {
        "https://registry.local:9001": {
            "auth": "$DOCKER_CREDENTIALS"
        }
        }
    }
    EOF
    
    # Encode the whole config; KMM stores it in the pull secret
    dconfigjson=$(cat /tmp/docker-config.json | base64 -w 0)
    
    # Helm override that wires the pull secret into the KMM app
    cat >~sysadmin/kmm-app-override.yaml <<EOF
    dockerRegistrySecretName: "kmm-registry-secret"
    dockerConfigJson: "$dconfigjson"
    EOF
    
    # Apply the override and bring the app up, then wait until 'applied'
    system helm-override-update kernel-module-management \
        kernel-module-management kernel-module-management \
        --values ~sysadmin/kmm-app-override.yaml
    system application-apply kernel-module-management
    watch -n 10 system application-show kernel-module-management
    
  2. Build the px-fuse kernel module image.

    Online:

    KMM_BUILDER_TAG="stx.13.0-v1.0.0"
    cat >~sysadmin/kmm-px-cm.yaml <<EOF
    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: kmm-px-cm
      namespace: kernel-module-management
    data:
      dockerfile: |
        FROM docker.io/starlingx/kmm-builder:${KMM_BUILDER_TAG} as builder
        ARG KERNEL_FULL_VERSION
        RUN apt-get update && apt-get install -y autoconf
        WORKDIR /usr/src
        RUN ["git", "clone", "https://github.com/portworx/px-fuse.git"]
        WORKDIR /usr/src/px-fuse
        RUN autoreconf && ./configure && make KERNELPATH=/lib/modules/\${KERNEL_FULL_VERSION}/build && mkdir /out && cp *.ko /out/
        FROM debian:trixie-slim
        ARG KERNEL_FULL_VERSION
        RUN apt-get update && apt-get install -y kmod && rm -rf /var/lib/apt/lists/*
        COPY --from=builder /out/*.ko /opt/lib/modules/\${KERNEL_FULL_VERSION}/
        RUN depmod -b /opt \${KERNEL_FULL_VERSION}
    EOF
    
    kubectl apply -f ~sysadmin/kmm-px-cm.yaml
    

    Air-gapped:

    KMM_BUILDER_TAG="stx.13.0-v1.0.0"
    KERNEL_VERSION="6.18.15+deb13-amd64"
    KERNEL_VERSION_RT="6.18.15+deb13-rt-amd64"
    PX_FUSE_IMAGE_TAG="6.18.15_deb13-amd64"
    PX_FUSE_IMAGE_TAG_RT="6.18.15_deb13-rt-amd64"
    
    # Please make sure to use a KMM builder image aligned to the kernel version.
    # The KMM builder image supports the kernel version shipped in the same release.
    cat > Dockerfile.px-fuse <<EOF
    FROM registry.local:9001/starlingx/kmm-builder:${KMM_BUILDER_TAG} as builder
    ARG KERNEL_FULL_VERSION
    WORKDIR /usr/src/px-fuse
    COPY px-fuse/ .
    RUN autoreconf && ./configure && \\
        make KERNELPATH=/lib/modules/\${KERNEL_FULL_VERSION}/build && \\
        mkdir /out && cp *.ko /out/
    
    FROM registry.local:9001/debian:trixie-slim
    ARG KERNEL_FULL_VERSION
    COPY --from=builder /out/*.ko /opt/lib/modules/\${KERNEL_FULL_VERSION}/
    RUN depmod -b /opt \${KERNEL_FULL_VERSION}
    EOF
    
    sudo docker build -f Dockerfile.px-fuse \
    --build-arg KERNEL_FULL_VERSION="$KERNEL_VERSION" \
    -t "registry.local:9001/kmm/px:$PX_FUSE_IMAGE_TAG" .
    
    sudo docker build -f Dockerfile.px-fuse \
    --build-arg KERNEL_FULL_VERSION="$KERNEL_VERSION_RT" \
    -t "registry.local:9001/kmm/px:$PX_FUSE_IMAGE_TAG_RT" .
    
    echo "$(keyring get sysinv services)" \
        | sudo docker login registry.local:9001 -u sysinv --password-stdin
    sudo docker push registry.local:9001/kmm/px:$PX_FUSE_IMAGE_TAG
    sudo docker push registry.local:9001/kmm/px:$PX_FUSE_IMAGE_TAG_RT
    system registry-image-tags kmm/px
    
  3. Configure KMM module.

    KERNEL_VERSION="6.18.15+deb13-amd64"
    KERNEL_VERSION_RT="6.18.15+deb13-rt-amd64"
    PX_FUSE_IMAGE_TAG="6.18.15_deb13-amd64"
    PX_FUSE_IMAGE_TAG_RT="6.18.15_deb13-rt-amd64"
    
    # Module CRD: KMM builds and loads px on nodes labeled px-node=true,
    # mapping each kernel flavor to a distinct image in registry.local
    cat >~sysadmin/kmm-px-module.yaml <<EOF
    apiVersion: kmm.sigs.x-k8s.io/v1beta1
    kind: Module
    metadata:
      name: kmm-px
      namespace: kernel-module-management
    spec:
      moduleLoader:
        container:
          modprobe:
            moduleName: px
          kernelMappings:
            - literal: "${KERNEL_VERSION}"
              containerImage: "registry.local:9001/kmm/px:${PX_FUSE_IMAGE_TAG}"
              build:
                buildArgs:
                  - name: KERNEL_FULL_VERSION
                    value: "${KERNEL_VERSION}"
                baseImageRegistryTLS:
                  insecure: false
                  insecureSkipTLSVerify: true
                dockerfileConfigMap:
                  name: kmm-px-cm
              registryTLS:
                insecure: false
                insecureSkipTLSVerify: true
            - literal: "${KERNEL_VERSION_RT}"
              containerImage: "registry.local:9001/kmm/px:${PX_FUSE_IMAGE_TAG_RT}"
              build:
                buildArgs:
                  - name: KERNEL_FULL_VERSION
                    value: "${KERNEL_VERSION_RT}"
                baseImageRegistryTLS:
                  insecure: false
                  insecureSkipTLSVerify: true
                dockerfileConfigMap:
                  name: kmm-px-cm
              registryTLS:
                insecure: false
                insecureSkipTLSVerify: true
          imagePullPolicy: Always
      imageRepoSecret:
        name: "kmm-registry-secret"
      selector:
        kubernetes.io/os: linux
        px-node: "true"
    EOF
    
    # Create the Module — KMM starts the loader pods once workers are labeled
    kubectl apply -f ~sysadmin/kmm-px-module.yaml
    

    Warning

    Image Tag cannot contain “+” symbol, so replace “+” with “_” in the kmm/px image tag. E.g. if the kernel version is 6.18.15+deb13-amd64, then the image tag should be 6.18.15_deb13-amd64.

  4. Label workers and verify px-fuse installation.

    # Storage workers, px-node is a custom label for all workers
    for node in worker-0 worker-1 worker-2; do
        kubectl label node $node px-node="true"
        kubectl label node $node portworx.io/node-type=storage
    done
    
    # Application worker
    kubectl label node worker-3 px-node="true"
    kubectl label node worker-3 portworx.io/node-type=storageless
    
    # verify module loaded
    kubectl get modules.kmm.sigs.x-k8s.io -n kernel-module-management
    system registry-image-tags kmm/px
    
    for node in worker-0 worker-1 worker-2 worker-3; do
        echo "=== $node ==="
        ssh -t $node "sudo lsmod | grep '^px'"
    done
    

Install Portworx

Prerequisites

Perform the following steps on the active controller (controller-0).

Note

This procedure is validated using Portworx Enterprise 3.6.0 on StarlingX with kernel 6.18.15+deb13-amd64 (Debian 13 Trixie).

Replace the following values with those from your environment:

  • <portworx-disk-by-path> — Dedicated Portworx disk. E.g. pci-0000:00:1f.2-ata-3.0. Find with: ls -l /dev/disk/by-path/

  • <storage-interface> — Worker interface for data/management traffic. E.g. enp2s2. Find with: system host-if-list <worker>

  • <kernel-version> — Worker kernel. E.g. 6.18.15+deb13-amd64. Find with: uname -r

Procedure

  1. Prepare the disks.

    STORAGE_WORKERS="worker-0 worker-1 worker-2"
    
    for node in $STORAGE_WORKERS; do
        system host-disk-list $node
    done
    
    1. Pick, wipe and partition the disk on all storage workers.

      Warning

      The disk wipe command (system host-disk-wipe) erases all data on the selected disk.

      # Wipe disk
      for node in $STORAGE_WORKERS; do
          disk=$(system host-disk-list $node --nowrap \
              | awk /<portworx-disk-by-path>/'{print $2}')
          system host-disk-wipe $node $disk --confirm
      done
      for node in $STORAGE_WORKERS; do
          system host-disk-list $node
      done
      
      # 64 GiB metadata partition
      for node in $STORAGE_WORKERS; do
          disk=$(system host-disk-list $node --nowrap \
              | awk /<portworx-disk-by-path>/'{print $2}')
          system host-disk-partition-add $node $disk 64
      done
      watch -n 10 "for node in $STORAGE_WORKERS; do system host-disk-partition-list $node; done"
      
      # Data partition (remainder)
      for node in $STORAGE_WORKERS; do
          disk=$(system host-disk-list $node --nowrap \
              | awk /<portworx-disk-by-path>/'{print $2}')
          system host-disk-partition-add $node $disk 191
      done
      watch -n 10 "for node in $STORAGE_WORKERS; do system host-disk-partition-list $node; done"
      
      # Wipe partition signatures
      for node in $STORAGE_WORKERS; do
          echo "=== $node ==="
          ssh -t $node "sudo wipefs -a \
              /dev/disk/by-path/<portworx-disk-by-path>-part1 \
              /dev/disk/by-path/<portworx-disk-by-path>-part2 \
              && lsblk /dev/disk/by-path/<portworx-disk-by-path>"
      done
      
  2. Create Helm overrides.

    For PX-StoreV1 (Btrfs), confirm btrfs is available:

    for node in $STORAGE_WORKERS; do
        echo "=== $node ==="
        ssh -t $node "sudo modprobe btrfs; sudo lsmod | grep btrfs || modinfo btrfs"
    done
    
    cat <<'EOF' > ~sysadmin/portworx-install.yaml
    clusterName: px-storev1
    autopilot:
      enabled: false
    dataInterface: <storage-interface>
    drives: "/dev/disk/by-path/<portworx-disk-by-path>-part2"
    envVars: "none"
    installCertManager: false
    initialStorageNodes: 3
    kvdb:
      internal: true
    managementInterface: <storage-interface>
    systemMetadataDevice: "/dev/disk/by-path/<portworx-disk-by-path>-part1"
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
          - matchExpressions:
              - key: px-node
                operator: In
                values:
                  - "true"
    volumes:
      - name: docker-root
        mountPath: /var/lib/docker
        hostPath:
          path: /var/lib/docker
      - name: exec-sock
        mountPath: /var/run/exec
        mountPropagation: Bidirectional
        hostPath:
          path: /var/run/exec
    EOF
    

    For PX-StoreV2 (dm-thin), add the clusterAnnotations line:

    cat <<'EOF' > ~sysadmin/portworx-install.yaml
    clusterAnnotations: "portworx.io/misc-args= -T px-storev2"
    clusterName: px-storev2
    autopilot:
      enabled: false
    dataInterface: <storage-interface>
    drives: "/dev/disk/by-path/<portworx-disk-by-path>-part2"
    envVars: "none"
    installCertManager: false
    initialStorageNodes: 3
    kvdb:
      internal: true
    managementInterface: <storage-interface>
    systemMetadataDevice: "/dev/disk/by-path/<portworx-disk-by-path>-part1"
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
          - matchExpressions:
              - key: px-node
                operator: In
                values:
                  - "true"
    volumes:
      - name: docker-root
        mountPath: /var/lib/docker
        hostPath:
          path: /var/lib/docker
      - name: exec-sock
        mountPath: /var/run/exec
        mountPropagation: Bidirectional
        hostPath:
          path: /var/run/exec
    EOF
    
  3. Install the Helm chart.

    Online:

    # Add the public chart repository
    helm repo add portworx https://raw.githubusercontent.com/portworx/helm/master/stable
    helm repo update
    
    # Create the namespace
    kubectl create namespace portworx
    kubectl label namespace portworx app.starlingx.io/component=application
    
    # Install the Portworx cluster
    helm upgrade --install portworx portworx/portworx -n portworx -f ~sysadmin/portworx-install.yaml
    

    Air-gapped:

    # Point to local registry
    cat <<'EOF' >> ~sysadmin/portworx-install.yaml
    customRegistryURL: registry.local:9001
    registrySecret: default-registry-key
    EOF
    
    kubectl create namespace portworx
    kubectl label namespace portworx app.starlingx.io/component=application
    
    # Create an image pull secret for the local Docker registry (authentication is required by default).
    source /etc/platform/openrc
    kubectl -n portworx create secret docker-registry default-registry-key \
    --docker-server=registry.local:9001 \
    --docker-username=$OS_USERNAME --docker-password=$OS_PASSWORD
    
    # Version manifest — operator stays paused until this exists
    kubectl -n portworx create configmap px-versions --from-file=versions=versions.yaml
    
    # Install from local chart
    helm upgrade --install portworx ~sysadmin/portworx-9.0.0.tgz -n portworx -f ~sysadmin/portworx-install.yaml
    
  4. Verify the installation.

    kubectl get pods -n portworx -l name=portworx-operator
    kubectl get pods -n portworx -l name=portworx -o wide -w
    
    kubectl wait -n portworx --for=condition=Ready pods -lname=portworx --timeout=600s
    

Validate Cluster Status

Perform the following steps on the active controller (controller-0).

  1. Check cluster status.

    Note

    pxctl is shipped by Portworx, not by StarlingX. It is only present at /opt/pwx/bin/pxctl on the Portworx nodes themselves, so it cannot be run from a controller. Run it inside a portworx pod, as shown below;

    PX_POD=$(kubectl get pods -n portworx -l name=portworx -o jsonpath='{.items[0].metadata.name}')
    kubectl exec -it -n portworx $PX_POD -- /opt/pwx/bin/pxctl status
    
    sysadmin@controller-0:~$ PX_POD=$(kubectl get pods -n portworx -l name=portworx -o jsonpath='{.items[0].metadata.name}')
    sysadmin@controller-0:~$ kubectl exec -it -n portworx $PX_POD -- /opt/pwx/bin/pxctl status
     Status: PX is operational
     Telemetry: Healthy
     Metering: Disabled or Unhealthy
     Node ID: 1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d
             IP: 192.168.206.11
             Local Storage Pool: 1 pool
             POOL  IO_PRIORITY  RAID_LEVEL  USABLE   USED    STATUS  ZONE     REGION
             0     HIGH         raid0       173 GiB  35 MiB  Online  default  default
             Local Storage Devices: 1 device
             Device  Path        Media Type          Size     Last-Scan
             0:0     /dev/sdc2   STORAGE_MEDIUM_SSD  192 GiB  20 Apr 26 21:06 UTC
             total                                   192 GiB
             Cache Devices:
                 * No cache devices
             Metadata Device:
             1       /dev/sdc1   STORAGE_MEDIUM_SSD  64 GiB
                 * Internal kvdb on this node is using this dedicated metadata device to store its data.
     Cluster Summary
             Cluster ID: px-storev2
             Cluster UUID: aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
             Scheduler: kubernetes
             Total Nodes: 3 node(s) with storage (3 online), 1 node(s) without storage (1 online)
             IP               ID                                    SchedulerNodeName  Auth      StorageNode      Used    Capacity  Status  StorageStatus   Version          Kernel          OS
             192.168.206.13  3c4d5e6f-7a8b-9c0d-1e2f-3a4b5c6d7e8f  worker-2           Disabled  Yes(PX-StoreV2)  35 MiB  173 GiB   Online  Up              3.6.0.0-a81cf43  6.18.15+deb13-amd64  Debian GNU/Linux 13 (trixie)
             192.168.206.11  1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d  worker-0           Disabled  Yes(PX-StoreV2)  35 MiB  173 GiB   Online  Up (This node)  3.6.0.0-a81cf43  6.18.15+deb13-amd64  Debian GNU/Linux 13 (trixie)
             192.168.206.12  2b3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e  worker-1           Disabled  Yes(PX-StoreV2)  35 MiB  173 GiB   Online  Up              3.6.0.0-a81cf43  6.18.15+deb13-amd64  Debian GNU/Linux 13 (trixie)
             192.168.206.14  4d5e6f7a-8b9c-0d1e-2f3a-4b5c6d7e8f9a  worker-3           Disabled  No(PX-StoreV2)   0 B     0 B       Online  No Storage      3.6.0.0-a81cf43  6.18.15+deb13-amd64  Debian GNU/Linux 13 (trixie)
             Warnings:
                         WARNING: Persistent journald logging is not enabled on this node.
     Global Storage Pool
             Total Used      : 105 MiB
             Total Capacity  : 519 GiB
    
  2. Check StorageClasses.

    kubectl get storageclasses | grep portworx
    
  3. Validate with a PVC.

    # Minimal PVC against the Portworx replicated StorageClass
    cat <<'EOF' > ~sysadmin/portworx-test-pvc.yaml
    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      name: px-test-pvc
    spec:
      storageClassName: px-csi-replicated
      accessModes:
        - ReadWriteOnce
      resources:
        requests:
          storage: 5Gi
    EOF
    # Create, watch it reach Bound, then clean up
    kubectl apply -f ~sysadmin/portworx-test-pvc.yaml
    watch kubectl get pvc
    kubectl delete -f ~sysadmin/portworx-test-pvc.yaml
    kubectl get pvc
    

    The installation is confirmed when status of px-test-pvc shows Bound state.

Online vs Air-gapped

The only difference between the two paths is where the artifacts originate. This table summarizes the choices made at each step.

Task

Online

Air-gapped

px-fuse module image

git clone source, kmm-builder & trixie-slim on-the-fly image modifications

bundled source, kmm-builder (autoconf) & trixie-slim (kmod) pre-built externally in registry.local:9001

Helm chart source

helm repo add portworx/portworx

oci://registry.local:9001/helm-charts/portworx

Portworx container images

Pulled from public registries

Pre-loaded into registry.local:9001 + customRegistryURL override

Extra objects

None

px-versions ConfigMap + default-registry-key secret

Internet required at deploy time

Yes

No

Troubleshooting

For troubleshooting Portworx issues on StarlingX, refer to the following Portworx documentation listed below:

General troubleshooting

Quick diagnostic commands

Use the following commands to check Portworx status on your StarlingX cluster. Each cmd runs inside a portworx pod.

# Check Portworx cluster status
PX_POD=$(kubectl get pods -n portworx -l name=portworx -o jsonpath='{.items[0].metadata.name}')
kubectl exec -it -n portworx $PX_POD -- /opt/pwx/bin/pxctl status

# Check Portworx alerts
kubectl exec -it -n portworx $PX_POD -- /opt/pwx/bin/pxctl alerts show

# Check KVDB cluster health
kubectl exec -it -n portworx $PX_POD -- /opt/pwx/bin/pxctl service kvdb members

# Collect diagnostics on all nodes
kubectl exec -it -n portworx $PX_POD -- /opt/pwx/bin/pxctl service diags -a

# Check Portworx pods status
kubectl get pods -n portworx -l name=portworx -o wide