canette
Self-hosting

Helm reference

Deploy canette to your cluster using the Helm chart.

The canette Helm chart deploys the API, UI, builder, controller, logstreamer, and BuildKit daemon to your cluster. PostgreSQL and an in-cluster image registry are included and enabled by default.

Prerequisites

  • Helm 3.8+
  • Kubernetes 1.27+ with Gateway API CRDs installed
  • A Gateway API implementation (Traefik, Cilium, etc.)
  • A pre-existing Gateway resource — see Installation for the full setup checklist

Install

The chart is published to GitHub Container Registry. Create a values.yaml and install:

helm install canette oci://ghcr.io/canette/canette \
  -n canette-system --create-namespace \
  -f values.yaml

Omitting --version installs the latest release. To pin a specific version:

helm install canette oci://ghcr.io/canette/canette \
  --version 1.2.3 \
  -n canette-system --create-namespace \
  -f values.yaml

Available versions are listed on the canette package page.

The -n flag controls the system namespace. canette always creates a second namespace for build jobs named {system}-build — with the default above that becomes canette-system-build.

Install from source

If you have the repository cloned, you can install directly from the chart directory:

helm install canette charts/canette \
  -n canette-system --create-namespace \
  -f values.yaml

Multiple instances

Because all namespaces are derived from -n, you can run isolated installations side by side:

# Production — apps at {app}-{project}.apps.example.com
helm install canette oci://ghcr.io/canette/canette \
  -n canette-system --create-namespace -f prod-values.yaml

# Staging — apps at {app}-{project}.staging.example.com
helm install canette-staging oci://ghcr.io/canette/canette \
  -n canette-staging --create-namespace -f staging-values.yaml

This creates canette-system + canette-system-build for production, and canette-staging + canette-staging-build for staging — no resource conflicts.

Key values

The sections below cover the most commonly used values. For the full list with descriptions, see charts/canette/values.yaml in the repository.

Required

ValueDescription
domainBase domain for all dynamic hostnames, e.g. apps.example.com. App URLs use {app}-{project}.<domain>; the in-cluster registry is exposed at registry.<domain>.
ui.hostnamePublic hostname for the UI, e.g. canette.example.com

Authentication (optional)

Email and password login is enabled by default — no OAuth app required. To add GitHub or Google OAuth:

ValueDescription
api.githubClientIdGitHub OAuth app client ID
api.githubClientSecretGitHub OAuth app client secret (or use api.githubClientSecretRef)
api.googleClientIdGoogle OAuth client ID
api.googleClientSecretGoogle OAuth client secret (or use api.googleClientSecretRef)

Images (optional)

All service images default to {image.repo}/{serviceName}:{image.tag}, where image.tag defaults to the chart's appVersion. Installing or upgrading the chart automatically uses the matching image versions — no image overrides needed for a standard install.

ValueDefaultDescription
image.repoghcr.io/canetteBase repository for all canette service images
image.tagChart.AppVersionTag applied to all service images. Set to edge to use the latest development build.
api.imagederivedOverride the API image
ui.imagederivedOverride the UI image
builder.imagederivedOverride the builder image
builder.builderImagederivedOverride the Railpack+buildctl build job image
builder.gitInitImagederivedOverride the git-init build job image
controller.imagederivedOverride the controller image
logstreamer.imagederivedOverride the logstreamer image
migrations.enabledfalseSet to true to run migrations as a Helm pre-upgrade Job instead of at API startup

Do not use :edge in production. The edge tag is rebuilt on every commit to main and is not immutable — a pod restart can silently pull a newer image, leaving components running at mismatched versions. Stick with the chart's default versioned tag for any environment where stability matters.

API configuration (optional)

