Purpose: For platform engineers and developers, shows how to stand up a local openCenter cluster on Kind with a disposable Gitea instance and FluxCD.
Prerequisites
-
Podman or Docker running
-
kind,fluxinstalled -
8 GB RAM minimum
-
This repository checked out locally
Install the CLI and the opencenter-local plugin:
mise run local-install
Verify:
opencenter version
opencenter local --help
kind --version
flux --version
Bootstrap Flow
The sequence diagram below shows the internal calls made by opencenter cluster deploy for a Kind cluster. Podman binds the Gitea container port on 0.0.0.0, so the host’s routable IP (e.g. 172.16.0.146:3001) is reachable from both the macOS host and from inside the Kind cluster. During gitea-attach-kind, the TLS certificate is regenerated with the host IP as a SAN, and all subsequent operations use this single URL.
sequenceDiagram
autonumber
participant User as User (host)
participant CLI as opencenter CLI
participant BS as BootstrapService
participant KBP as KindBootstrapProvider
participant Kind as kind CLI
participant Gitea as Gitea Container
participant GitOps as GitOps Service
participant Flux as flux CLI
participant K8s as Kind Cluster (K8s API)
User->>CLI: opencenter cluster deploy my-cluster --container-runtime podman
CLI->>BS: Bootstrap(ctx, opts)
BS->>BS: loadConfig, resolvePaths, acquireLock
BS->>KBP: BuildSteps(cfg, clusterPaths, opts)
KBP-->>BS: []bootstrapStep (6 steps)
Note over BS: Step execution loop (resumable via state file)
rect rgb(230, 245, 255)
Note over BS,Kind: Step 1: kind-create
BS->>Kind: kind create cluster using kind-config.yaml
Kind->>Kind: Pull kindest/node image, create containers
Kind-->>BS: cluster ready (control-plane + workers)
end
rect rgb(230, 245, 255)
Note over BS,Kind: Step 2: kind-export-kubeconfig
BS->>Kind: kind export kubeconfig --name my-cluster
Kind-->>BS: kubeconfig written to infrastructure/clusters/my-cluster/kubeconfig.yaml
end
rect rgb(255, 243, 224)
Note over BS,Gitea: Step 3: gitea-attach-kind
BS->>Gitea: gitea.NewService → Status(ctx)
Gitea-->>BS: Running: true
BS->>Gitea: AttachKind(ctx)
Gitea->>Gitea: podman network connect kind gitea
Gitea->>Gitea: kindIP(ctx) → e.g. 10.89.0.38
Gitea->>Gitea: hostRoutableIP() → e.g. 172.16.0.146
Gitea->>Gitea: writeCertificates([10.89.0.38, 172.16.0.146])<br/>SANs: localhost, gitea, 127.0.0.1, ::1, 10.89.0.38, 172.16.0.146
Gitea->>Gitea: podman restart gitea
Gitea->>Gitea: waitForAPI(ctx)
Note over Gitea: Verify IP stable after restart<br/>(re-cert + restart if IP changed)
Gitea-->>BS: AttachResult{HostIP: 172.16.0.146, HostRepoURL: https://172.16.0.146:3001/...}
end
rect rgb(232, 245, 233)
Note over BS,K8s: Step 4: flux-bootstrap
BS->>Flux: flux.NewService → Bootstrap(ctx, "local/my-cluster")
Flux->>Gitea: Status(ctx)
Gitea-->>Flux: HostRepoURL: https://172.16.0.146:3001/..., UserToken: present
Note over Flux: Single URL for both host and cluster:<br/>repoURL = https://172.16.0.146:3001/...
Flux->>Flux: flux bootstrap git --url=repoURL --branch=main --path=applications/overlays/my-cluster --token-auth --ca-file=ca.pem
Note over Flux,K8s: flux bootstrap git internally:
Flux->>Gitea: 1. Clone repo via 172.16.0.146:3001 (host → Gitea ✅)
Flux->>Flux: 2. Generate gotk-components.yaml, gotk-sync.yaml
Flux->>Gitea: 3. Commit & push manifests via 172.16.0.146:3001 ✅
Flux->>K8s: 4. kubectl apply gotk-components (installs Flux controllers)
Flux->>K8s: 5. kubectl apply gotk-sync (creates GitRepository CR with url=172.16.0.146:3001)
K8s->>Gitea: 6. source-controller clones https://172.16.0.146:3001/... ✅
Note over K8s,Gitea: ✅ Works: Podman binds on 0.0.0.0<br/>so the host IP is reachable from<br/>inside the Kind cluster via the<br/>Podman VM bridge.
Flux-->>Flux: ✅ health check passes (GitRepository ready)
Flux-->>BS: BootstrapResult{RepoURL: https://172.16.0.146:3001/...}
end
rect rgb(245, 235, 255)
Note over BS,Gitea: Step 5: gitea-rebase
BS->>GitOps: PullRebase(ctx, gitDir)
GitOps->>Gitea: Status(ctx)
Gitea-->>GitOps: CAPath, UserToken
GitOps->>GitOps: git pull --rebase origin main (via localhost:3001, token auth, CA cert)
Gitea-->>GitOps: Flux bootstrap commits rebased into local checkout
GitOps-->>BS: branch: main
end
rect rgb(232, 245, 233)
Note over BS,Gitea: Step 6: gitops-push
BS->>GitOps: Push(ctx, "local/my-cluster")
GitOps->>GitOps: resolve cluster → git_dir
GitOps->>Gitea: Status(ctx)
Gitea-->>GitOps: LocalRepoURL: https://localhost:3001/newuser/test-repo.git
GitOps->>GitOps: git remote set-url origin https://localhost:3001/...
GitOps->>Gitea: git push -u origin main (via localhost:3001, token auth, CA cert)
Gitea-->>GitOps: push accepted
GitOps-->>BS: PushResult{RemoteURL: localhost:3001, Branch: main}
end
BS-->>CLI: ✅ bootstrap complete
CLI-->>User: Bootstrap successful
The host’s routable IP (172.16.0.146) works from both contexts because Podman binds the Gitea container port on 0.0.0.0. The TLS certificate includes this IP as a SAN (added during gitea-attach-kind), so HTTPS verification succeeds everywhere. No post-bootstrap patching is needed.
Steps
1. Start Local Gitea
opencenter local gitea up
Starts a disposable Gitea container (docker.gitea.com/gitea:1.24.5) and provisions a test user, API tokens, and a test-repo repository. State is written to $OPENCENTER_CONFIG_DIR/local/ (typically ~/.config/opencenter/local/).
Verify:
opencenter local gitea status
You should see Running: true and a repository URL at https://localhost:3001/newuser/test-repo.git.
2. Initialize the Cluster Configuration
opencenter cluster init my-cluster --org local --type kind
Creates a Kind cluster config under the local organization directory. Generates SOPS Age keys and an SSH key pair automatically.
Confirm the resolved paths:
opencenter cluster describe my-cluster
4. Set the Git Remote and Token Provider
GITEA_REPO_URL=$(opencenter local gitea status 2>/dev/null | grep "Bootstrap repo URL:" | awk '{print $NF}')
GITEA_TOKEN_PATH=$(opencenter local gitea status 2>/dev/null | grep "User token present:" | sed 's/.*(\(.*\))/\1/')
opencenter cluster set my-cluster \
opencenter.gitops.git_url="$GITEA_REPO_URL" \
opencenter.gitops.git_token="$GITEA_TOKEN_PATH" \
opencenter.gitops.git_token_provider=gitea
Points the cluster configuration at the local Gitea repository using the host-routable IP (e.g. https://172.16.0.146:3001/…;). This IP is reachable from both the macOS host and from inside the Kind cluster because Podman binds on 0.0.0.0. The git_token field is set to the path of the Gitea user token file, and git_token_provider tells bootstrap how to read it.
Verify:
opencenter cluster describe my-cluster | grep git_url
5. Generate the GitOps Tree
opencenter cluster generate my-cluster --force
Produces the overlay tree under the cluster’s git_dir: applications/overlays/my-cluster/, infrastructure/clusters/my-cluster/, and secrets/. The flux-system/ directory does not exist yet; it is created later during bootstrap by flux bootstrap git.
The templates use the git_url from the configuration, so this step must run after setting the Git remote in step 4.
6. Bootstrap the Cluster
opencenter cluster deploy my-cluster --container-runtime podman
Substitute docker if that is your runtime. This command runs the full Kind bootstrap sequence:
Before starting, the command checks whether the GitOps directory has uncommitted changes. If it does, changes are auto-committed with the message chore: auto-commit before bootstrap. The gitea-rebase step runs git pull --rebase, which requires a clean working tree. Use --confirm-commit to prompt for confirmation before auto-committing.
-
kind-create— Creates the Kind cluster using the generatedkind-config.yaml -
kind-export-kubeconfig— Exports the kubeconfig to the cluster’s infrastructure directory -
gitea-attach-kind— Connects the local Gitea container to the Kind network and reissues the TLS certificate with the in-cluster IP -
flux-bootstrap— Runsflux bootstrap gitagainst the in-cluster Gitea URL -
gitea-rebase— Rebases the local checkout to include the Flux bootstrap commits from Gitea -
gitops-push— Pushes the generated GitOps repository to the local Gitea instance
The command is resumable. If a step fails, fix the issue and re-run. Use --restart to re-run all steps from scratch, or --from-step <id> to resume from a specific step (e.g., --from-step gitea-attach-kind).
Verification
GITOPS_DIR=$(opencenter cluster describe my-cluster 2>/dev/null | grep "git_dir:" | awk '{print $2}')
export KUBECONFIG="$GITOPS_DIR/infrastructure/clusters/my-cluster/kubeconfig.yaml"
kubectl get nodes
kubectl get pods -n flux-system
flux get sources git -n flux-system
flux get kustomizations -n flux-system
All Kind nodes should be Ready, Flux pods running in flux-system, and the flux-system Git source READY=True.
Cleanup
Destroy the cluster
opencenter cluster destroy my-cluster --force
This runs kind delete cluster, removes the GitOps directory, the cluster configuration file, and the infrastructure and applications directories. It also clears the active cluster marker if my-cluster was selected.
The command prompts for confirmation unless --force is passed.
Destroy the local Gitea instance
opencenter local gitea destroy
Stops the Gitea container and removes the local state directory ($OPENCENTER_CONFIG_DIR/local/ — metadata, tokens, certificates, mounted data). This is separate from cluster destroy because a single Gitea instance can serve multiple local clusters.
Troubleshooting
kindest/node Image Not Found
The selected Kubernetes version does not have a published kindest/node image. Pick a valid tag, update the config, then rerun:
opencenter cluster generate my-cluster --force
opencenter cluster deploy my-cluster --container-runtime podman --restart
Bootstrap Fails at gitea-attach-kind
Gitea must be running before bootstrap. Verify with opencenter local gitea status. If it is not running, start it with opencenter local gitea up and re-run bootstrap.
Bootstrap Fails at gitops-push With "rejected (fetch first)"
The remote Gitea repository already contains content from a previous run. Either destroy and recreate Gitea (opencenter local gitea destroy && opencenter local gitea up) or restart bootstrap from scratch:
opencenter cluster deploy my-cluster --container-runtime podman --restart
Bootstrap Fails at flux-bootstrap With Connection Timeout
The Flux source-controller inside the cluster cannot reach Gitea at the host IP. Verify Gitea is attached to the Kind network and the host IP is set:
opencenter local gitea status
Check that Kind Attached: true, Host IP is set, and Bootstrap repo URL shows the host IP (not localhost). If not, re-run bootstrap from the attach step:
opencenter cluster deploy my-cluster --container-runtime podman --from-step gitea-attach-kind
If the host IP changed (e.g. after switching networks), destroy and recreate Gitea to regenerate the TLS certificate:
opencenter local gitea destroy
opencenter local gitea up
opencenter cluster deploy my-cluster --container-runtime podman --from-step gitea-attach-kind
Stale Bootstrap Lock
If bootstrap was interrupted:
rm -f ~/.local/state/opencenter/locks/my-cluster.lock
opencenter cluster deploy my-cluster --container-runtime podman --restart
Re-rendering Configuration After Changes
If you update the cluster configuration after running cluster generate (e.g., changing git_url, service settings, or other template variables), you need to re-render the templates and push the changes to Git:
# Re-render all templates (creates backups of existing files)
opencenter cluster generate my-cluster --render-only --force
# Commit the changes
GITOPS_DIR=$(opencenter cluster describe my-cluster | grep git_dir | awk '{print $2}')
cd "$GITOPS_DIR"
git add -A && git commit -m "chore: re-render templates after config update"
# Push to Gitea
opencenter local gitops push --cluster my-cluster
# Trigger Flux to pull the changes immediately (optional - Flux polls every 15m by default)
flux reconcile source git flux-system -n flux-system
Selective service or infrastructure-only rendering is not exposed in the GA CLI. Re-run the render-only flow after changing service or infrastructure settings:
opencenter cluster generate my-cluster --render-only --force
Services Stuck in "pending" or "unknown" Status
If opencenter cluster describe shows services with status: unknown or status: pending, sync the live cluster state:
# Preview what would change
opencenter cluster status --sync my-cluster --dry-run
# Sync and save to config
opencenter cluster status --sync my-cluster
GitRepository Resources Failing With Authentication Errors
If Flux GitRepository resources show errors like failed to configure authentication options or reference placeholder URLs (ssh://git@example.com/…), the templates were rendered before git_url was set correctly.
Fix by re-rendering and pushing:
# Verify git_url is correct in config
opencenter cluster describe my-cluster | grep git_url
# Re-render templates
opencenter cluster generate my-cluster --render-only --force
# Commit and push
GITOPS_DIR=$(opencenter cluster describe my-cluster | grep git_dir | awk '{print $2}')
cd "$GITOPS_DIR"
git add -A && git commit -m "fix: update git_url in GitRepository manifests"
opencenter local gitops push --cluster my-cluster
# Reconcile Flux
flux reconcile source git flux-system -n flux-system
flux reconcile kustomization sources -n flux-system
Evidence
-
Kind bootstrap provider (step definitions):
internal/cluster/kind_bootstrap_provider.go -
Bootstrap service (step execution engine):
internal/cluster/bootstrap_service.go -
Flux bootstrap logic:
internal/localdev/flux/service.go -
GitOps push logic:
internal/localdev/gitops/service.go -
Gitea service:
internal/localdev/gitea/service.go -
Kind provider (create/delete/kubeconfig):
internal/cloud/kind/provider.go -
Kind defaults:
internal/config/defaults/kind.yaml -
Renderer vs bootstrap ownership:
docs/dev/rendering-contract.md