Skip to main content
Version: 0.7 (Next)

Civo Bootstrap Installation

Deploy Konstruct on a Civo Kubernetes cluster with a self-hosted GitLab instance as the git provider. Three steps: create a Civo cluster, create a GitLab OAuth App, then run Terraform.

Support matrix

In the 0.7 release line, the Civo self-hosted control plane supports GitLab (self-hosted or SaaS) and GitHub as git providers, with Civo DNS or Cloudflare as the DNS provider. The terraform roots are organized as a provider matrix — this guide follows konstruct-bootstrap/civo-gitlab; swap in civo-github for the GitHub flow.

Prerequisites

  • A Civo account and API key (get one here)
  • civo CLI (install guide)
  • Terraform 1.5+
  • kubectl v1.30+
  • A domain you control, managed either as a Civo DNS domain in the same Civo account as the cluster or as a Cloudflare zone
  • A GitLab instance reachable from the cluster, and a user with Owner access to a group

Step 0: Create a Civo Kubernetes Cluster

If you already have a cluster, skip to Step 1.

civo apikey save konstruct <YOUR_API_KEY>
civo apikey current konstruct
civo region use NYC1

civo kubernetes create konstruct-mgmt \
--region NYC1 \
--nodes 3 \
--size g4p.kube.large \
--version 1.35.0-k3s1 \
--remove-applications "Traefik-v2-nodeport" \
--create-firewall \
--firewall-rules "80;443;6443" \
--wait

Traefik is removed because Envoy Gateway is the platform ingress — leaving it installed wastes a NodePort and a load-balancing hop. Civo clusters provision fast — usually within a few minutes.