ValueDefaultDescription
api.defaultResources.cpuRequest100mDefault CPU request for every app deployment
api.defaultResources.memoryRequest128MiDefault memory request for every app deployment
api.defaultResources.cpuLimit500mDefault CPU limit for every app deployment
api.defaultResources.memoryLimit512MiDefault memory limit for every app deployment
api.webhookBaseUrl""Public base URL used when constructing webhook URLs registered with git providers. Leave empty to use the UI hostname. Set this if your API is accessible at a different hostname than the UI.
api.disableEmailSignupfalseHard global disable for email sign-up. Overrides the admin UI signup mode setting. Use for deployments where email accounts should be permanently locked at the infrastructure level. See Authentication — Hard disable via Helm.
api.email.provider""Email provider for magic-link login. Supported values: resend. Leave empty to disable magic links.
api.email.resendApiKey""Resend API key. Used when api.email.provider=resend. Use api.email.resendApiKeyRef to reference an existing Secret.
api.email.from"canette <noreply@example.com>"From address for outgoing emails. Must match a verified sending domain in your email provider.

Resource values use standard Kubernetes quantity notation: 100m (millicores), 256Mi (mebibytes), 1 (full core or gigabyte).

These values are snapshotted into each deployment at trigger time. Changing them takes effect on the next deployment — existing live apps are not affected until redeployed.

Infrastructure (optional)

ValueDefaultDescription
postgres.enabledtrueDeploy in-cluster PostgreSQL
postgres.password""Override auto-generated postgres password (leave empty to auto-generate)
externalDatabase.url""External DB connection string when postgres.enabled=false
externalDatabase.ssl.rejectUnauthorizedtrueVerify the database server's TLS certificate. Set to false only for dev/testing — disables MITM protection.
registry.enabledtrueDeploy in-cluster image registry, exposed via HTTPRoute at registry.<domain>
registry.username""Override auto-generated registry username (both fields required to override)
registry.password""Override auto-generated registry password
controller.pullRepo""Registry URL for kubelet image pulls. Auto-derives to registry.<domain>/ when registry.enabled=true. Required when registry.enabled=false.
externalRegistry.username""Username for an external registry (when registry.enabled=false)
externalRegistry.password""Password for an external registry
backup.enabledfalseDeploy a nightly CronJob that backs up PostgreSQL to S3. Requires postgres.enabled=true. See Backups.
backup.schedule0 3 * * *Cron schedule for the backup Job
backup.s3.uri""Destination URI, e.g. s3://my-bucket/canette-backups. Required when backup.enabled=true.
metrics.enabledfalseUnlock CPU/memory-over-time charts on the app Metrics tab. Pod health and instant usage work regardless of this setting. See Metrics.
metrics.prometheus.bundled.enabledfalseDeploy a small in-chart Prometheus (no operator/CRDs) that scrapes kubelet cAdvisor cluster-wide. Ignored when externalUrl is set.
metrics.prometheus.externalUrl""BYO Prometheus-compatible query API (Prometheus, Thanos, Mimir, VictoriaMetrics). Takes precedence over the bundled instance.

Gateway

ValueDefaultDescription
controller.gateway.namecan-gatewayGateway resource name
controller.gateway.namespacekube-systemGateway resource namespace

BuildKit

ValueDefaultDescription
buildkit.rootlesstrueRun buildkitd in rootless mode. Requires user namespace support on build nodes. Set to false on nodes that cannot support user namespaces (e.g. EKS Bottlerocket).
buildkit.imagederivedOverride the buildkitd image. When empty, defaults to moby/buildkit:v0.21.0-rootless when rootless=true, or moby/buildkit:v0.21.0 when rootless=false.
buildkit.registries{}Additional registry trust entries for buildkitd. The in-cluster registry is added automatically. Format: "host:port": { http: true, insecure: true }
buildkit.restrictEgresstrueBlock RFC-1918 egress from the build namespace. Prevents a compromised build from reaching cluster-internal services while allowing internet access for package fetches.

Registry authentication

In-cluster registry

Registry authentication is always enabled and auto-generated on first install. canette generates a random username and password, stores them in the canette-registry-credentials Secret, and reuses them on every helm upgrade — no configuration required.

Build jobs are automatically given a matching docker config Secret; no extra setup needed.

To use your own credentials instead (e.g. for disaster recovery or cluster migration):

registry:
  username: myuser
  password: mysecret

