Files
website/helm/ezscale-website/README.md
Andrew 4ff3048dd3 fix(helm): hardening from review
- _helpers.tpl: required guard on image.tag — silent empty deploys can
  no longer happen; helm fails fast with a clear message.
- configmap-nginx: HTTPS fastcgi param uses if_not_empty, so PHP only
  sees HTTPS when X-Forwarded-Proto is actually present.
- deployment-app: add startupProbe with 100s budget so first-boot cache
  warmup doesn't trip liveness.
- deployment-horizon: failureThreshold=5 on the horizon:status probe;
  transient Valkey blips no longer cause restart loops.
- job-migrate: mount oauth-keys so seeders that touch Passport clients
  don't silently fail.
- statefulset-valkey: replace separate password Secret with a
  requirePassword toggle that reads REDIS_PASSWORD from the main chart
  Secret (same Secret app/horizon/scheduler already mount). Liveness
  probe authenticates with the password when set.
- values-us-prod: enable valkey.requirePassword.
- README: add REDIS_PASSWORD to bootstrap procedure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 23:08:10 -04:00

5.6 KiB

ezscale-website Helm Chart

Production deployment for the EZSCALE billing platform Laravel app.

Environments

Env Values file Notes
Local k3d values-local.yaml Self-contained: in-cluster MariaDB + Valkey. No ingress (use port-forward).
US prod values-us-prod.yaml Connects to existing mariadb CR in ezscale ns; per-app Valkey on Longhorn.

Prerequisites (us-prod)

The ezscale namespace must already have:

  • mariadb-operator installed cluster-wide (provides MariaDB/Database/User/Grant CRDs)
  • A MariaDB CR named mariadb
  • cert-manager ClusterIssuer named letsencrypt
  • Traefik with cloudflarewarp and http-to-https middlewares in kube-system
  • Image-pull Secret named gitea-registry

These are all already provisioned per infrastructure/kubernetes/.

First-time bootstrap (us-prod)

This step is run once per environment, never repeated. The Secret it creates lives forever — re-running it would invalidate every encrypted DB column and every issued OAuth token.

# 1. Generate APP_KEY locally (any Laravel checkout works)
APP_KEY=$(cd /tmp && composer create-project laravel/laravel _key_gen --quiet --no-install \
  && cd _key_gen && composer install --quiet \
  && php artisan key:generate --show)

# 2. Generate Passport keys
TMP_KEYS=$(mktemp -d)
openssl genrsa -out "$TMP_KEYS/oauth-private.key" 4096
openssl rsa -in "$TMP_KEYS/oauth-private.key" -pubout -out "$TMP_KEYS/oauth-public.key"

# 3. Gather production secrets. Read each one and store in shell vars.
DB_PASS=$(openssl rand -base64 32 | tr -d '/=+' | cut -c1-32)
REDIS_PASS=$(openssl rand -base64 32 | tr -d '/=+' | cut -c1-32)
read -sp "STRIPE_KEY (pk_live_...): " STRIPE_KEY; echo
read -sp "STRIPE_SECRET (sk_live_...): " STRIPE_SECRET; echo
read -sp "STRIPE_WEBHOOK_SECRET (whsec_...): " STRIPE_WEBHOOK; echo
read -sp "PAYPAL_CLIENT_ID: " PAYPAL_ID; echo
read -sp "PAYPAL_CLIENT_SECRET: " PAYPAL_SECRET; echo
read -sp "AWS_ACCESS_KEY_ID (Storj): " AWS_KEY; echo
read -sp "AWS_SECRET_ACCESS_KEY (Storj): " AWS_SECRET; echo
read -sp "MAIL_USERNAME (SMTP): " MAIL_USER; echo
read -sp "MAIL_PASSWORD (SMTP): " MAIL_PASS; echo
read -sp "VIRTFUSION_API_KEY: " VF_KEY; echo
read -sp "PTERODACTYL_APP_KEY: " PTERO_KEY; echo

