Skip to content

Discount Bandit

Self-hosted price tracker for products across multiple stores. Discount Bandit provides a Laravel and FrankenPHP web UI, scheduled crawling, price history, currency conversion, user management, and notification integrations such as email, Ntfy, Telegram, and Gotify.

The HelmForge chart defaults to the HelmForge MySQL subchart for a Kubernetes-friendly database lifecycle. SQLite remains available for development and small single-replica installs.

Key Features

  • HelmForge MySQL by default — primary database path with persistence and chart-managed credentials
  • External MySQL or MariaDB — connect to an operator-managed or platform-managed database
  • SQLite dev mode — single-replica setup with a focused PVC mount for local or personal testing
  • Gateway API and Ingress — native HTTPRoute support plus classic Kubernetes Ingress
  • External Secrets Operator v1 — manage APP_KEY, exchange-rate API key, and external database passwords
  • Service dual-stack fields — optional ipFamilyPolicy and ipFamilies
  • NetworkPolicy and PDB — optional production controls for ingress, crawler egress, database egress, and disruption
  • Supervisor hardening — removes the upstream unauthenticated Supervisor HTTP endpoint from the generated config

Architecture

Users
  |
  v
Gateway API HTTPRoute or Ingress
  |
  v
Service discount-bandit
  |
  v
Deployment discount-bandit
  |-- FrankenPHP web UI
  |-- Laravel scheduler
  |-- Laravel queue worker
  |-- Chromium crawler runtime
  |
  v
Service <release>-mysql
  |
  v
StatefulSet mysql
  |
  v
PVC MySQL data

For production, use the bundled MySQL subchart or an external MySQL/MariaDB service. SQLite is intentionally documented as a development path because it is single-writer and normally uses ReadWriteOnce storage.

Installation

HTTPS repository:

helm repo add helmforge https://repo.helmforge.dev
helm repo update
helm install discount-bandit helmforge/discount-bandit -n discount-bandit --create-namespace

OCI registry:

helm install discount-bandit oci://ghcr.io/helmforgedev/helm/discount-bandit -n discount-bandit --create-namespace

Default values deploy Discount Bandit with HelmForge MySQL.

Access

kubectl port-forward -n discount-bandit svc/discount-bandit-discount-bandit 8080:80

Open http://localhost:8080 and create the first admin account.

Deployment Examples

# values.yaml - default path, shown explicitly
mysql:
  enabled: true
  auth:
    database: discount_bandit
    username: discount_bandit
  standalone:
    persistence:
      enabled: true
      size: 8Gi

Create the Secrets referenced by the production values before installing or upgrading the release:

kubectl create namespace discount-bandit
kubectl create secret generic discount-bandit-app -n discount-bandit \
  --from-literal=app-key="base64:$(openssl rand -base64 32)"
kubectl create secret generic discount-bandit-mysql -n discount-bandit \
  --from-literal=mysql-root-password="$(openssl rand -base64 24)" \
  --from-literal=mysql-user-password="$(openssl rand -base64 24)" \
  --from-literal=mysql-replication-password="$(openssl rand -base64 24)"
discountBandit:
  appUrl: https://deals.example.com
  assetUrl: https://deals.example.com
  timezone: UTC
  themeColor: Stone
  cron: '*/10 * * * *'
  existingSecret: discount-bandit-app

mysql:
  enabled: true
  auth:
    database: discount_bandit
    username: discount_bandit
    existingSecret: discount-bandit-mysql
  standalone:
    persistence:
      enabled: true
      size: 20Gi

gatewayAPI:
  enabled: true
  parentRefs:
    - name: public-gateway
      namespace: gateway-system
  hostnames:
    - deals.example.com

networkPolicy:
  enabled: true
  ingress:
    extraFrom:
      - namespaceSelector:
          matchLabels:
            kubernetes.io/metadata.name: gateway-system
  egress:
    enabled: true
    allowDNS: true
    allowHTTPS: true
    allowSameNamespaceDatabase: true

resources:
  requests:
    cpu: 250m
    memory: 512Mi
  limits:
    memory: 1Gi

Create the external database password Secret before installing or upgrading the release:

kubectl create namespace discount-bandit
kubectl create secret generic discount-bandit-db -n discount-bandit \
  --from-literal=database-password="$(openssl rand -base64 24)"
mysql:
  enabled: false