If you set custom credentials after auto-generated ones are already in use, existing push credentials in build jobs will be replaced. Rotate them in one upgrade to avoid authentication failures.

External registry

When registry.enabled=false, provide credentials for the external registry and set controller.pullRepo explicitly:

registry:
  enabled: false
externalRegistry:
  username: myuser
  password: mytoken
builder:
  imageRepo: ghcr.io/myorg/
controller:
  pullRepo: ghcr.io/myorg/

The host for the docker config is derived automatically from builder.imageRepo.

Amazon ECR with IRSA

For ECR on EKS, use IRSA (IAM Roles for Service Accounts) instead of static credentials. ECR repositories are automatically created on first build:

registry:
  enabled: false

builder:
  imageRepo: "123456.dkr.ecr.us-east-1.amazonaws.com/canette/"
  registryAuthType: "irsa"  # or omit to auto-detect from URL
  serviceAccount:
    annotations:
      eks.amazonaws.com/role-arn: arn:aws:iam::123456:role/canette-builder

controller:
  pullRepo: "123456.dkr.ecr.us-east-1.amazonaws.com/canette/"

The registryAuthType value controls authentication:

  • irsa — Use ServiceAccount IAM role (EKS IRSA / GKE Workload Identity)
  • static — Use username/password from externalRegistry values
  • Empty — Auto-detect: irsa for ECR URLs, static otherwise

See AWS (EKS) for IAM role setup.

Image scanning

Scanning is configured under security.scan and is applied at build time by the builder.

security:
  scan:
    provider: "auto"     # "auto" | "trivy" | "ecr" | "none"
    enabled: ""          # "" = provider-aware default; use "true"/"false" (quoted strings)
    mandatory: false     # true blocks deployments when findings exceed failSeverity
    failSeverity: "HIGH" # CRITICAL | HIGH | MEDIUM | LOW
    sbomEnabled: false   # true generates a CycloneDX SBOM (second Trivy pass, ~30–60 s extra)

Provider selection

provider: "auto" (the default) picks the backend based on the builder.imageRepo URL:

RegistryProviderEnabled by default
ECR (*.ecr.*.amazonaws.com)ECR basic scanningyes (free)
Everything elseTrivy K8s Jobno (opt-in)

Override by setting provider explicitly, or flip the default with enabled: "true" or "false".

ECR scanning

The builder calls StartImageScan after each image push, then polls DescribeImageScanFindings until the scan completes (up to 10 minutes). No extra K8s Job is created. Results are stored as vulnerability counts — no CycloneDX SBOM is produced.

The builder's IAM role needs two additional permissions. See AWS (EKS) — ECR image scanning for the IAM policy.

Trivy scanning

When provider: "trivy" (or auto-selected for non-ECR registries), the builder creates a short-lived K8s Job in the build namespace that runs trivy image against the pushed image. The job produces vulnerability counts and, optionally, a CycloneDX SBOM available for download on the deployment detail page.

SBOM generation requires a second trivy image pass and adds roughly 30–60 seconds to scan time. It is disabled by default:

security:
  scan:
    sbomEnabled: true

Override the Trivy image version:

builder:
  trivyImage: "aquasec/trivy:0.58.0"

Secret references

Several sensitive values can be sourced from existing Kubernetes Secrets rather than plain Helm values. Create the secret in the system namespace before running helm install or helm upgrade.

External database password

Put $(DB_PASSWORD) in the URL where the password goes, then point passwordSecretRef at your secret:

externalDatabase:
  url: "postgresql://myuser:$(DB_PASSWORD)@myhost:5432/mydb?sslmode=require"
  passwordSecretRef:
    name: my-db-secret
    key: password

Kubernetes substitutes $(DB_PASSWORD) at pod startup. Create the secret before running helm install or helm upgrade:

kubectl create secret generic my-db-secret \
  --namespace canette-system \
  --from-literal=password=<your-db-password>

See Production setup for a full walkthrough of using an external database.

External database SSL