# 4. Create the Secret in the ezscale namespace
kubectl create secret generic ezscale-website-secrets \
  --namespace ezscale \
  --from-literal=APP_KEY="$APP_KEY" \
  --from-literal=DB_PASSWORD="$DB_PASS" \
  --from-literal=REDIS_PASSWORD="$REDIS_PASS" \
  --from-literal=STRIPE_KEY="$STRIPE_KEY" \
  --from-literal=STRIPE_SECRET="$STRIPE_SECRET" \
  --from-literal=STRIPE_WEBHOOK_SECRET="$STRIPE_WEBHOOK" \
  --from-literal=PAYPAL_CLIENT_ID="$PAYPAL_ID" \
  --from-literal=PAYPAL_CLIENT_SECRET="$PAYPAL_SECRET" \
  --from-literal=AWS_ACCESS_KEY_ID="$AWS_KEY" \
  --from-literal=AWS_SECRET_ACCESS_KEY="$AWS_SECRET" \
  --from-literal=MAIL_USERNAME="$MAIL_USER" \
  --from-literal=MAIL_PASSWORD="$MAIL_PASS" \
  --from-literal=VIRTFUSION_API_KEY="$VF_KEY" \
  --from-literal=PTERODACTYL_APP_KEY="$PTERO_KEY" \
  --from-file=oauth-private.key="$TMP_KEYS/oauth-private.key" \
  --from-file=oauth-public.key="$TMP_KEYS/oauth-public.key"

# 5. Wipe the local key copies
rm -rf /tmp/_key_gen "$TMP_KEYS"

After this, helm install and every subsequent helm upgrade reference the existing Secret without ever rewriting it.

Deploy

helm upgrade --install ezscale-website ./helm/ezscale-website \
  --namespace ezscale \
  -f helm/ezscale-website/values-us-prod.yaml \
  --set image.tag=v0.1.0

Local dev cluster

# 1. Create cluster with Longhorn (or local-path) and the operator
k3d cluster create ezscale-local
helm repo add mariadb-operator https://helm.mariadb.com/mariadb-operator
helm install mariadb-operator -n mariadb-operator --create-namespace mariadb-operator/mariadb-operator

# 2. Build the image into the cluster
docker build --target app -t ezscale-website:app-local .
docker build --target horizon -t ezscale-website:horizon-local .
docker build --target scheduler -t ezscale-website:scheduler-local .
k3d image import ezscale-website:app-local ezscale-website:horizon-local ezscale-website:scheduler-local -c ezscale-local

# 3. Edit values-local.yaml — replace APP_KEY with `php artisan key:generate --show` output

# 4. Install
helm upgrade --install ezscale-website ./helm/ezscale-website \
  --namespace ezscale --create-namespace \
  -f helm/ezscale-website/values-local.yaml \
  --set image.registry=docker.io \
  --set image.repository=library/ezscale-website \
  --set image.tag=local

# 5. Port-forward and visit http://localhost:8080
kubectl port-forward -n ezscale svc/ezscale-website 8080:80

Operations

# Tail Horizon logs
kubectl logs -n ezscale -l app.kubernetes.io/component=horizon -f

# Run an artisan command in a one-off pod
kubectl run -n ezscale --rm -it --image=git.ezscale.cloud/ezscale/website:app-v0.1.0 \
  --env-from='[{"configMapRef":{"name":"ezscale-website-env"}},{"secretRef":{"name":"ezscale-website-secrets"}}]' \
  artisan-shell -- bash

# Force a Horizon rolling restart
kubectl rollout restart deployment -n ezscale ezscale-website-horizon

What's NOT in this chart (intentionally)

  • Cloudflare Zero Trust for the admin panel — layered on later.
  • ExternalDNS records — DNS managed manually in Cloudflare Terraform.
  • Backup CronJob for the application database — covered by the existing mariadb instance's backup CronJob in infrastructure/kubernetes/ezscale/mysql/.
  • Backups for Storj uploads — Storj provides its own replication; per-bucket lifecycle policies live in the Storj console.