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
HTTPRoutesupport plus classic Kubernetes Ingress - External Secrets Operator v1 — manage
APP_KEY, exchange-rate API key, and external database passwords - Service dual-stack fields — optional
ipFamilyPolicyandipFamilies - 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: 8GiCreate 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: 1GiCreate 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-passwordmysql:
enabled: false
database:
mode: sqlite
sqlite:
enabled: true
persistence:
database:
enabled: true
size: 5Gi
replicaCount: 1mysql:
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: passwordConfiguration Reference
Database
| Parameter | Default | Description |
|---|---|---|
database.mode | auto | Database mode: auto, mysql, external, or sqlite. |
mysql.enabled | true | Deploy HelmForge MySQL as the primary database. |
mysql.auth.database | discount_bandit | MySQL database created by the subchart. |
mysql.auth.username | discount_bandit | MySQL application user. |
database.external.host | "" | External MySQL or MariaDB hostname. |
database.external.existingSecret | "" | Secret containing the external database password. |
database.sqlite.enabled | false | Enable SQLite development mode. |
Application
| Parameter | Default | Description |
|---|---|---|
discountBandit.appUrl | "" | Public application URL (APP_URL). |
discountBandit.assetUrl | "" | Public asset URL (ASSET_URL). Defaults to appUrl when empty. |
discountBandit.timezone | UTC | Application timezone. |
discountBandit.themeColor | Stone | UI 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
| Parameter | Default | Description |
|---|---|---|
service.type | ClusterIP | Kubernetes Service type. |
service.port | 80 | Service HTTP port. |
service.ipFamilyPolicy | "" | Optional Service IP family policy. |
service.ipFamilies | [] | Optional Service IP families for dual-stack clusters. |
ingress.enabled | false | Render classic Kubernetes Ingress. |
gatewayAPI.enabled | false | Render Gateway API HTTPRoute. |
gatewayAPI.parentRefs | [] | Platform-owned Gateway references. Required when Gateway API is enabled. |
networkPolicy.enabled | false | Render NetworkPolicy. |
networkPolicy.egress.enabled | false | Restrict egress when enabled. |
Runtime and Operations
| Parameter | Default | Description |
|---|---|---|
serviceAccount.create | true | Create a dedicated ServiceAccount. |
serviceAccount.automountServiceAccountToken | false | Avoid mounting Kubernetes API credentials into the app pod. |
externalSecrets.enabled | false | Render External Secrets Operator resources using external-secrets.io/v1. |
pdb.enabled | false | Render a PodDisruptionBudget. |
persistence.database.enabled | true | SQLite data PVC. Used only in SQLite mode. |
persistence.logs.enabled | false | Persist /logs instead of using container-local logs. |
supervisor.configMap.enabled | true | Mount a sanitized Supervisor base config. |
resources | {} | CPU and memory requests/limits. |
Production Guidance
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.
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.
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 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.
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.