Cluster requirements:

  • Minimum 3x g4s.kube.large nodes (the platform deploys ESO, cert-manager, Envoy Gateway, external-dns, Dex, Vault in HA, and the Konstruct platform with its operators); pin a current k3s --version so your nodes match the release line your team validates against
  • Firewall must allow 80 and 443 (Let's Encrypt HTTP-01 challenges and platform traffic) plus 6443 (Kubernetes API)
  • No special CNI or storage requirements — k3s defaults work
Do not undersize the nodes

On k3s the control plane shares the nodes with your workloads. On g4s.kube.medium nodes the full platform runs at 85–105% node memory: the API server becomes intermittently unreachable and operators restart continuously. g4s.kube.large is the working minimum; use a performance tier (for example g4p.kube.large) when the platform also builds and serves platform theme workloads.

Verify nodes:

civo kubernetes config konstruct-mgmt --region NYC1 > ./konstruct-mgmt.kubeconfig
export KUBECONFIG=$PWD/konstruct-mgmt.kubeconfig
kubectl get nodes
# Expect 3 nodes in Ready state

Step 1: Create a GitLab OAuth App

Konstruct uses a GitLab OAuth application on your self-hosted instance for user authorization and repository management.

Create a group

If you don't already have one, create the GitLab group that will own your GitOps repositories (for example, platform). Your GitLab user needs Owner access to it.

Create the OAuth application

  1. In your GitLab instance, go to one of:
    • Instance-level: Admin Area > Applications > New application
    • Group-level: Your group > Settings > Applications > Add new application
  2. Configure:
    • Name: konstruct-<your-domain> (e.g., konstruct-example-com)

    • Redirect URI:

      https://konstruct.<your-domain>/api/v1/gitlab/callback
    • Scopes: check api, read_repository, write_repository

    • Leave Confidential checked

  3. Click Save application
  4. Note the Application ID and Secret — you will need both for terraform.tfvars

GitLab OAuth application scopes

caution

The redirect URI must exactly match https://konstruct.<your-domain>/api/v1/gitlab/callback. A trailing slash or wrong path will cause OAuth to fail silently. (The /api/v1/auth/callback path used elsewhere in these docs belongs to the Dex OIDC client, not the GitLab application.)

What the scopes grant

ScopePurpose
apiCreate and manage repositories, webhooks, and group members
read_repositoryClone and read GitOps repository contents
write_repositoryPush GitOps manifests during cluster provisioning

Step 2: Choose a DNS Provider

Konstruct's external-dns manages DNS records for the platform hostnames (konstruct.<domain>, dex.<domain>, vault.<domain>).

Register the domain in the same Civo account that runs the cluster — external-dns reuses the civo_token terraform already has, so there is no extra credential to mint:

civo domain create <your-domain>   # a 409 duplicate means it's registered in ANOTHER Civo account

Point your registrar's nameservers at ns0.civo.com and ns1.civo.com, and set dns_provider = "civo" in terraform.tfvars (Step 3). No Cloudflare token is needed.

note

Two Civo DNS caveats: the dns_provider flag is landing in the konstruct-bootstrap/civo-* roots via konstruct-aws#7 (until it merges, Civo DNS installs use the hotfix-streamline lineage), and the wildcard apps certificate (apps-wildcard-tls, used for platform theme app hostnames) requires DNS-01 and stays Pending on Civo DNS until a Civo DNS-01 solver ships — platform hostnames (konstruct., dex., vault., harbor.) all issue normally via HTTP-01.

Option B: Cloudflare

Your zone lives on Cloudflare and external-dns writes records there via an API token:

  1. Go to Cloudflare API Tokens and click Create Token
  2. Use the Edit zone DNS template
  3. Configure:
    • Permissions: Zone / DNS / Edit, plus add Zone / Zone / Read
    • Zone Resources: Include / Specific zone / <your-domain>
  4. Click Continue to summary, then Create Token, and copy the token value (shown once)

Verify the token before using it:

curl -s -H "Authorization: Bearer <YOUR_TOKEN>" \
"https://api.cloudflare.com/client/v4/zones?name=<your-domain>" | jq '.success'
# Expect: true

Step 3: Run Terraform

Terraform installs the platform services and the Konstruct chart onto your cluster, and creates a Civo Object Store for platform state.

git clone https://github.com/konstructio/konstruct-aws.git
cd konstruct-aws/konstruct-bootstrap/civo-gitlab

The repository is organized as a provider matrix under konstruct-bootstrap/civo-gitlab, civo-github, aws-gitlab, aws-github — each a thin root over the shared modules/bootstrap-* modules. Pick the root matching your cloud and git provider; a terraform.tfvars.example in each root is the starting point.

Generate a bcrypt password hash

The dex_config.admin_password field requires a bcrypt hash of your desired login password:

htpasswd -nbBC 10 "" 'your-password' | tr -d ':\n' | sed 's/$2y/$2a/'

Copy the output (starts with $2a$10$...). Remember the plain text password — you'll use it to login to Konstruct as the kbot user.

warning

Use a simple alphanumeric password. Special characters like ! can break shell quoting.

Generate a shared OIDC secret

The oidc_client_secret in konstruct_api_config must match the secret in dex_config.static_clients[0]:

openssl rand -hex 32

Configure terraform.tfvars

terraform.tfvars
cluster_name     = "konstruct-mgmt"
civo_region = "NYC1"
civo_token = "<YOUR_CIVO_API_KEY>"
domain = "your-domain.com"
alerts_email = "alerts@your-domain.com"
gitops_repo_name = "platform-gitops"

konstruct_chart_version = "0.7.0" # pin the specific published version your team validated — see note below

# DNS provider from Step 2: "civo" reuses civo_token (leave cloudflare_api_token
# unset); "cloudflare" requires the token from Step 2 Option B.
dns_provider = "civo"
cloudflare_api_token = "" # only for dns_provider = "cloudflare"

# Pins the konstruct-templates ref for the default cluster template catalog and
# bootstrap hydration. Leave at "main" unless you are validating a templates branch.
default_templates_ref = "main"

# Publishes the wildcard apps listener (https-apps) and the Harbor listener on
# the Gateway. Required for platform theme app delivery; harmless
# to enable on day one and saves a Gateway change later.
enable_foreman_gateway = true

gitlab_config = {
owner = "platform" # Group from Step 1
base_url = "https://gitlab.your-company.com" # Your self-hosted GitLab
client_id = "<APPLICATION_ID>" # From Step 1
client_secret = "<SECRET>" # From Step 1
redirect_url = "https://konstruct.your-domain.com/api/v1/gitlab/callback"
}

konstruct_api_config = {
admin_user = "kbot"
client_email = "admin@your-domain.com"
license_validator_url = ""
oidc_client_id = "konstruct"
oidc_client_secret = "<YOUR_SHARED_SECRET>" # From openssl command above
webhook_url = ""
}

dex_config = {
admin_password = "$2a$10$..." # bcrypt hash from htpasswd command above

# Azure AD (optional — leave empty if not using Azure SSO)
azure_client_id = ""
azure_client_secret = ""
azure_tenant_id = ""

static_clients = [
{
id = "konstruct"
name = "Konstruct"
public = false
redirectURIs = [
"https://konstruct.your-domain.com/api/v1/auth/callback"
]
secret = "<YOUR_SHARED_SECRET>" # Must match oidc_client_secret above
}
]
}
Pin a real chart version

konstruct_chart_version becomes the Argo CD targetRevision for the platform itself. Pin the version your team has validated rather than a moving line — release candidates look like 0.7.0-rc.<8-char-sha>. After install you change it in GitOps (registry/clusters/<name>/components/konstruct-system/konstruct.yaml), never in terraform.

Apply

terraform init
terraform plan # Review: expect ~38 resources to add on a fresh install
terraform apply
tip

Open a second terminal and watch pods come up: kubectl get pods -A -w

This creates:

  • A Civo Object Store (<cluster_name>-state-store) and credentials for platform state
  • Namespaces: vault, external-secrets-operator, dex, external-dns, envoy-gateway-system, konstruct-system
  • CRDs: helmtemplates, pipelinetemplates, Gateway API
  • Helm releases: External Secrets Operator, Cert Manager, Envoy Gateway, External DNS (Cloudflare), Vault (HA Raft), Dex, Konstruct
  • Envoy Gateway with HTTPS listeners for konstruct.<domain>, dex.<domain>, vault.<domain>, plus an HTTP-to-HTTPS redirect
  • Vault initialization and configuration jobs (init, unseal, seed secrets)
  • ClusterSecretStore (vault-kv-secret), ClusterIssuer (letsencrypt-prod)
  • The konstruct-initial-state secret that pins your chart_version for Argo CD

Vault initialization adds a few minutes — the vault-handler and vault-config jobs must complete before Dex and Konstruct become fully ready.

Step 4: Verify DNS and TLS

DNS records are created automatically by external-dns in your provider — Civo DNS records land in the same account's domain (make sure your registrar already points at ns0/ns1.civo.com), and Cloudflare zones need no manual delegation since the zone already lives there.

# 1. Gateway has an external address (Civo load balancer)
kubectl get gateway -n default
kubectl get svc -n envoy-gateway-system

# 2. external-dns has written the records
kubectl logs -n external-dns deploy/external-dns | tail -20
dig konstruct.your-domain.com +short

# 3. Certificates are issued (HTTP-01 via the Gateway)
kubectl get certificates -A
kubectl get challenges -A

Certificates stay Pending until DNS resolves to the load balancer; this usually completes within a few minutes on Cloudflare.

Step 5: Login and Onboard

Check readiness:

kubectl get pods -n konstruct-system
kubectl get certificates -A

Once certificates show Ready = True:

  1. Open https://konstruct.<your-domain>
  2. Login with username kbot and the plaintext password you hashed in Step 3

Konstruct login page

Connect GitLab

After login, Konstruct redirects you to the onboarding page: "Connect your Git provider".

Connect your Git provider — choose GitLab

  1. Click Connect with GitLab — a popup opens for OAuth against your self-hosted instance. Make sure you are signed in to GitLab as a user with Owner access to your group.

    GitLab authorization popup on the self-hosted instance

    note

    If you authorize as a GitLab administrator, GitLab warns that the application can interact with GitLab as an administrator. For production installs, prefer a dedicated non-admin user that owns only your platform group.

  2. Click Authorize — the popup closes and Konstruct shows your GitLab groups

  3. Select the group where Konstruct should create and manage repositories (only groups where you have Owner access appear)

    Select the GitLab group for GitOps repositories

Welcome page

After connecting, Konstruct redirects you to the home page with a setup checklist:

Konstruct welcome page with setup checklist

What happens behind the scenes

When you connect GitLab, Konstruct automatically:

  1. Stores the GitLab OAuth connection
  2. Creates a bootstrap Project custom resource
  3. The team-management-operator hydrates a GitOps repository (<group>/<gitops_repo_name>) through the registered control-plane cluster template (cluster-templates/control-plane in konstruct-templates at your default_templates_ref) — the control plane provisions itself the same way every other cluster does
  4. Argo CD is installed and takes over management of all platform components — including the Konstruct chart itself, pinned to the chart_version from your install (inspect with kubectl get applications.argoproj.io -n argocd — the unqualified applications name resolves to Konstruct's own CRD)
  5. A few minutes after Argo CD takes over, the platform ingress moves to the GitOps-managed gateway: a new load balancer is provisioned, external-dns updates all records to point to it, and certificates re-issue — don't pin the initial IP anywhere

From here, create your first organization. Each organization gets its own clusters graph with the control plane rendered at the root, plus Create management cluster and Add theme cluster actions — the management cluster form ships with the built-in Konstruct default template, so no template registration is needed before your first cluster.

Verify

# All pods running
kubectl get pods -n konstruct-system

# After onboarding completes, ArgoCD manages everything
kubectl get pods -n argocd
kubectl get applications -n argocd

# Check the gitops repo was created in your GitLab group

Troubleshooting

Dex CrashLoopBackOff: "malformed bcrypt hash"

kubectl logs -n dex deploy/dex
# error parse config file ... malformed bcrypt hash: hashedSecret too short

The bcrypt hash in dex_config.admin_password was corrupted — most commonly by shell expansion eating the $2a$10$ prefix when the hash passed through an unquoted heredoc or double-quoted string. Fix the value in terraform.tfvars (paste it inside single quotes or via an editor), then re-seed Vault and let the config resync:

terraform apply -replace="module.konstruct-bootstrap-civo.kubernetes_job_v1.vault_config"

Dex recovers on its next restart once External Secrets syncs the corrected value (~10s refresh interval).

Vault jobs stuck

The vault-handler job initializes and unseals Vault; vault-config seeds platform secrets. If pods wait on Vault:

kubectl get jobs -n vault
kubectl logs -n vault job/vault-handler
kubectl get secret -n vault vault-unseal-secret

Certificates stuck in Pending

kubectl describe challenges -A

HTTP-01 challenges flow through the Envoy Gateway on port 80 — confirm your Civo firewall allows 80 and that dig konstruct.<your-domain> resolves to the gateway's load balancer IP.

OAuth fails after login

Verify the GitLab OAuth app's redirect URIs exactly match your domain, and that gitlab_config.base_url has no trailing slash.

What's Next?

  1. Configure cloud accounts
  2. Create your first cluster
  3. Light up the Themes surface — create a theme cluster and serve platform themes (Konduit is the flagship) on the platform you just installed