database:
  mode: external
  external:
    type: mysql
    host: mysql.example.internal
    port: 3306
    name: discount_bandit
    username: discount_bandit
    existingSecret: discount-bandit-db
    existingSecretPasswordKey: database-password
mysql:
  enabled: false

database:
  mode: sqlite
  sqlite:
    enabled: true

persistence:
  database:
    enabled: true
    size: 5Gi

replicaCount: 1
mysql:
  enabled: false

database:
  mode: external
  external:
    host: mysql.example.internal
    name: discount_bandit
    username: discount_bandit

externalSecrets:
  enabled: true
  secretStoreRef:
    name: production-secrets
    kind: ClusterSecretStore
  app:
    enabled: true
    appKeyRemoteRef:
      key: apps/discount-bandit
      property: app-key
    exchangeRateApiKeyRemoteRef:
      key: apps/discount-bandit
      property: exchange-rate-api-key
  database:
    enabled: true
    passwordRemoteRef:
      key: databases/discount-bandit
      property: password

Configuration Reference

Database

ParameterDefaultDescription
database.modeautoDatabase mode: auto, mysql, external, or sqlite.
mysql.enabledtrueDeploy HelmForge MySQL as the primary database.
mysql.auth.databasediscount_banditMySQL database created by the subchart.
mysql.auth.usernamediscount_banditMySQL application user.
database.external.host""External MySQL or MariaDB hostname.
database.external.existingSecret""Secret containing the external database password.
database.sqlite.enabledfalseEnable SQLite development mode.

Application

ParameterDefaultDescription
discountBandit.appUrl""Public application URL (APP_URL).
discountBandit.assetUrl""Public asset URL (ASSET_URL). Defaults to appUrl when empty.
discountBandit.timezoneUTCApplication timezone.
discountBandit.themeColorStoneUI theme color.
discountBandit.cron*/5 * * * *Product crawl schedule.
discountBandit.existingSecret""Existing Secret for APP_KEY and optional exchange-rate key.
discountBandit.exchangeRateApiKey""ExchangeRate API key. Prefer a Secret or ExternalSecret in production.
discountBandit.extraEnv[]Extra environment variables for mail, notifications, or advanced Laravel config.

Networking

ParameterDefaultDescription
service.typeClusterIPKubernetes Service type.
service.port80Service HTTP port.
service.ipFamilyPolicy""Optional Service IP family policy.
service.ipFamilies[]Optional Service IP families for dual-stack clusters.
ingress.enabledfalseRender classic Kubernetes Ingress.
gatewayAPI.enabledfalseRender Gateway API HTTPRoute.
gatewayAPI.parentRefs[]Platform-owned Gateway references. Required when Gateway API is enabled.
networkPolicy.enabledfalseRender NetworkPolicy.
networkPolicy.egress.enabledfalseRestrict egress when enabled.

Runtime and Operations

ParameterDefaultDescription
serviceAccount.createtrueCreate a dedicated ServiceAccount.
serviceAccount.automountServiceAccountTokenfalseAvoid mounting Kubernetes API credentials into the app pod.
externalSecrets.enabledfalseRender External Secrets Operator resources using external-secrets.io/v1.
pdb.enabledfalseRender a PodDisruptionBudget.
persistence.database.enabledtrueSQLite data PVC. Used only in SQLite mode.
persistence.logs.enabledfalsePersist /logs instead of using container-local logs.
supervisor.configMap.enabledtrueMount a sanitized Supervisor base config.
resources{}CPU and memory requests/limits.

Production Guidance

Use MySQL for production

Discount Bandit supports SQLite, but Kubernetes production installs should use HelmForge MySQL or an external MySQL/MariaDB database. SQLite is best kept for development and small single-user testing.

Plan crawler egress before enabling strict NetworkPolicy

Discount Bandit crawls public store pages and may call notification providers or exchange-rate APIs. Start with open egress, identify required destinations, then enable NetworkPolicy rules that match your environment.

External Secrets are optional

The chart can render external-secrets.io/v1 resources, but it does not install the External Secrets Operator or a SecretStore. Install and validate those platform components before enabling externalSecrets.enabled in production.

Common Issues

SQLite data hidden by an incorrect mount

SQLite mode mounts only /app/database/sqlite so upstream migrations and seeders under /app/database remain visible. Do not replace this with a broad /app/database mount.

Prices not updating

Price updates are driven by the Laravel scheduler and queue worker inside the container. Check pod logs and confirm outbound access to target stores, notification providers, and exchange-rate APIs.

More Information