> ## Documentation Index
> Fetch the complete documentation index at: https://sure-917046f5-docs-cloudflare-tunnel-self-hosting.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Self-hosting with Helm

> Deploy Sure on Kubernetes using the official Helm chart

This guide shows you how to deploy Sure on Kubernetes using the official Helm chart. The chart supports web (Rails) and worker (Sidekiq) workloads, optional in-cluster PostgreSQL and Redis, and production-grade features like pre-upgrade migrations, pod security contexts, and horizontal pod autoscaling.

## Prerequisites

* Kubernetes >= 1.25
* Helm >= 3.10
* Basic familiarity with Kubernetes and Helm

## Features

* Web (Rails) deployment with service and optional ingress
* Worker (Sidekiq) deployment
* Optional database migrations via Helm hook job or initContainer
* Optional subcharts for PostgreSQL (CloudNativePG) and Redis (OT-CONTAINER-KIT redis-operator)
* Security best practices: runAsNonRoot, readOnlyRootFilesystem, no hardcoded secrets
* Scalability: replicas, resources, topology spread constraints, optional HPAs
* Optional CronJobs for custom tasks

## Installation

### Add Helm repositories

Add the Sure Helm repository:

```bash theme={null}
helm repo add sure https://we-promise.github.io/sure
helm repo update
```

If you plan to use the bundled PostgreSQL or Redis subcharts, add their repositories as well:

```bash theme={null}
helm repo add cloudnative-pg https://cloudnative-pg.github.io/charts
helm repo add ot-helm https://ot-container-kit.github.io/helm-charts
helm repo update
```

### Quickstart (turnkey self-hosting)

This installs CloudNativePG operator with a Postgres cluster and Redis managed by the OT redis-operator.

<Warning>
  For production stability, use immutable image tags (for example, `image.tag=v1.2.3`) instead of `latest`.
</Warning>

