Build from a Private Repository and Deploy via a Private Registry
Overviewβ
This tutorial covers a fully private end-to-end pipeline: your source code lives in a private Git repository, the container image is built inside OpenChoreo's Workflow Plane, pushed to a private container registry, and then pulled and deployed on the Data Plane.
Each stage of the pipeline requires its own credentials:
| Stage | Credential Required |
|---|---|
| Clone private Git repo | Git personal access token (PAT) |
| Push image to private registry | Registry push credentials |
| Pull image from private registry | Registry pull credentials (image pull secret) |
This tutorial uses GitHub as the Git host and Docker Hub as the container registry.
Prerequisitesβ
Before you begin, ensure you have:
- OpenChoreo installed in your Kubernetes cluster
- kubectl configured to access your cluster
- A private GitHub repository containing your application source code.
- A private Docker Hub repository to push and pull your built image
- A Docker Hub personal access token (or password) for both push (Workflow Plane) and pull (Data Plane)
- A GitHub personal access token with at least
reporead access - External secret store accessible from the Workflow Plane and Data Plane (each plane may have its own store, or they may share one β e.g., OpenBao for local dev, or Vault/AWS Secrets Manager for production)
Step 1 β Store Git Credentialsβ
Store your Git credentials in your secret backend so the Workflow Plane can clone the private repository.
This example uses OpenBao, the default secret store in a local OpenChoreo development setup. For production, see Secret Management.
Store a username and personal access token (PAT) under the key secret/git/github-token:
kubectl exec -n openbao openbao-0 -- sh -c '
export BAO_ADDR=http://127.0.0.1:8200 BAO_TOKEN=root
bao kv put secret/git/github-token \
username="your-github-username" \
token="your-personal-access-token"
'
Replace your-github-username and your-personal-access-token with your actual Git credentials. The token must have at least read access to the repository.
Step 2 β Create a SecretReference for the Git Repositoryβ
Create a SecretReference custom resource that syncs your Git credentials from the secret store into the Workflow Plane namespace.
kubectl apply -f - <<EOF
apiVersion: openchoreo.dev/v1alpha1
kind: SecretReference
metadata:
name: github-credentials
namespace: default
spec:
template:
type: kubernetes.io/basic-auth
data:
- secretKey: username
remoteRef:
key: secret/git/github-token
property: username
- secretKey: password
remoteRef:
key: secret/git/github-token
property: token
refreshInterval: 1h
EOF
Step 3 β Store Registry Push Credentialsβ
Store credentials that allow the Workflow Plane to push the built image to your private registry.
Generate base64-encoded credentialsβ
echo -n 'your-registry-username:your-registry-password' | base64
Example output: ZGVtby11c2VyOmRlbW8tcGFzcw==
Store in OpenBaoβ
kubectl exec -n openbao openbao-0 -- sh -c '
export BAO_ADDR=http://127.0.0.1:8200 BAO_TOKEN=root
bao kv put secret/registry-push-secret \
value="{\"auths\":{\"<REGISTRY-HOST>\":{\"auth\":\"<BASE64-TOKEN>\"}}}"
'
Replace:
<REGISTRY-HOST>β your registry host (e.g.,https://index.docker.io/v1/for Docker Hub,ghcr.iofor GHCR)<BASE64-TOKEN>β the base64 string from the previous step
Example for Docker Hub:
kubectl exec -n openbao openbao-0 -- sh -c '
export BAO_ADDR=http://127.0.0.1:8200 BAO_TOKEN=root
bao kv put secret/registry-push-secret \
value="{\"auths\":{\"https://index.docker.io/v1/\":{\"auth\":\"ZGVtby11c2VyOmRlbW8tcGFzcw==\"}}}"
'
Step 4 β Configure the Publish Step to Use Your Private Registryβ
This step is a one-time platform-level configuration performed by a cluster admin. You do not need to repeat this for every application.
Replace the default publish-image ClusterWorkflowTemplate with one pointing to your private registry endpoint. Before applying, determine your REGISTRY_ENDPOINT value:
| Registry | REGISTRY_ENDPOINT |
|---|---|
| Docker Hub | docker.io/your-username |
| Amazon ECR | 123456789.dkr.ecr.us-east-1.amazonaws.com/openchoreo-builds |
| Google Artifact Registry | us-central1-docker.pkg.dev/my-project/openchoreo-builds |
| Azure Container Registry | myregistry.azurecr.io |
| GitHub Container Registry | ghcr.io/my-org/openchoreo |
Then apply, replacing your-registry-host/your-repo-path with your value from the table above:
kubectl apply -f - <<EOF
apiVersion: argoproj.io/v1alpha1
kind: ClusterWorkflowTemplate
metadata:
name: publish-image
spec:
templates:
- name: publish-image
inputs:
parameters:
- name: git-revision
outputs:
parameters:
- name: image
valueFrom:
path: /tmp/image.txt
volumes:
- name: registry-push-secret
secret:
optional: true
secretName: '{{workflow.parameters.registry-push-secret}}'
container:
image: ghcr.io/openchoreo/podman-runner:v1.0
command: [sh, -c]
args:
- |-
set -e
GIT_REVISION={{inputs.parameters.git-revision}}
IMAGE_NAME={{workflow.parameters.image-name}}
IMAGE_TAG={{workflow.parameters.image-tag}}
SRC_IMAGE="\${IMAGE_NAME}:\${IMAGE_TAG}-\${GIT_REVISION}"
# Replace with your registry endpoint
REGISTRY_ENDPOINT="your-registry-host/your-repo-path"
AUTH_FILE="/etc/secrets/registry-push-secret/.dockerconfigjson"
mkdir -p /etc/containers
cat <<CONF > /etc/containers/storage.conf
[storage]
driver = "overlay"
runroot = "/run/containers/storage"
graphroot = "/var/lib/containers/storage"
[storage.options.overlay]
mount_program = "/usr/bin/fuse-overlayfs"
CONF
podman load -i /mnt/vol/app-image.tar
podman tag \$SRC_IMAGE \$REGISTRY_ENDPOINT/\$SRC_IMAGE
if [ -f "\$AUTH_FILE" ]; then
podman push --tls-verify=true --authfile "\$AUTH_FILE" \$REGISTRY_ENDPOINT/\$SRC_IMAGE
else
podman push --tls-verify=true \$REGISTRY_ENDPOINT/\$SRC_IMAGE
fi
echo -n "\$REGISTRY_ENDPOINT/\$SRC_IMAGE" > /tmp/image.txt
securityContext:
privileged: true
volumeMounts:
- mountPath: /mnt/vol
name: workspace
- mountPath: /etc/secrets/registry-push-secret
name: registry-push-secret
readOnly: true
EOF
Step 5 β Store Registry Pull Credentialsβ
The Data Plane also needs credentials to pull the image from your private registry. Store them in OpenBao with the key registry-credentials:
kubectl exec -n openbao openbao-0 -- sh -c '
export BAO_ADDR=http://127.0.0.1:8200 BAO_TOKEN=root
bao kv put secret/registry-credentials \
value="{\"auths\":{\"<REGISTRY-HOST>\":{\"auth\":\"<BASE64-TOKEN>\"}}}"
'
If your push and pull credentials are the same (e.g., same Docker Hub account), you can reuse the same base64 token. The separate keys (registry-push-secret and registry-credentials) exist because push credentials are used by the Workflow Plane and pull credentials are used by the Data Plane, which may be separate clusters.
Step 6 β Configure Your ClusterComponentType to Pull from the Private Registryβ
This is another one-time platform-level change performed by a cluster admin. Once configured, all components using this ClusterComponentType will automatically get the image pull secret injected β no per-component changes are needed.
Edit the deployment/service ClusterComponentType (or your custom type) to add two pieces:
1. An ExternalSecret that syncs pull credentials into the workload namespace:
- id: registry-pull-secret
template:
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: registry-pull-secret
namespace: ${metadata.namespace}
spec:
refreshInterval: 15s
secretStoreRef:
name: ${dataplane.secretStore}
kind: ClusterSecretStore
target:
name: registry-pull-secret
creationPolicy: Owner
template:
type: kubernetes.io/dockerconfigjson
data:
- secretKey: .dockerconfigjson
remoteRef:
key: registry-credentials
property: value
2. imagePullSecrets in the Deployment template so pods can authenticate when pulling:
- id: deployment
template:
apiVersion: apps/v1
kind: Deployment
metadata:
name: ${metadata.name}
namespace: ${metadata.namespace}
spec:
template:
spec:
imagePullSecrets:
- name: registry-pull-secret
containers:
- name: main
image: ${workload.container.image}
# ... rest of container config
For the full ClusterComponentType configuration, see Deploy from a Private Registry.
Step 7 β Create the Componentβ
Create the Component that points to your private Git repository:
kubectl apply -f - <<EOF
apiVersion: openchoreo.dev/v1alpha1
kind: Component
metadata:
name: private-go-service
namespace: default
spec:
autoDeploy: true
componentType:
kind: ClusterComponentType
name: deployment/service
owner:
projectName: default
workflow:
kind: ClusterWorkflow
name: dockerfile-builder
parameters:
repository:
url: https://github.com/openchoreo/sample-workloads.git # Replace with your private repo URL
secretRef: github-credentials
revision:
branch: main # Replace with your branch
appPath: /service-go-greeter # Replace with the path to your app within the repo
docker:
context: /service-go-greeter # Replace with your Docker build context
filePath: /service-go-greeter/Dockerfile # Replace with your Dockerfile path
EOF
Replace url with your private repository's clone URL, branch with your target branch, appPath with the path to your application within the repository, and context and filePath with the Docker build context and Dockerfile path relative to the repository root.
Step 8 β Trigger a Buildβ
To build the image, create a WorkflowRun that references the Component:
kubectl apply -f - <<EOF
apiVersion: openchoreo.dev/v1alpha1
kind: WorkflowRun
metadata:
name: private-go-service-run-01
namespace: default
labels:
openchoreo.dev/component: private-go-service
openchoreo.dev/project: default
spec:
workflow:
kind: ClusterWorkflow
name: dockerfile-builder
parameters:
repository:
url: https://github.com/openchoreo/sample-workloads.git # Replace with your private repo URL
secretRef: github-credentials
revision:
branch: main # Replace with your branch
appPath: /service-go-greeter # Replace with the path to your app within the repo
docker:
context: /service-go-greeter # Replace with your Docker build context
filePath: /service-go-greeter/Dockerfile # Replace with your Dockerfile path
EOF
Replace url, branch, appPath, context, and filePath with the values matching your private repository.
Monitor the Buildβ
# Check WorkflowRun conditions
kubectl get workflowrun private-go-service-run-01 -n default \
-o jsonpath='{.status.conditions}' | jq .
# Watch workflow pods in the Workflow Plane namespace
kubectl get pods -n workflows-default | grep private-go-service
# Stream logs from the workflow pod (replace <pod-name> with the actual pod name)
kubectl logs -n workflows-default <pod-name> -f
The WorkflowRun goes through these stages:
| Condition | Description |
|---|---|
WorkflowRunning | Argo Workflow is executing (clone β build β push) |
WorkflowSucceeded | Image pushed to registry; Workload CR created |
WorkflowFailed | Check step logs in the Workflow Plane |
Wait for both WorkflowCompleted: True and WorkflowSucceeded: True before proceeding.
Step 9 β Verify the Deploymentβ
Once the WorkflowRun succeeds, OpenChoreo automatically creates a Workload CR with the pushed image reference and deploys it on the Data Plane.
Check the component and workload:
kubectl get component private-go-service
kubectl get workload -A | grep private-go-service
Check that pods are running:
kubectl get pods -A | grep private-go-service
If pods are in ImagePullBackOff, verify that the pull secret is synced correctly:
kubectl get externalsecret -A | grep registry-pull-secret
kubectl describe externalsecret registry-pull-secret -n <workload-namespace>
Test Your Applicationβ
Once the deployment is ready, test the service:
curl http://development-default.openchoreoapis.localhost:19080/private-go-service-greeter-api/greeter/greet
# Expected response: Hello, Stranger!
Troubleshootingβ
| Symptom | Likely Cause | Fix |
|---|---|---|
WorkflowRun stuck in Running with clone step failing | Invalid Git credentials or secret not found | Check SecretReference status and verify credentials in secret store |
WorkflowRun fails at publish-image step with "unauthorized" | Push credentials missing or wrong registry host | Re-verify registry-push-secret in OpenBao and the REGISTRY_ENDPOINT in the CWT |
Pod in ImagePullBackOff after successful build | Pull secret not configured or synced | Verify ExternalSecret status and imagePullSecrets in ClusterComponentType |
| "x509: certificate signed by unknown authority" | Self-signed registry TLS cert | Set --tls-verify=false in the CWT or configure the CA cert |
Summaryβ
You have set up a fully private end-to-end pipeline in OpenChoreo:
- Source code is cloned from a private Git repository using a
SecretReference - The image is built in the Workflow Plane and pushed to a private registry using a push secret
- The Data Plane pulls the image using an
ExternalSecret-backed image pull secret and deploys it
Next Stepsβ
- Set up Secret Management for production-grade secret backends and automatic rotation
- Container Registry Configuration β Configure push credentials and registry providers in detail
- Private Git Repositories β More on Git authentication methods (SSH, basic auth)
- Workload Generation β How build outputs automatically become Workload CRs