Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,19 @@ docker run -p 3000:3000 --env-file .env quackback

Requires PostgreSQL and a Redis-compatible store. Set `DATABASE_URL` and `REDIS_URL` in `.env`. Migrations run automatically on startup.

### Kubernetes

```bash
helm install quackback ./deploy/kubernetes/quackback \
--namespace quackback --create-namespace \
--set secretKey="$(openssl rand -base64 32)" \
--set baseUrl=https://feedback.example.com \
--set ingress.host=feedback.example.com \
--set ingress.tls.secretName=feedback-tls
```

See the [Helm chart guide](deploy/kubernetes/quackback/README.md) for full configuration, including running against an external database and object storage.

## Contributing

See the [Contributing Guide](CONTRIBUTING.md) to get started.
Expand Down
31 changes: 29 additions & 2 deletions deploy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ This directory contains deployment configurations for Quackback.
| Option | For | Infrastructure |
| ------------------------------------------ | ---------------------------- | -------------------- |
| **[Self-Hosted](./self-hosted/README.md)** | Community & Enterprise users | Docker, Bun, any VPS |
| **[Kubernetes](./kubernetes/quackback/README.md)** | Community & Enterprise users | Kubernetes, Helm |
| **[Cloud](./cloud/README.md)** | Quackback team only | Cloudflare Workers |

---
Expand Down Expand Up @@ -43,6 +44,26 @@ See the [Self-Hosted Guide](./self-hosted/README.md) for complete documentation.

---

## Kubernetes

Running on a cluster instead of a single host? Use the Helm chart at
[`kubernetes/quackback`](./kubernetes/quackback) — it bundles the same
Postgres/Dragonfly/MinIO stack as `docker-compose.prod.yml`, plus a
`pre-upgrade` migration hook Job (per the guidance in `apps/web/Dockerfile`).

```bash
helm install quackback ./deploy/kubernetes/quackback \
--namespace quackback --create-namespace \
--set secretKey="$(openssl rand -base64 32)" \
--set baseUrl=https://feedback.example.com \
--set ingress.host=feedback.example.com \
--set ingress.tls.secretName=feedback-tls
```

See the [Helm Chart Guide](./kubernetes/quackback/README.md) for full configuration.

---

## Quackback Cloud (Internal)