Managed databases (AWS RDS, Cloud SQL, Azure Database) use a certificate authority that is not in the default system trust store. Provide the CA certificate so canette can verify the server's TLS certificate.

Option 1 — reference an existing Secret (recommended):

Create a Secret containing the PEM-encoded CA bundle, then point caCertSecretRef at it:

kubectl create secret generic my-db-ca \
  --namespace canette-system \
  --from-file=ca.crt=rds-ca-bundle.pem
externalDatabase:
  url: "postgresql://myuser:$(DB_PASSWORD)@mydb.abc123.us-east-1.rds.amazonaws.com:5432/mydb?sslmode=verify-full"
  ssl:
    caCertSecretRef:
      name: my-db-ca
      key: ca.crt

Option 2 — inline PEM in values:

externalDatabase:
  ssl:
    caCert: |
      -----BEGIN CERTIFICATE-----
      ...
      -----END CERTIFICATE-----

Do not use rejectUnauthorized: false in production. This disables certificate verification entirely, leaving the connection vulnerable to man-in-the-middle attacks. It exists only as an escape hatch for local dev or testing against managed cloud databases where you control the network.

externalDatabase:
  ssl:
    rejectUnauthorized: false

Last resort — disable SSL entirely. If the database server does not require SSL at all, you can turn it off via the connection string. This sends credentials and data in plaintext and must never be used in production.

externalDatabase:
  url: "postgresql://myuser:mypassword@myhost:5432/mydb?sslmode=disable"

No ssl values are needed when SSL is disabled in the connection string — sslmode=disable is handled entirely by the pg driver.

External registry password

externalRegistry:
  username: myuser
  passwordSecretRef:
    name: my-registry-secret
    key: password

The chart looks up the secret at render time to construct the docker config — the secret must exist before helm install.

GitHub client secret

api:
  githubClientId: "my-client-id"
  githubClientSecretRef:
    name: my-github-secret
    key: client-secret

Backup S3 credentials

backup:
  s3:
    secretRef:
      name: canette-backup-s3-credentials
      accessKeyKey: access-key
      secretKeyKey: secret-key

See Backups for the full setup, including the IAM policy.

Encryption key

The encryption key used to protect git credentials and app secrets at rest is auto-generated on first install and stored as a Kubernetes Secret. Every helm upgrade reuses the existing key automatically — no action required.

To provide your own key (disaster recovery, cluster migration):

encryptionKey: "your-32-character-key-here"

Changing the encryption key after secrets have been stored will break decryption of all existing credentials and app secrets.

Upgrading

helm upgrade canette oci://ghcr.io/canette/canette --reuse-values

To upgrade to a specific version:

helm upgrade canette oci://ghcr.io/canette/canette --version 1.2.3 --reuse-values

Migrations run automatically at API startup after the upgrade.

Uninstalling

helm uninstall canette -n canette-system
kubectl delete namespace canette-system canette-system-build

Uninstalling deletes the BuildKit state volume and all in-cluster registry data. Back up any images you want to keep before uninstalling.

Operations

Rotating registry credentials

To rotate credentials without changing other values, use a gitignored secrets file:

# secrets.yaml  (add to .gitignore)
externalRegistry:
  username: myuser
  password: mytoken
helm upgrade canette oci://ghcr.io/canette/canette --reuse-values -f secrets.yaml

After upgrading, restart the builder to pick up the new credentials:

kubectl rollout restart deployment/canette-builder -n canette-system

Updating resource defaults

# values.yaml
api:
  defaultResources:
    cpuRequest: "250m"
    memoryRequest: "256Mi"
    cpuLimit: "1"
    memoryLimit: "1Gi"
helm upgrade canette oci://ghcr.io/canette/canette --reuse-values -f values.yaml

The new values take effect on the next deployment for each app. Existing live apps continue to use the resource values from when they were last deployed.

Restarting a service

Rolling restart with zero downtime — the old pod stays up until the new one is ready:

kubectl rollout restart deployment/canette-builder -n canette-system
kubectl rollout restart deployment/canette-controller -n canette-system

On this page