<Warning>
  Do not pass production secrets with `--set` on the command line. Use a values file with restricted permissions or a Kubernetes Secret as described in [Secrets management](#secrets-management).
</Warning>

```bash theme={null}
# Create namespace
kubectl create ns sure || true

# Create a values file for the initial secret
SECRET_KEY_BASE=$(openssl rand -hex 32)
cat > values-quickstart.yaml <<EOF
image:
  tag: v1.2.3
rails:
  secret:
    enabled: true
    values:
      SECRET_KEY_BASE: "$SECRET_KEY_BASE"
EOF
chmod 600 values-quickstart.yaml

# Install chart with a pinned image tag
helm upgrade --install sure sure/sure \
  -n sure \
  -f values-quickstart.yaml
```

Expose the app via an ingress (see configuration below) or port-forward:

```bash theme={null}
kubectl port-forward svc/sure 8080:80 -n sure
```

Navigate to `http://localhost:8080` to access Sure.

## Configuration

### Using external Postgres and Redis

To use external managed databases instead of the bundled subcharts, store connection strings in a Kubernetes Secret and reference them from your values file. See [Secrets management](#secrets-management) for the general pattern.

```yaml theme={null}
cnpg:
  enabled: false

redisOperator:
  managed:
    enabled: false

redisSimple:
  enabled: false

rails:
  extraEnv:
    DATABASE_URL:
      valueFrom:
        secretKeyRef:
          name: external-db-credentials
          key: database-url
    REDIS_URL:
      valueFrom:
        secretKeyRef:
          name: external-db-credentials
          key: redis-url
```

### Deployment profiles

#### Simple single-node

Minimal setup for development or small deployments:

* `encryptionEnv.enabled: true` tells the chart to wire the Active Record encryption keys from your secret into the app container.
* `SELF_HOSTED: "true"` enables self-hosted behavior inside Sure.
* Replace `longhorn` with a storage class that exists in your cluster.

```yaml theme={null}
image:
  repository: ghcr.io/we-promise/sure
  tag: "v1.0.0"
  pullPolicy: IfNotPresent

rails:
  existingSecret: sure-secrets
  encryptionEnv:
    enabled: true
  settings:
    SELF_HOSTED: "true"

cnpg:
  enabled: true
  cluster:
    enabled: true
    name: sure-db
    instances: 1
    storage:
      size: 8Gi
      storageClassName: longhorn

redisOperator:
  enabled: true
  managed:
    enabled: true
  mode: standalone
  replicas: 1
  persistence:
    enabled: true
    className: longhorn
    size: 8Gi

migrations:
  strategy: job
```

#### HA k3s profile

High availability setup with multiple replicas and synchronous replication:

```yaml theme={null}
cnpg:
  enabled: true
  cluster:
    enabled: true
    name: sure-db
    instances: 3
    storage:
      size: 20Gi
      storageClassName: longhorn
    minSyncReplicas: 1
    maxSyncReplicas: 2
    topologySpreadConstraints:
      - maxSkew: 1
        topologyKey: kubernetes.io/hostname
        whenUnsatisfiable: ScheduleAnyway
        labelSelector:
          matchLabels:
            cnpg.io/cluster: sure-db

redisOperator:
  enabled: true
  managed:
    enabled: true
  mode: replication
  replicas: 3
  persistence:
    enabled: true
    className: longhorn
    size: 8Gi

migrations:
  strategy: job
  initContainer:
    enabled: true

hpa:
  web:
    enabled: true
    minReplicas: 2
    maxReplicas: 10
    targetCPUUtilizationPercentage: 70
```

### Secrets management

Generate strong secrets before creating the Kubernetes Secret:

```bash theme={null}
openssl rand -hex 32   # SECRET_KEY_BASE
openssl rand -hex 32   # ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY
openssl rand -hex 32   # ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY
openssl rand -hex 32   # redis-password
openssl rand -hex 64   # ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT
```

Create a Kubernetes secret with the required credentials:

```yaml theme={null}
apiVersion: v1
kind: Secret
metadata:
  name: sure-secrets
type: Opaque
stringData:
  # Rails secrets
  SECRET_KEY_BASE: "__SET_SECRET__"

  # Active Record Encryption keys (required for self-hosted mode)
  ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY: "__SET_SECRET__"
  ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY: "__SET_SECRET__"
  ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT: "__SET_SECRET__"

  # Redis password
  redis-password: "__SET_SECRET__"

  # Optional external service URLs
  database-url: "postgresql://user:pass@db.example.com:5432/sure"
  redis-url: "redis://:pass@redis.example.com:6379/0"
```

Apply the secret:

```bash theme={null}
kubectl apply -f sure-secrets.yaml -n sure
```

Reference the secret in your values:

```yaml theme={null}
rails:
  existingSecret: sure-secrets

redisOperator:
  managed:
    enabled: true
  auth:
    existingSecret: sure-secrets
    passwordKey: redis-password
```

### Ingress configuration

Enable ingress to expose Sure externally:

```yaml theme={null}
ingress:
  enabled: true
  className: "nginx"
  hosts:
    - host: finance.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - hosts: [finance.example.com]
      secretName: finance-tls
```

### Horizontal pod autoscaling

Enable HPAs for automatic scaling based on CPU utilization:

```yaml theme={null}
hpa:
  web:
    enabled: true
    minReplicas: 2
    maxReplicas: 10
    targetCPUUtilizationPercentage: 70

  worker:
    enabled: true
    minReplicas: 2
    maxReplicas: 10
    targetCPUUtilizationPercentage: 70
```

## Updating

To update to a new version of Sure:

```bash theme={null}
# Update the Helm repository
helm repo update sure

# Update the deployment with a new image tag
helm upgrade sure sure/sure -n sure \
  --set image.tag=v1.2.4 \
  -f your-values.yaml
```

The chart will automatically run database migrations before deploying the new version.

## Backup and restore

### PostgreSQL backups with CloudNativePG

CloudNativePG supports volume snapshot backups:

```yaml theme={null}
cnpg:
  cluster:
    backup:
      method: volumeSnapshot
      volumeSnapshot:
        className: longhorn
```

### Manual backup

Create a manual backup of your PostgreSQL database:

```bash theme={null}
# Get the primary pod name
PRIMARY_POD=$(kubectl get pod -n sure -l cnpg.io/cluster=sure-db,role=primary -o name)

# Create a backup
kubectl exec -n sure $PRIMARY_POD -- pg_dump -U sure sure_production > backup.sql
```

> \[!NOTE]
> If you customized the PostgreSQL username or database name in your chart values, replace `sure` and `sure_production` in the backup and restore commands below.

### Restore from backup

```bash theme={null}
# Copy backup to pod
kubectl cp backup.sql sure/$PRIMARY_POD:/tmp/backup.sql

# Restore
kubectl exec -n sure $PRIMARY_POD -- psql -U sure sure_production -f /tmp/backup.sql
```

## Troubleshooting

### View logs

```bash theme={null}
# Web logs
kubectl logs -n sure -l app.kubernetes.io/component=web

# Worker logs
kubectl logs -n sure -l app.kubernetes.io/component=worker

# Migration job logs
kubectl logs -n sure -l job-name=sure-migrate
```

### Check pod status

```bash theme={null}
kubectl get pods -n sure
```

### Verify database connectivity

```bash theme={null}
# Test connection from web pod
kubectl exec -n sure deploy/sure-web -- rails runner "puts ActiveRecord::Base.connection.execute('SELECT 1').first"
```

### Run Helm tests

After installation, verify the deployment:

```bash theme={null}
helm test sure -n sure
```

## Uninstall

To remove Sure from your cluster:

```bash theme={null}
helm uninstall sure -n sure
```

<Warning>
  This will not delete PersistentVolumeClaims. Before deleting PVCs, make sure the namespace does not contain volumes for anything other than Sure.
</Warning>

```bash theme={null}
kubectl get pvc -n sure
kubectl delete pvc -n sure <sure-pvc-name>
```

## Getting help

If you find bugs or have feature requests:

* Read the [contributing guide](https://github.com/we-promise/sure/wiki/How-to-Contribute-Effectively-to-Sure)
* Ask in the [Discord](https://discord.gg/36ZGBsxYEK)
* Open an [issue](https://github.com/we-promise/sure/issues/new/choose)