The `cloud/` directory contains Cloudflare Workers deployment configuration for Quackback Cloud (app.quackback.io).
Expand All @@ -65,8 +86,14 @@ deploy/
│ ├── wrangler.production.jsonc # Production environment
│ ├── .dev.vars.example # Secrets template
│ └── .gitignore # Ignores .dev.vars
└── self-hosted/ # Self-hosting documentation
└── README.md # Self-hosted deployment guide
├── self-hosted/ # Self-hosting documentation
│ └── README.md # Self-hosted deployment guide
└── kubernetes/
└── quackback/ # Helm chart
├── README.md # Helm chart guide
├── Chart.yaml
├── values.yaml
└── templates/
```

---
Expand Down
11 changes: 11 additions & 0 deletions deploy/kubernetes/quackback/.helmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Patterns to ignore when building packages.
.DS_Store
.git/
.gitignore
.bak
*.orig
*~
.project
.idea/
*.tmproj
.vscode/
20 changes: 20 additions & 0 deletions deploy/kubernetes/quackback/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
apiVersion: v2
name: quackback
description: Open-source feedback platform — boards, roadmap, changelog, and an AI-powered inbox.
type: application
version: 0.1.0
appVersion: "0.13.1"
home: https://quackback.io
icon: https://raw.githubusercontent.com/QuackbackIO/quackback/main/.github/logo.svg
sources:
- https://github.com/QuackbackIO/quackback
keywords:
- feedback
- roadmap
- changelog
- support
- helpdesk
maintainers:
- name: Quackback
url: https://quackback.io
email: support@quackback.io
162 changes: 162 additions & 0 deletions deploy/kubernetes/quackback/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# Quackback Helm Chart

Deploy Quackback on Kubernetes with a single, self-contained chart: the app plus
bundled PostgreSQL, Dragonfly (Redis-compatible queue store), and MinIO
(S3-compatible object storage) — the same stack `docker-compose.prod.yml`
bundles for single-host self-hosting, adapted for a cluster.

## Table of Contents

- [Quick Start](#quick-start)
- [Migrations](#migrations)
- [Configuration](#configuration)
- [Bundled vs. External Datastores](#bundled-vs-external-datastores)
- [Ingress](#ingress)
- [Scheduled Jobs](#scheduled-jobs)
- [Upgrading](#upgrading)
- [Uninstalling](#uninstalling)

## Quick Start

```bash
helm install quackback ./deploy/kubernetes/quackback \
--namespace quackback --create-namespace \
--set secretKey="$(openssl rand -base64 32)" \
--set baseUrl=https://feedback.example.com \
--set ingress.host=feedback.example.com \
--set ingress.tls.secretName=feedback-tls
```

This brings up the app plus in-cluster Postgres, Dragonfly, and MinIO —
enough to try Quackback end to end. `ingress.tls.secretName` must name a
TLS secret that already exists in the namespace (e.g. one managed by
cert-manager); the chart doesn't create certificates itself.

Prefer a values file for anything beyond a quick trial — see
[`values.yaml`](values.yaml) for every field, each documented inline.

```bash
helm install quackback ./deploy/kubernetes/quackback \
--namespace quackback --create-namespace \
-f my-values.yaml
```

## Migrations

`apps/web/Dockerfile` documents the intended Kubernetes pattern: skip the
container's built-in startup migration and run migrations from a Helm hook
Job instead, so a rolling deploy doesn't pay a DB round-trip on every cold
start. This chart implements that directly:

- `templates/migration-job.yaml` runs `bun /app/migrate.mjs` as a
`pre-install,pre-upgrade` hook — it must succeed before the release
proceeds.
- The app Deployment gets `SKIP_MIGRATIONS=true` automatically whenever
`migrations.enabled` is `true` (the default), so it never re-runs them.

Set `migrations.enabled: false` only if you're managing migrations through
some other out-of-band process; in that case the app's `SKIP_MIGRATIONS` is
left unset and it falls back to running migrations on container start (the
same behavior as `docker run`).

## Configuration

The chart covers the same environment variables as
[`.env.prod.example`](../../../.env.prod.example) — see that file for the
full, authoritative list (including every integration: Slack, Linear, Jira,
Discord, etc.). Commonly-set fields:

| Value | Purpose |
| --------------------------- | ----------------------------------------------------------------- |
| `secretKey` | Required. Session signing/encryption key, 32+ chars. |
| `baseUrl` | Required. Public URL — drives auth, emails, OAuth callbacks. |
| `email.smtp.*` / `email.resendApiKey` | Outbound email. Leave unset to log OTP/invite codes to the pod's console. |
| `oauth.github.*` / `oauth.google.*` | OAuth sign-in providers. |
| `ai.openaiApiKey` / `ai.openaiBaseUrl` / `ai.chatModel` / `ai.embeddingModel` | AI features (summaries, duplicate detection, extraction) — all four must be set together. |
| `app.extraEnv` / `app.extraEnvFrom` | Escape hatch for anything else in `.env.prod.example` (integrations, `EMAIL_INBOUND_*`, per-feature `AI_*_MODEL` overrides). |

Values not set fall back to the same defaults the app itself uses (see
`docker-entrypoint.sh` and the app's env validation) — nothing in this chart
invents new defaults beyond what self-hosting already documents.

## Bundled vs. External Datastores

Each datastore defaults to **enabled** (bundled, in-cluster) so the chart is
usable standalone. For production, point at managed services instead:

| Datastore | Disable with | Then set |
| ---------- | ------------------------ | ------------------------ |
| PostgreSQL | `postgres.enabled=false` | `externalDatabaseUrl` |
| Dragonfly | `dragonfly.enabled=false`| `externalRedisUrl` |
| MinIO | `minio.enabled=false` | your own `S3_*` vars via `app.extraEnv` |

The bundled PostgreSQL image (`pgvector/pgvector:pg17`) has `pgvector` but
not `pg_cron` — it's meant for evaluation, not production. If you rely on
the scheduled jobs `pg_cron` normally drives (see
[Scheduled Jobs](#scheduled-jobs) below and the self-hosting docs), use an
external Postgres built from [`docker/postgres/Dockerfile`](../../../docker/postgres/Dockerfile)
(which enables both extensions), or handle the SLA tick via this chart's
`slaTickCronJob` instead.

Generated passwords (bundled Postgres/MinIO, when left blank) are stable
across `helm upgrade` — the chart reads back the existing Secret instead of
re-rolling a new one, so upgrades never desync from data already written to
the PVC.

## Ingress

`ingress.className: nginx` (the default) also adds a few
`nginx.ingress.kubernetes.io/*` annotations (body size, redirect, timeouts)
tuned for Quackback's upload endpoints. Any other `ingress.className`
(`traefik`, `caddy`, ...) — both mentioned as supported reverse proxies in
the [self-hosting guide](../self-hosted/README.md#reverse-proxy) — skips
those and applies only `ingress.annotations` verbatim, so you can add
controller-specific annotations (e.g. `cert-manager.io/cluster-issuer`)
without fighting nginx-specific ones.

Set `ingress.enabled: false` to skip the Ingress entirely and expose the
`ClusterIP` Service through your own means (a Gateway API `HTTPRoute`, a
different ingress mechanism, `kubectl port-forward`, ...).

## Scheduled Jobs

The self-hosting guide's [pg_cron section](../self-hosted/README.md#scheduled-jobs-pg_cron)
recommends two jobs: an SLA escalation tick (`POST /api/v1/internal/sla-tick`
every minute) and a webhook-delivery audit purge. This chart adds an
optional CronJob for the first one — the one with an HTTP endpoint to call:

```yaml
internalTaskSecret: "<same value as your pg_cron setup's x-internal-secret, or a fresh one>"
slaTickCronJob:
enabled: true
```

The webhook-delivery purge is a raw SQL statement, not an HTTP endpoint —
run it via `pg_cron` on whichever Postgres you're using (bundled or
external), per the self-hosting guide.

## Upgrading

```bash
helm upgrade quackback ./deploy/kubernetes/quackback -f my-values.yaml
```

The `pre-upgrade` migration hook runs and must succeed before the app's
Deployment is touched. Back up your database first if you're using the
bundled PostgreSQL — this chart doesn't automate backups (see
[Database Backups](../self-hosted/README.md#database-backups) for the
`pg_dump`/`pg_restore` commands; run them via `kubectl exec` into the
`*-postgres-0` pod).

## Uninstalling

```bash
helm uninstall quackback -n quackback
```

PersistentVolumeClaims (Postgres data, Dragonfly data, MinIO data) are not
deleted automatically — remove them explicitly if you want a clean slate:

```bash
kubectl -n quackback delete pvc -l app.kubernetes.io/instance=quackback
```
32 changes: 32 additions & 0 deletions deploy/kubernetes/quackback/templates/NOTES.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
Quackback has been deployed.

{{- if .Values.ingress.enabled }}

Once the ingress controller has provisioned, your instance will be available at:
{{ if .Values.ingress.tls.enabled }}https{{ else }}http{{ end }}://{{ .Values.ingress.host }}
{{- else }}

No ingress was created (ingress.enabled=false). Reach the app with:
kubectl -n {{ .Release.Namespace }} port-forward svc/{{ include "quackback.fullname" . }} 3000:{{ .Values.app.port }}
then open http://localhost:3000
{{- end }}

{{- if .Values.migrations.enabled }}

Migrations run automatically via a pre-install/pre-upgrade hook Job
({{ include "quackback.fullname" . }}-migrate). Check its status with:
kubectl -n {{ .Release.Namespace }} logs job/{{ include "quackback.fullname" . }}-migrate
{{- end }}

{{- if .Values.postgres.enabled }}

A bundled PostgreSQL StatefulSet is running for evaluation. For production,
point at a managed database instead: set postgres.enabled=false and
externalDatabaseUrl, then see the note on pg_cron in
deploy/kubernetes/README.md.
{{- end }}

Check the rollout with:
kubectl -n {{ .Release.Namespace }} rollout status deployment/{{ include "quackback.fullname" . }}

Docs: https://quackback.io/docs/self-hosting
97 changes: 97 additions & 0 deletions deploy/kubernetes/quackback/templates/_helpers.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "quackback.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Create a default fully qualified app name.
*/}}
{{- define "quackback.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}

{{/*
Common labels
*/}}
{{- define "quackback.labels" -}}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
app.kubernetes.io/version: {{ .Values.app.image.tag | default .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{ include "quackback.selectorLabels" . }}
{{- end }}

{{/*
Selector labels
*/}}
{{- define "quackback.selectorLabels" -}}
app.kubernetes.io/name: {{ include "quackback.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

{{/*
Image tag, defaulting to the chart's appVersion (kept in sync with the
project's latest tested release — see Chart.yaml).
*/}}
{{- define "quackback.appImage" -}}
{{- printf "%s:%s" .Values.app.image.repository (.Values.app.image.tag | default .Chart.AppVersion) -}}
{{- end }}

{{/*
Redis/Dragonfly internal URL. Only meaningful when dragonfly.enabled — the
bundled Dragonfly runs with no auth, same as docker-compose.prod.yml.
*/}}
{{- define "quackback.redisUrl" -}}
redis://{{ include "quackback.fullname" . }}-dragonfly:6379
{{- end }}

{{/*
MinIO internal endpoint.
*/}}
{{- define "quackback.minioEndpoint" -}}
http://{{ include "quackback.fullname" . }}-minio:9000
{{- end }}

{{/*
Stable Postgres password: reused across `helm upgrade` by reading it back
from the already-deployed Secret (lookup), so the bundled StatefulSet's PVC
never ends up holding a password older than the current Secret. Falls back
to postgres.password, then to a random 24-char value on first install.
*/}}
{{- define "quackback.postgresPassword" -}}
{{- $secretName := printf "%s-postgres" (include "quackback.fullname" .) -}}
{{- $existing := lookup "v1" "Secret" .Release.Namespace $secretName -}}
{{- if $existing }}
{{- index $existing.data "POSTGRES_PASSWORD" | b64dec -}}
{{- else if .Values.postgres.password }}
{{- .Values.postgres.password -}}
{{- else -}}
{{- randAlphaNum 24 -}}
{{- end -}}
{{- end }}

{{/*
Stable MinIO root password — same reuse-on-upgrade rationale as
quackback.postgresPassword above.
*/}}
{{- define "quackback.minioPassword" -}}
{{- $secretName := printf "%s-minio" (include "quackback.fullname" .) -}}
{{- $existing := lookup "v1" "Secret" .Release.Namespace $secretName -}}
{{- if $existing }}
{{- index $existing.data "MINIO_ROOT_PASSWORD" | b64dec -}}
{{- else if .Values.minio.rootPassword }}
{{- .Values.minio.rootPassword -}}
{{- else -}}
{{- randAlphaNum 24 -}}
{{- end -}}
{{- end }}
Loading